Update RBAC on console

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-12-23 09:04:26 +01:00
parent 72831d4c2b
commit e9ac50d91c
26 changed files with 2123 additions and 1444 deletions

View File

@@ -12,9 +12,20 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// LEGACY: This is the legacy access management service that is used to authorize actions on entities.
// It is deprecated and will be removed in the future.
// Use the Authorizer instead.
// LEGACY ACCESS MANAGEMENT SERVICE - DEPRECATED
//
// This service implements the legacy authorization model that uses the Permissions
// map from permissions.go to check if a principal can perform an action.
//
// It is being replaced by Authorizer which uses a policy-based evaluation system.
// During migration, this service is still used for:
// - API key authorization (intersection semantics between user and API key roles)
// - Fallback for any unmapped legacy actions
//
// Once all actions are migrated and API key authorization is implemented in the
// new system, this service will be removed.
//
// Deprecated: Use Authorizer.Authorize() instead for new code.
package iam
import (
@@ -148,18 +159,21 @@ func (s *AccessManagementService) loadAPIKeyRoleForEntity(
apiKeyID gid.GID,
entityID gid.GID,
) (Role, error) {
var akm coredata.PersonalAPIKeyMembership
if err := akm.LoadRoleByAPIKeyAndEntityID(ctx, conn, scope, apiKeyID, entityID); err != nil {
return "", err
// Load the API key to get the identity
apiKey := &coredata.PersonalAPIKey{}
if err := apiKey.LoadByID(ctx, conn, apiKeyID); err != nil {
return "", fmt.Errorf("cannot load api key: %w", err)
}
// Strict API key semantics: FULL only matches RoleFull explicitly.
switch akm.Role {
case coredata.APIRoleFull:
return RoleFull, nil
default:
return "", fmt.Errorf("unsupported api key role: %s", akm.Role)
// Use the Identity's membership role for authorization
var m coredata.Membership
if err := m.LoadRoleByIdentityAndEntityID(ctx, conn, scope, apiKey.IdentityID, entityID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return "", err
}
return "", err
}
return Role(m.Role.String()), nil
}
// requiredRoleNamesContain is a temporary evaluator for the current in-code permissions registry

View File

@@ -17,7 +17,9 @@ package iam
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"go.gearno.de/kit/pg"
"go.probo.inc/probo/pkg/coredata"
@@ -32,24 +34,28 @@ type Authorizer struct {
policySet *PolicySet
}
// NewAuthorizer creates a new authorizer with the given PolicySet.
// The PolicySet should contain all role-based and self-management policies
// from all services that need authorization.
// NewAuthorizer creates a new authorizer.
// Services register their policies by calling RegisterPolicySet.
//
// Example:
//
// policySet := iam.IAMPolicySet().
// Merge(documents.DocumentPolicySet()).
// Merge(risks.RiskPolicySet())
// authorizer := iam.NewAuthorizer(pgClient, policySet)
func NewAuthorizer(pgClient *pg.Client, policySet *PolicySet) *Authorizer {
// authorizer := iam.NewAuthorizer(pgClient)
// authorizer.RegisterPolicySet(iam.IAMPolicySet())
// authorizer.RegisterPolicySet(probo.ProboPolicySet())
func NewAuthorizer(pgClient *pg.Client) *Authorizer {
return &Authorizer{
pg: pgClient,
evaluator: policy.NewEvaluator(),
policySet: policySet,
policySet: NewPolicySet(),
}
}
// RegisterPolicySet merges policies from another service into this authorizer.
// Services call this method to register their policies.
func (a *Authorizer) RegisterPolicySet(policySet *PolicySet) {
a.policySet.Merge(policySet)
}
// AuthorizeParams contains all parameters for an authorization check.
type AuthorizeParams struct {
// Principal is the user requesting access.
@@ -66,6 +72,53 @@ type AuthorizeParams struct {
ResourceAttributes map[string]string
}
func (a *Authorizer) GetPermissionsForMembership(ctx context.Context, identityID gid.GID, membershipID gid.GID) (map[string]map[Action]bool, error) {
var (
scope = coredata.NewScopeFromObjectID(membershipID)
membership = &coredata.Membership{}
)
err := a.pg.WithConn(
ctx,
func(conn pg.Conn) error {
err := membership.LoadByID(ctx, conn, scope, membershipID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return NewMembershipNotFoundError(membershipID)
}
return fmt.Errorf("cannot load membership: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
permissions := make(map[string]map[Action]bool)
for entityType, actions := range Permissions {
entityTypeName, ok := coredata.EntityModel(entityType)
if !ok {
continue
}
if permissions[entityTypeName] == nil {
permissions[entityTypeName] = make(map[Action]bool)
}
for action, allowedRoles := range actions {
if slices.Contains(allowedRoles, Role(membership.Role)) {
permissions[entityTypeName][action] = true
}
}
}
return permissions, nil
}
// Authorize checks if the principal can perform the action on the resource.
// It combines self-management policies with role-based policies.
func (a *Authorizer) Authorize(ctx context.Context, params AuthorizeParams) error {

View File

@@ -32,12 +32,13 @@ const (
ActionIAMOrganizationListInvitations = "iam:organization:list-invitations"
// Identity actions
ActionIAMIdentityGet = "iam:identity:get"
ActionIAMIdentityUpdate = "iam:identity:update"
ActionIAMIdentityDelete = "iam:identity:delete"
ActionIAMIdentityListMemberships = "iam:identity:list-memberships"
ActionIAMIdentityListInvitations = "iam:identity:list-invitations"
ActionIAMIdentityListSessions = "iam:identity:list-sessions"
ActionIAMIdentityGet = "iam:identity:get"
ActionIAMIdentityUpdate = "iam:identity:update"
ActionIAMIdentityDelete = "iam:identity:delete"
ActionIAMIdentityListMemberships = "iam:identity:list-memberships"
ActionIAMIdentityListInvitations = "iam:identity:list-invitations"
ActionIAMIdentityListSessions = "iam:identity:list-sessions"
ActionIAMIdentityListPersonalAPIKeys = "iam:identity:list-personal-api-keys"
// Session actions
ActionIAMSessionGet = "iam:session:get"
@@ -52,4 +53,17 @@ const (
// Membership actions
ActionIAMMembershipGet = "iam:membership:get"
ActionIAMMembershipUpdate = "iam:membership:update"
// Personal API Key actions
ActionIAMPersonalAPIKeyCreate = "iam:personal-api-key:create"
ActionIAMPersonalAPIKeyGet = "iam:personal-api-key:get"
ActionIAMPersonalAPIKeyUpdate = "iam:personal-api-key:update"
ActionIAMPersonalAPIKeyDelete = "iam:personal-api-key:delete"
// SAML Configuration actions
ActionIAMSAMLConfigurationCreate = "iam:saml-configuration:create"
ActionIAMSAMLConfigurationGet = "iam:saml-configuration:get"
ActionIAMSAMLConfigurationUpdate = "iam:saml-configuration:update"
ActionIAMSAMLConfigurationDelete = "iam:saml-configuration:delete"
ActionIAMSAMLConfigurationList = "iam:saml-configuration:list"
)

View File

@@ -34,14 +34,15 @@ var IAMSelfManageIdentityPolicy = policy.NewPolicy(
).WithSID("manage-own-identity").
When(policy.Equals("principal.id", "resource.id")),
// Users can list their own memberships and invitations
// Users can list their own memberships, invitations, sessions, and API keys
policy.Allow(
ActionIAMIdentityListMemberships,
ActionIAMIdentityListInvitations,
ActionIAMIdentityListSessions,
ActionIAMIdentityListPersonalAPIKeys,
).WithSID("list-own-associations").
When(policy.Equals("principal.id", "resource.id")),
).WithDescription("Allows users to manage their own identity, sessions, and view their memberships")
).WithDescription("Allows users to manage their own identity, sessions, API keys, and view their memberships")
// IAMSelfManageSessionPolicy allows users to manage their own sessions.
var IAMSelfManageSessionPolicy = policy.NewPolicy(
@@ -79,6 +80,20 @@ var IAMSelfManageMembershipPolicy = policy.NewPolicy(
When(policy.Equals("principal.id", "resource.user_id")),
).WithDescription("Allows users to view their organization memberships")
// IAMSelfManagePersonalAPIKeyPolicy allows users to manage their own API keys.
var IAMSelfManagePersonalAPIKeyPolicy = policy.NewPolicy(
"iam:self-manage-personal-api-key",
"Self-Manage Personal API Keys",
// Users can create, view, update, and delete their own API keys
policy.Allow(
ActionIAMPersonalAPIKeyCreate,
ActionIAMPersonalAPIKeyGet,
ActionIAMPersonalAPIKeyUpdate,
ActionIAMPersonalAPIKeyDelete,
).WithSID("manage-own-api-keys").
When(policy.Equals("principal.id", "resource.user_id")),
).WithDescription("Allows users to manage their own personal API keys")
// IAMOwnerPolicy defines permissions for organization owners.
var IAMOwnerPolicy = policy.NewPolicy(
"iam:owner",
@@ -92,6 +107,8 @@ var IAMOwnerPolicy = policy.NewPolicy(
ActionIAMInvitationGet,
ActionIAMInvitationDelete,
).WithSID("manage-invitations"),
// Full access to SAML configuration management
policy.Allow("iam:saml-configuration:*").WithSID("full-saml-access"),
).WithDescription("Full IAM access for organization owners")
// IAMAdminPolicy defines permissions for organization admins.
@@ -116,11 +133,22 @@ var IAMAdminPolicy = policy.NewPolicy(
ActionIAMInvitationGet,
ActionIAMInvitationDelete,
).WithSID("invitation-admin-access"),
// Can view SAML configurations
policy.Allow(
ActionIAMSAMLConfigurationGet,
ActionIAMSAMLConfigurationList,
).WithSID("saml-viewer-access"),
// Cannot delete organization
policy.Deny(ActionIAMOrganizationDelete).WithSID("deny-org-delete"),
// Cannot remove members (only owner can)
policy.Deny(ActionIAMOrganizationRemoveMember).WithSID("deny-remove-member"),
).WithDescription("IAM admin access - can manage members but cannot delete organization")
// Cannot manage SAML configurations (only owner can)
policy.Deny(
ActionIAMSAMLConfigurationCreate,
ActionIAMSAMLConfigurationUpdate,
ActionIAMSAMLConfigurationDelete,
).WithSID("deny-saml-management"),
).WithDescription("IAM admin access - can manage members but cannot delete organization or manage SAML")
// IAMViewerPolicy defines permissions for organization viewers.
var IAMViewerPolicy = policy.NewPolicy(

View File

@@ -12,9 +12,25 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// LEGACY: This is the legacy access management service that is used to authorize actions on entities.
// It is deprecated and will be removed in the future.
// Use the Authorizer instead.
// LEGACY PERMISSION SYSTEM - DEPRECATED
//
// This file contains the legacy permission system that maps entity types and actions
// to allowed roles. It is being replaced by a policy-based authorization system.
//
// Migration path:
// - New actions are defined in core_actions.go with namespaced format (e.g., "core:asset:get")
// - New policies are defined in core_policies.go and iam_policies.go
// - The action_mapping.go file provides a bridge between legacy and new actions
// - The Authorizer in authorizer.go evaluates policies using the new system
//
// During the migration period, the MustBeAuthorized function in resolvers will:
// 1. Attempt to map legacy actions to new namespaced actions
// 2. Use the new Authorizer if mapping succeeds
// 3. Fall back to LegacyAccessManagementService for unmapped actions or API key requests
//
// Once migration is complete, this file and access_management_service.go will be removed.
//
// Deprecated: Use Authorizer.Authorize() with new namespaced actions instead.
package iam
import (

View File

@@ -67,5 +67,6 @@ func IAMPolicySet() *PolicySet {
IAMSelfManageSessionPolicy,
IAMSelfManageInvitationPolicy,
IAMSelfManageMembershipPolicy,
IAMSelfManagePersonalAPIKeyPolicy,
)
}

View File

@@ -60,7 +60,6 @@ type (
Certificate *x509.Certificate
PrivateKey *rsa.PrivateKey
Logger *log.Logger
PolicySet *PolicySet
TracerProvider trace.TracerProvider
DomainVerificationInterval time.Duration
DomainVerificationResolverAddr string
@@ -113,15 +112,8 @@ func NewService(
svc.APIKeyService = NewAPIKeyService(svc)
svc.LegacyAccessManagementService = NewAccessManagementService(svc)
// Use provided PolicySet or default to IAM-only policies
policySet := NewPolicySet()
if cfg.PolicySet != nil {
policySet = cfg.PolicySet
}
policySet.Merge(IAMPolicySet())
svc.Authorizer = NewAuthorizer(pgClient, policySet)
svc.Authorizer = NewAuthorizer(pgClient)
svc.Authorizer.RegisterPolicySet(IAMPolicySet())
samlService, err := saml.NewService(svc.pg, svc.encryptionKey, svc.baseURL, svc.certificate, svc.privateKey, cfg.Logger)
if err != nil {