Update RBAC on console
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -67,5 +67,6 @@ func IAMPolicySet() *PolicySet {
|
||||
IAMSelfManageSessionPolicy,
|
||||
IAMSelfManageInvitationPolicy,
|
||||
IAMSelfManageMembershipPolicy,
|
||||
IAMSelfManagePersonalAPIKeyPolicy,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user