diff --git a/pkg/server/api/authz/authorization.go b/pkg/server/api/authz/authorization.go index 2b1a73f9d..c57949f64 100644 --- a/pkg/server/api/authz/authorization.go +++ b/pkg/server/api/authz/authorization.go @@ -27,8 +27,10 @@ import ( ) type ( - AuthorizeFuncOption func(*iam.AuthorizeParams) - AuthorizeFunc func(context.Context, gid.GID, string, ...AuthorizeFuncOption) (*coredata.Scope, error) + AuthorizeFuncOption func(*iam.AuthorizeParams) + AuthorizeFunc func(context.Context, gid.GID, string, ...AuthorizeFuncOption) (*coredata.Scope, error) + BatchAuthorizeFuncOption func(*iam.AuthorizeBatchParams) + BatchAuthorizeFunc func(context.Context, string, []gid.GID, ...BatchAuthorizeFuncOption) (*coredata.Scope, error) ) func WithAttr(key, value string) AuthorizeFuncOption { @@ -51,6 +53,24 @@ func WithDryRun() AuthorizeFuncOption { } } +func WithBatchAttr(key, value string) BatchAuthorizeFuncOption { + return func(params *iam.AuthorizeBatchParams) { + params.ResourceAttributes[key] = value + } +} + +func WithBatchSkipAssumptionCheck() BatchAuthorizeFuncOption { + return func(params *iam.AuthorizeBatchParams) { + params.SkipAssumptionCheck = true + } +} + +func WithBatchDryRun() BatchAuthorizeFuncOption { + return func(params *iam.AuthorizeBatchParams) { + params.DryRun = true + } +} + func NewAuthorizeFunc( svc *iam.Service, logger *log.Logger, @@ -100,3 +120,65 @@ func NewAuthorizeFunc( return scope, nil } } + +func NewBatchAuthorizeFunc( + svc *iam.Service, + logger *log.Logger, +) BatchAuthorizeFunc { + return func( + ctx context.Context, + action string, + objectIDs []gid.GID, + options ...BatchAuthorizeFuncOption, + ) (*coredata.Scope, error) { + identity := authn.IdentityFromContext(ctx) + session := authn.SessionFromContext(ctx) + + params := iam.AuthorizeBatchParams{ + Principal: identity.ID, + Action: action, + Resources: objectIDs, + ResourceAttributes: make(map[string]string), + } + if session != nil { + params.Session = &session.ID + } + + for _, option := range options { + option(¶ms) + } + + scope, err := svc.Authorizer.AuthorizeBatch(ctx, params) + if err != nil { + if _, ok := errors.AsType[*iam.ErrAssumptionRequired](err); ok { + return nil, gqlutils.AssumptionRequired(ctx, err) + } + + if _, ok := errors.AsType[*iam.ErrInsufficientPermissions](err); ok { + return nil, gqlutils.Forbidden(ctx, err) + } + + if _, ok := errors.AsType[*iam.ErrMixedOrganizationBatch](err); ok { + return nil, gqlutils.Invalid(ctx, err) + } + + if _, ok := errors.AsType[*iam.ErrEmptyResourceBatch](err); ok { + return nil, gqlutils.Invalid(ctx, err) + } + + if _, ok := errors.AsType[*iam.ErrBatchAuthorizationUnsupportedResourceType](err); ok { + return nil, gqlutils.Invalid(ctx, err) + } + + if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, gqlutils.NotFoundf(ctx, "resource not found") + } + + logger.ErrorCtx(ctx, "cannot batch authorize", log.Error(err)) + + return nil, gqlutils.Internal(ctx) + } + + return scope, nil + } +} diff --git a/pkg/server/api/connect/v1/graphql_handler.go b/pkg/server/api/connect/v1/graphql_handler.go index 179697389..edd43be54 100644 --- a/pkg/server/api/connect/v1/graphql_handler.go +++ b/pkg/server/api/connect/v1/graphql_handler.go @@ -31,11 +31,12 @@ import ( func NewGraphQLHandler(svc *iam.Service, logger *log.Logger, baseURL *baseurl.BaseURL, cookieConfig securecookie.Config) http.Handler { config := schema.Config{ Resolvers: &Resolver{ - authorize: authz.NewAuthorizeFunc(svc, logger), - logger: logger, - iam: svc, - baseURL: baseURL, - sessionCookie: authn.NewCookie(&cookieConfig), + authorize: authz.NewAuthorizeFunc(svc, logger), + batchAuthorize: authz.NewBatchAuthorizeFunc(svc, logger), + logger: logger, + iam: svc, + baseURL: baseURL, + sessionCookie: authn.NewCookie(&cookieConfig), }, Directives: schema.DirectiveRoot{ Session: session.Directive, diff --git a/pkg/server/api/connect/v1/resolver.go b/pkg/server/api/connect/v1/resolver.go index 288c6f9e8..3fe661153 100644 --- a/pkg/server/api/connect/v1/resolver.go +++ b/pkg/server/api/connect/v1/resolver.go @@ -48,11 +48,12 @@ import ( type ( Resolver struct { - authorize authz.AuthorizeFunc - logger *log.Logger - iam *iam.Service - baseURL *baseurl.BaseURL - sessionCookie *authn.Cookie + authorize authz.AuthorizeFunc + batchAuthorize authz.BatchAuthorizeFunc + logger *log.Logger + iam *iam.Service + baseURL *baseurl.BaseURL + sessionCookie *authn.Cookie } ) diff --git a/pkg/server/api/mcp/v1/resolver.go b/pkg/server/api/mcp/v1/resolver.go index 3831f3cad..edc0410b5 100644 --- a/pkg/server/api/mcp/v1/resolver.go +++ b/pkg/server/api/mcp/v1/resolver.go @@ -88,3 +88,47 @@ func (r *Resolver) Authorize(ctx context.Context, entityID gid.GID, action iam.A return nil, fmt.Errorf("internal server error") } + +func (r *Resolver) AuthorizeBatch(ctx context.Context, entityIDs []gid.GID, action iam.Action) (*coredata.Scope, error) { + identity := authn.IdentityFromContext(ctx) + + scope, err := r.iamSvc.Authorizer.AuthorizeBatch( + ctx, + iam.AuthorizeBatchParams{ + Principal: identity.ID, + Resources: entityIDs, + Action: action, + }, + ) + if err == nil { + return scope, nil + } + + if _, ok := errors.AsType[*iam.ErrInsufficientPermissions](err); ok { + return nil, fmt.Errorf("permission denied") + } + + if _, ok := errors.AsType[*iam.ErrAssumptionRequired](err); ok { + return nil, fmt.Errorf("assumption required") + } + + if _, ok := errors.AsType[*iam.ErrMixedOrganizationBatch](err); ok { + return nil, fmt.Errorf("mixed-organization batch") + } + + if _, ok := errors.AsType[*iam.ErrEmptyResourceBatch](err); ok { + return nil, fmt.Errorf("empty resource batch") + } + + if _, ok := errors.AsType[*iam.ErrBatchAuthorizationUnsupportedResourceType](err); ok { + return nil, fmt.Errorf("batch authorization unsupported for resource type") + } + + if errors.Is(err, coredata.ErrResourceNotFound) { + return nil, fmt.Errorf("resource not found") + } + + r.logger.ErrorCtx(ctx, "cannot batch authorize MCP request", log.Error(err)) + + return nil, fmt.Errorf("internal server error") +}