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.
82 lines
2.6 KiB
Go
82 lines
2.6 KiB
Go
package connect_v1
|
|
|
|
// This file will be automatically regenerated based on the schema, any resolver
|
|
// implementations
|
|
// will be copied through when generating and any unknown code will be moved to the end.
|
|
// Code generated by github.com/99designs/gqlgen version v0.17.93
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"go.gearno.de/kit/log"
|
|
"go.probo.inc/probo/pkg/iam"
|
|
"go.probo.inc/probo/pkg/server/api/authn"
|
|
"go.probo.inc/probo/pkg/server/api/authz"
|
|
"go.probo.inc/probo/pkg/server/api/connect/v1/schema"
|
|
"go.probo.inc/probo/pkg/server/api/connect/v1/types"
|
|
"go.probo.inc/probo/pkg/server/gqlutils"
|
|
)
|
|
|
|
// LastSession is the resolver for the lastSession field.
|
|
func (r *membershipResolver) LastSession(ctx context.Context, obj *types.Membership) (*types.Session, error) {
|
|
if _, err := r.authorize(ctx, obj.ID, iam.ActionMembershipGet, authz.WithSkipAssumptionCheck()); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
session := authn.SessionFromContext(ctx)
|
|
if session == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
childSession, err := r.iam.SessionService.GetActiveSessionForMembership(ctx, session.ID, obj.ID)
|
|
if err != nil {
|
|
if _, ok := errors.AsType[*iam.ErrSessionNotFound](err); ok {
|
|
return nil, nil
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot get active session for membership", log.Error(err))
|
|
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return types.NewSession(childSession), nil
|
|
}
|
|
|
|
// Permission is the resolver for the permission field.
|
|
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.
|
|
func (r *mutationResolver) UpdateMembership(ctx context.Context, input types.UpdateMembershipInput) (*types.UpdateMembershipPayload, error) {
|
|
if _, err := r.authorize(
|
|
ctx,
|
|
input.MembershipID,
|
|
iam.ActionMembershipUpdate,
|
|
authz.WithAttr("target_role", input.Role.String()),
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
membership, err := r.iam.OrganizationService.UpdateMembership(ctx, input.OrganizationID, input.MembershipID, input.Role)
|
|
if err != nil {
|
|
if _, ok := errors.AsType[*iam.ErrLastActiveOwner](err); ok {
|
|
return nil, gqlutils.Conflictf(ctx, "cannot demote last active owner")
|
|
}
|
|
|
|
r.logger.ErrorCtx(ctx, "cannot update membership", log.Error(err))
|
|
|
|
return nil, gqlutils.Internal(ctx)
|
|
}
|
|
|
|
return &types.UpdateMembershipPayload{
|
|
Membership: types.NewMembership(membership),
|
|
}, nil
|
|
}
|
|
|
|
// Membership returns schema.MembershipResolver implementation.
|
|
func (r *Resolver) Membership() schema.MembershipResolver { return &membershipResolver{r} }
|
|
|
|
type membershipResolver struct{ *Resolver }
|