opcotech/elemo

View on GitHub
internal/service/permission.go

Summary

Maintainability
A
1 hr
Test Coverage
C
74%
package service

import (
    "context"
    "errors"

    "github.com/opcotech/elemo/internal/model"
    "github.com/opcotech/elemo/internal/pkg"
    "github.com/opcotech/elemo/internal/repository"
)

// PermissionService serves the business logic of interacting with permissions.
type PermissionService interface {
    // Create creates a new permission. If the permission already exists, an
    // additional permission is created.
    Create(ctx context.Context, perm *model.Permission) error
    // CtxUserCreate creates a new permission if the user in the context has the
    // permission to create a new permission. If the permission already exists,
    // an additional permission is created. If the user in the context does not
    // have the permission to create a new permission, an error is returned.
    CtxUserCreate(ctx context.Context, perm *model.Permission) error
    // Get returns the permission with the given ID. If the permission does not
    // exist, an error is returned.
    Get(ctx context.Context, id model.ID) (*model.Permission, error)
    // GetBySubject returns all permissions where the subject is the given ID.
    // If no permissions exist, an error is returned.
    GetBySubject(ctx context.Context, id model.ID) ([]*model.Permission, error)
    // GetByTarget returns all permissions where the target is the given ID. If
    // no permissions exist, an error is returned.
    GetByTarget(ctx context.Context, id model.ID) ([]*model.Permission, error)
    // GetBySubjectAndTarget returns all permissions where the subject and the
    // target are both provided. If no permissions exist, an error is returned.
    GetBySubjectAndTarget(ctx context.Context, source, target model.ID) ([]*model.Permission, error)
    // HasAnyRelation checks if the subject has any relation to the target. If
    // the subject does not have any relation to the target, an error is
    // returned.
    HasAnyRelation(ctx context.Context, source, target model.ID) (bool, error)
    // CtxUserHasAnyRelation checks if the user in the context has any relation
    // to the target. If the user does not have any relation to the target, an
    // error is returned.
    CtxUserHasAnyRelation(ctx context.Context, target model.ID) bool
    // HasSystemRole checks if the subject has the system role. If the subject
    // does not have the system role, an error is returned.
    HasSystemRole(ctx context.Context, source model.ID, roles ...model.SystemRole) (bool, error)
    // CtxUserHasSystemRole checks if the user in the context has the system
    // role. If the user does not have the system role, an error is returned.
    CtxUserHasSystemRole(ctx context.Context, roles ...model.SystemRole) bool
    // HasPermission checks if the subject has the permission to perform the
    // action on the target. If the subject does not have the permission, an
    // error is returned.
    HasPermission(ctx context.Context, subject, target model.ID, kinds ...model.PermissionKind) (bool, error)
    // CtxUserHasPermission checks if the user in the context has the permission
    // to perform the action on the target. If the user does not have the
    // permission, an error is returned.
    CtxUserHasPermission(ctx context.Context, target model.ID, permissions ...model.PermissionKind) bool
    // Update updates the permission with the given ID. If the permission does
    // not exist, an error is returned.
    Update(ctx context.Context, id model.ID, kind model.PermissionKind) (*model.Permission, error)
    // CtxUserUpdate updates the permission with the given ID if the user in the
    // context has the permission to update the permission. If the permission
    // does not exist, an error is returned.
    CtxUserUpdate(ctx context.Context, id model.ID, kind model.PermissionKind) (*model.Permission, error)
    // Delete deletes the permission with the given ID. If the permission does
    // not exist, an error is returned.
    Delete(ctx context.Context, id model.ID) error
    // CtxUserDelete deletes the permission with the given ID if the user in the
    // context has the permission to delete the permission. If the permission
    // does not exist, an error is returned.
    CtxUserDelete(ctx context.Context, id model.ID) error
}

// permissionService is the concrete implementation of the PermissionService
// interface.
type permissionService struct {
    *baseService
    permissionRepo repository.PermissionRepository
}

func (s *permissionService) Create(ctx context.Context, perm *model.Permission) error {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/Create")
    defer span.End()

    if err := perm.Validate(); err != nil {
        return err
    }

    if err := s.permissionRepo.Create(ctx, perm); err != nil {
        return errors.Join(ErrPermissionCreate, err)
    }

    return nil
}

func (s *permissionService) CtxUserCreate(ctx context.Context, perm *model.Permission) error {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/CtxUserCreate")
    defer span.End()

    hasPermission := false

    // If the user has "write" permission on the target, they can give any
    // permission to the subject they own too (plus "read").
    if s.CtxUserHasPermission(ctx, perm.Target, model.PermissionKindWrite) {
        hasPermission = s.CtxUserHasPermission(ctx, perm.Target, perm.Kind)
    }

    if hasPermission {
        return s.Create(ctx, perm)
    }

    return errors.Join(ErrPermissionCreate, ErrNoPermission)
}

func (s *permissionService) Get(ctx context.Context, id model.ID) (*model.Permission, error) {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/Get")
    defer span.End()

    perm, err := s.permissionRepo.Get(ctx, id)
    if err != nil {
        return nil, errors.Join(ErrPermissionGet, err)
    }

    return perm, nil
}

func (s *permissionService) GetBySubject(ctx context.Context, id model.ID) ([]*model.Permission, error) {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/GetBySubject")
    defer span.End()

    permissions, err := s.permissionRepo.GetBySubject(ctx, id)
    if err != nil {
        return nil, errors.Join(ErrPermissionGetBySubject, err)
    }

    return permissions, nil
}

func (s *permissionService) GetByTarget(ctx context.Context, id model.ID) ([]*model.Permission, error) {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/GetByTarget")
    defer span.End()

    permissions, err := s.permissionRepo.GetByTarget(ctx, id)
    if err != nil {
        return nil, errors.Join(ErrPermissionGetByTarget, err)
    }

    return permissions, nil
}

func (s *permissionService) GetBySubjectAndTarget(ctx context.Context, source, target model.ID) ([]*model.Permission, error) {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/GetBySubjectAndTarget")
    defer span.End()

    permissions, err := s.permissionRepo.GetBySubjectAndTarget(ctx, source, target)
    if err != nil {
        return nil, errors.Join(ErrPermissionGetBySubjectAndTarget, err)
    }

    return permissions, nil
}

func (s *permissionService) HasAnyRelation(ctx context.Context, source, target model.ID) (bool, error) {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/HasAnyRelation")
    defer span.End()

    hasAnyRelation, err := s.permissionRepo.HasAnyRelation(ctx, source, target)
    if err != nil {
        return false, errors.Join(ErrPermissionHasAnyRelation, err)
    }

    return hasAnyRelation, nil
}

func (s *permissionService) CtxUserHasAnyRelation(ctx context.Context, target model.ID) bool {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/CtxUserHasAnyRelation")
    defer span.End()

    userID, ok := ctx.Value(pkg.CtxKeyUserID).(model.ID)
    if !ok {
        return false
    }

    hasAnyRelation, err := s.HasAnyRelation(ctx, userID, target)
    if err != nil {
        return false
    }

    return hasAnyRelation
}

func (s *permissionService) HasSystemRole(ctx context.Context, source model.ID, roles ...model.SystemRole) (bool, error) {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/HasSystemRole")
    defer span.End()

    hasSystemRole, err := s.permissionRepo.HasSystemRole(ctx, source, roles...)
    if err != nil {
        return false, errors.Join(ErrPermissionHasSystemRole, err)
    }

    return hasSystemRole, nil
}

func (s *permissionService) CtxUserHasSystemRole(ctx context.Context, roles ...model.SystemRole) bool {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/CtxUserHasSystemRole")
    defer span.End()

    userID, ok := ctx.Value(pkg.CtxKeyUserID).(model.ID)
    if !ok {
        return false
    }

    hasSystemRole, err := s.HasSystemRole(ctx, userID, roles...)
    if err != nil {
        return false
    }

    return hasSystemRole
}

func (s *permissionService) HasPermission(ctx context.Context, subject, target model.ID, kinds ...model.PermissionKind) (bool, error) {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/HasPermission")
    defer span.End()

    // Collect system roles to check for based on permission kinds.
    roles := []model.SystemRole{
        model.SystemRoleOwner,
        model.SystemRoleAdmin,
        model.SystemRoleSupport,
    }

    // Limit the roles to check for based on the permission kinds.
    // - If we have a permission kind of "all" or "delete, the role must be owner.
    // - If we have a permission kind of "create" or "write", the role must be owner or admin.
    // - Otherwise, the role can be any system role.
    for _, kind := range kinds {
        if kind == model.PermissionKindAll || kind == model.PermissionKindDelete {
            roles = []model.SystemRole{model.SystemRoleOwner}
            break
        }

        if kind == model.PermissionKindCreate || kind == model.PermissionKindWrite {
            roles = []model.SystemRole{model.SystemRoleOwner, model.SystemRoleAdmin}
            break
        }
    }

    hasSystemRoles, err := s.permissionRepo.HasSystemRole(ctx, subject, roles...)
    if err != nil {
        return false, errors.Join(ErrPermissionHasSystemRole, err)
    }

    hasPermission, err := s.permissionRepo.HasPermission(ctx, subject, target, append(kinds, model.PermissionKindAll)...)
    if err != nil {
        return false, errors.Join(ErrPermissionHasPermission, err)
    }

    return hasSystemRoles || hasPermission, nil
}

func (s *permissionService) CtxUserHasPermission(ctx context.Context, target model.ID, kinds ...model.PermissionKind) bool {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/CtxUserHasPermission")
    defer span.End()

    userID, ok := ctx.Value(pkg.CtxKeyUserID).(model.ID)
    if !ok {
        return false
    }

    hasPerm, err := s.HasPermission(ctx, userID, target, kinds...)
    if err != nil && !errors.Is(err, repository.ErrPermissionRead) {
        return false
    }

    return hasPerm
}

func (s *permissionService) Update(ctx context.Context, id model.ID, kind model.PermissionKind) (*model.Permission, error) {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/Update")
    defer span.End()

    permission, err := s.permissionRepo.Update(ctx, id, kind)
    if err != nil {
        return nil, errors.Join(ErrPermissionUpdate, err)
    }

    return permission, nil
}

func (s *permissionService) CtxUserUpdate(ctx context.Context, id model.ID, kind model.PermissionKind) (*model.Permission, error) {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/CtxUserUpdate")
    defer span.End()

    if _, ok := ctx.Value(pkg.CtxKeyUserID).(model.ID); !ok {
        return nil, errors.Join(ErrPermissionUpdate, ErrNoUser)
    }

    perm, err := s.Get(ctx, id)
    if err != nil {
        return nil, errors.Join(ErrPermissionUpdate, err)
    }

    if s.CtxUserHasPermission(ctx, perm.Target, model.PermissionKindWrite) {
        return s.Update(ctx, id, kind)
    }

    return nil, errors.Join(ErrPermissionUpdate, ErrNoPermission)
}

func (s *permissionService) Delete(ctx context.Context, id model.ID) error {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/Delete")
    defer span.End()

    if err := s.permissionRepo.Delete(ctx, id); err != nil {
        return errors.Join(ErrPermissionDelete, err)
    }

    return nil
}

func (s *permissionService) CtxUserDelete(ctx context.Context, id model.ID) error {
    ctx, span := s.tracer.Start(ctx, "service.permissionService/CtxUserDelete")
    defer span.End()

    if _, ok := ctx.Value(pkg.CtxKeyUserID).(model.ID); !ok {
        return errors.Join(ErrPermissionDelete, ErrNoUser)
    }

    perm, err := s.Get(ctx, id)
    if err != nil {
        return errors.Join(ErrPermissionDelete, err)
    }

    if s.CtxUserHasPermission(ctx, perm.Target, model.PermissionKindDelete) {
        return s.Delete(ctx, id)
    }

    return errors.Join(ErrPermissionDelete, ErrNoPermission)
}

// NewPermissionService creates a new permission service.
func NewPermissionService(permissionRepo repository.PermissionRepository, opts ...Option) (PermissionService, error) {
    s, err := newService(opts...)
    if err != nil {
        return nil, err
    }

    svc := &permissionService{
        baseService:    s,
        permissionRepo: permissionRepo,
    }

    if svc.permissionRepo == nil {
        return nil, ErrNoPermissionRepository
    }

    return svc, nil
}