481359ddb2
Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
41 lines
1.4 KiB
Go
41 lines
1.4 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/kandev/kandev/internal/authz"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
"github.com/kandev/kandev/internal/task/repository/repoerrors"
|
|
)
|
|
|
|
// ErrForbidden reports that the caller can see the resource but lacks the
|
|
// scope the action requires.
|
|
//
|
|
// It is deliberately distinct from the *NotFound sentinels. Those hide
|
|
// existence from a caller who has no business knowing the resource is there;
|
|
// this one is returned only after workspace.read has already been granted, so
|
|
// there is nothing left to hide and a 404 would just be confusing ("the task I
|
|
// am looking at does not exist?").
|
|
var ErrForbidden = errors.New("insufficient permissions for this action")
|
|
|
|
// IsForbidden reports whether an error is the scope-denied sentinel.
|
|
func IsForbidden(err error) bool { return errors.Is(err, ErrForbidden) }
|
|
|
|
// requireWorkspaceManage gates the owner-only workspace actions: rename,
|
|
// defaults, visibility, and delete. It applies the 404-vs-403 rule so a caller
|
|
// who cannot see the workspace never learns it exists.
|
|
func (s *Service) requireWorkspaceManage(ctx context.Context, workspace *models.Workspace) error {
|
|
if _, scoped := callerScope(ctx); !scoped {
|
|
return nil
|
|
}
|
|
decision := s.workspaceDecision(ctx, workspace)
|
|
if !decision.CanRead() {
|
|
return repoerrors.ErrWorkspaceNotFound
|
|
}
|
|
if !decision.Has(authz.ScopeWorkspaceManage) {
|
|
return ErrForbidden
|
|
}
|
|
return nil
|
|
}
|