Whitelist ownership grants via allow policies
Replace the deny-based restriction on granting OWNER with role-scoped allow policies so authorization fails closed: admins may create and update memberships only when the assigned role is not OWNER, and the absence of a target role no longer implies permission. To keep console UI gating accurate without loosening the base grants, the permission field gains an optional typed options argument (PermissionOptionsInput) that forwards target_role into the dry-run authorization. Only the two role-related console calls (create user, update membership) pass it; the OWNER option stays hidden for admins via the existing assignable-roles helper. Add a non-regression test that an admin cannot promote a member to OWNER while still being able to change members between non-owner roles.
This commit is contained in:
@@ -17,6 +17,7 @@ scalar Datetime
|
||||
scalar Upload
|
||||
scalar EmailAddr
|
||||
scalar OAuth2Scope
|
||||
scalar Map
|
||||
|
||||
interface Node {
|
||||
id: ID!
|
||||
|
||||
@@ -16,7 +16,7 @@ type Membership implements Node {
|
||||
|
||||
lastSession: Session @goField(forceResolver: true)
|
||||
|
||||
permission(action: String!): Boolean!
|
||||
permission(action: String!, attributes: Map): Boolean!
|
||||
@goField(forceResolver: true)
|
||||
@authentication(required: PRESENT)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ type Organization implements Node {
|
||||
|
||||
viewer: Profile @goField(forceResolver: true)
|
||||
|
||||
permission(action: String!): Boolean!
|
||||
permission(action: String!, attributes: Map): Boolean!
|
||||
@goField(forceResolver: true)
|
||||
@authentication(required: PRESENT)
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ func (r *membershipResolver) LastSession(ctx context.Context, obj *types.Members
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *membershipResolver) Permission(ctx context.Context, obj *types.Membership, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
func (r *membershipResolver) Permission(ctx context.Context, obj *types.Membership, action string, attributes map[string]any) (bool, error) {
|
||||
return r.Resolver.permission(ctx, obj, action, attributes)
|
||||
}
|
||||
|
||||
// UpdateMembership is the resolver for the updateMembership field.
|
||||
|
||||
@@ -358,8 +358,8 @@ func (r *organizationResolver) Viewer(ctx context.Context, obj *types.Organizati
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *organizationResolver) Permission(ctx context.Context, obj *types.Organization, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
func (r *organizationResolver) Permission(ctx context.Context, obj *types.Organization, action string, attributes map[string]any) (bool, error) {
|
||||
return r.Resolver.permission(ctx, obj, action, attributes)
|
||||
}
|
||||
|
||||
// Organization returns schema.OrganizationResolver implementation.
|
||||
|
||||
@@ -129,7 +129,18 @@ func NewMux(
|
||||
}
|
||||
|
||||
func (r *Resolver) Permission(ctx context.Context, obj types.Node, action string) (bool, error) {
|
||||
_, err := r.authorize(ctx, obj.GetID(), action, authz.WithDryRun())
|
||||
return r.permission(ctx, obj, action, nil)
|
||||
}
|
||||
|
||||
func (r *Resolver) permission(ctx context.Context, obj types.Node, action string, attributes map[string]any) (bool, error) {
|
||||
opts := []authz.AuthorizeFuncOption{authz.WithDryRun()}
|
||||
for key, value := range attributes {
|
||||
if s, ok := value.(string); ok {
|
||||
opts = append(opts, authz.WithAttr(key, s))
|
||||
}
|
||||
}
|
||||
|
||||
_, err := r.authorize(ctx, obj.GetID(), action, opts...)
|
||||
return err == nil, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user