From b907a961521d1b2e90ad4dc51c5713c0570fbcb0 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Fri, 27 Jun 2025 01:49:48 +0200 Subject: [PATCH 1/7] Sync only known resource policies Signed-off-by: Jose I. Paris --- app/controlplane/cmd/wire.go | 5 + app/controlplane/cmd/wire_gen.go | 61 +++---- app/controlplane/pkg/authz/authz.go | 156 +++++++++++------- .../pkg/authz/authz_integration_test.go | 4 +- app/controlplane/pkg/authz/authz_test.go | 10 +- app/controlplane/pkg/biz/testhelpers/wire.go | 5 + .../pkg/biz/testhelpers/wire_gen.go | 7 +- 7 files changed, 150 insertions(+), 98 deletions(-) diff --git a/app/controlplane/cmd/wire.go b/app/controlplane/cmd/wire.go index 8aa97c894..b3bd9f931 100644 --- a/app/controlplane/cmd/wire.go +++ b/app/controlplane/cmd/wire.go @@ -64,10 +64,15 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger, sdk.Availabl newCASServerOptions, newAuthAllowList, newJWTConfig, + authzManagedResources, ), ) } +func authzManagedResources() []authz.Resource { + return authz.AuthzManagedResources +} + func newJWTConfig(conf *conf.Auth) *biz.APITokenJWTConfig { return &biz.APITokenJWTConfig{ SymmetricHmacKey: conf.GeneratedJwsHmacSecret, diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 5855119d4..4dca004ba 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -107,7 +107,8 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l } apiTokenRepo := data.NewAPITokenRepo(dataData, logger) apiTokenJWTConfig := newJWTConfig(auth) - enforcer, err := authz.NewDatabaseEnforcer(databaseConfig) + v3 := authzManagedResources() + enforcer, err := authz.NewDatabaseEnforcer(databaseConfig, v3) if err != nil { cleanup() return nil, nil, err @@ -118,9 +119,9 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l return nil, nil, err } workflowContractRepo := data.NewWorkflowContractRepo(dataData, logger) - v3 := bootstrap.PolicyProviders - v4 := newPolicyProviderConfig(v3) - registry, err := policies.NewRegistry(logger, v4...) + v4 := bootstrap.PolicyProviders + v5 := newPolicyProviderConfig(v4) + registry, err := policies.NewRegistry(logger, v5...) if err != nil { cleanup() return nil, nil, err @@ -128,8 +129,8 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l workflowContractUseCase := biz.NewWorkflowContractUseCase(workflowContractRepo, registry, auditorUseCase, logger) workflowUseCase := biz.NewWorkflowUsecase(workflowRepo, projectsRepo, workflowContractUseCase, auditorUseCase, membershipUseCase, logger) projectUseCase := biz.NewProjectsUseCase(logger, projectsRepo) - v5 := serviceOpts(logger, enforcer, projectUseCase) - workflowService := service.NewWorkflowService(workflowUseCase, workflowContractUseCase, projectUseCase, v5...) + v6 := serviceOpts(logger, enforcer, projectUseCase) + workflowService := service.NewWorkflowService(workflowUseCase, workflowContractUseCase, projectUseCase, v6...) orgInvitationRepo := data.NewOrgInvitation(dataData, logger) orgInvitationUseCase, err := biz.NewOrgInvitationUseCase(orgInvitationRepo, membershipRepo, userRepo, auditorUseCase, logger) if err != nil { @@ -137,12 +138,12 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l return nil, nil, err } confServer := bootstrap.Server - authService, err := service.NewAuthService(userUseCase, organizationUseCase, membershipUseCase, orgInvitationUseCase, auth, confServer, auditorUseCase, v5...) + authService, err := service.NewAuthService(userUseCase, organizationUseCase, membershipUseCase, orgInvitationUseCase, auth, confServer, auditorUseCase, v6...) if err != nil { cleanup() return nil, nil, err } - robotAccountService := service.NewRobotAccountService(robotAccountUseCase, v5...) + robotAccountService := service.NewRobotAccountService(robotAccountUseCase, v6...) workflowRunRepo := data.NewWorkflowRunRepo(dataData, logger) signingUseCase, err := biz.NewChainloopSigningUseCase(bootstrap, logger) if err != nil { @@ -159,21 +160,21 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l WorkflowUC: workflowUseCase, WorkflowContractUC: workflowContractUseCase, CredsReader: readerWriter, - Opts: v5, + Opts: v6, } workflowRunService := service.NewWorkflowRunService(newWorkflowRunServiceOpts) attestationUseCase := biz.NewAttestationUseCase(casClientUseCase, logger) fanOutDispatcher := dispatcher.New(integrationUseCase, workflowUseCase, workflowRunUseCase, readerWriter, casClientUseCase, availablePlugins, logger) casMappingRepo := data.NewCASMappingRepo(dataData, casBackendRepo, logger) casMappingUseCase := biz.NewCASMappingUseCase(casMappingRepo, membershipRepo, projectsRepo, logger) - v6 := bootstrap.PrometheusIntegration + v7 := bootstrap.PrometheusIntegration orgMetricsRepo := data.NewOrgMetricsRepo(dataData, logger) orgMetricsUseCase, err := biz.NewOrgMetricsUseCase(orgMetricsRepo, organizationRepo, workflowUseCase, logger) if err != nil { cleanup() return nil, nil, err } - prometheusUseCase := biz.NewPrometheusUseCase(v6, organizationUseCase, orgMetricsUseCase, logger) + prometheusUseCase := biz.NewPrometheusUseCase(v7, organizationUseCase, orgMetricsUseCase, logger) projectVersionRepo := data.NewProjectVersionRepo(dataData, logger) projectVersionUseCase := biz.NewProjectVersionUseCase(projectVersionRepo, logger) newAttestationServiceOpts := &service.NewAttestationServiceOpts{ @@ -193,24 +194,24 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l ProjectUC: projectUseCase, ProjectVersionUC: projectVersionUseCase, SigningUseCase: signingUseCase, - Opts: v5, + Opts: v6, } attestationService := service.NewAttestationService(newAttestationServiceOpts) - workflowContractService := service.NewWorkflowSchemaService(workflowContractUseCase, v5...) - contextService := service.NewContextService(casBackendUseCase, userUseCase, v5...) - casCredentialsService := service.NewCASCredentialsService(casCredentialsUseCase, casMappingUseCase, casBackendUseCase, enforcer, v5...) - orgMetricsService := service.NewOrgMetricsService(orgMetricsUseCase, v5...) - integrationsService := service.NewIntegrationsService(integrationUseCase, workflowUseCase, availablePlugins, v5...) - organizationService := service.NewOrganizationService(membershipUseCase, organizationUseCase, v5...) - casBackendService := service.NewCASBackendService(casBackendUseCase, providers, v5...) - casRedirectService, err := service.NewCASRedirectService(casMappingUseCase, casCredentialsUseCase, bootstrap_CASServer, v5...) + workflowContractService := service.NewWorkflowSchemaService(workflowContractUseCase, v6...) + contextService := service.NewContextService(casBackendUseCase, userUseCase, v6...) + casCredentialsService := service.NewCASCredentialsService(casCredentialsUseCase, casMappingUseCase, casBackendUseCase, enforcer, v6...) + orgMetricsService := service.NewOrgMetricsService(orgMetricsUseCase, v6...) + integrationsService := service.NewIntegrationsService(integrationUseCase, workflowUseCase, availablePlugins, v6...) + organizationService := service.NewOrganizationService(membershipUseCase, organizationUseCase, v6...) + casBackendService := service.NewCASBackendService(casBackendUseCase, providers, v6...) + casRedirectService, err := service.NewCASRedirectService(casMappingUseCase, casCredentialsUseCase, bootstrap_CASServer, v6...) if err != nil { cleanup() return nil, nil, err } - orgInvitationService := service.NewOrgInvitationService(orgInvitationUseCase, v5...) - referrerService := service.NewReferrerService(referrerUseCase, v5...) - apiTokenService := service.NewAPITokenService(apiTokenUseCase, v5...) + orgInvitationService := service.NewOrgInvitationService(orgInvitationUseCase, v6...) + referrerService := service.NewReferrerService(referrerUseCase, v6...) + apiTokenService := service.NewAPITokenService(apiTokenUseCase, v6...) attestationStateRepo := data.NewAttestationStateRepo(dataData, logger) attestationStateUseCase, err := biz.NewAttestationStateUseCase(attestationStateRepo, workflowRunRepo) if err != nil { @@ -221,15 +222,15 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l AttestationStateUseCase: attestationStateUseCase, WorkflowUseCase: workflowUseCase, WorkflowRunUseCase: workflowRunUseCase, - Opts: v5, + Opts: v6, } attestationStateService := service.NewAttestationStateService(newAttestationStateServiceOpt) - userService := service.NewUserService(membershipUseCase, organizationUseCase, v5...) - signingService := service.NewSigningService(signingUseCase, v5...) - prometheusService := service.NewPrometheusService(organizationUseCase, prometheusUseCase, v5...) + userService := service.NewUserService(membershipUseCase, organizationUseCase, v6...) + signingService := service.NewSigningService(signingUseCase, v6...) + prometheusService := service.NewPrometheusService(organizationUseCase, prometheusUseCase, v6...) groupRepo := data.NewGroupRepo(dataData, logger) groupUseCase := biz.NewGroupUseCase(logger, groupRepo, membershipRepo, auditorUseCase) - groupService := service.NewGroupService(groupUseCase, v5...) + groupService := service.NewGroupService(groupUseCase, v6...) federatedAuthentication := bootstrap.FederatedAuthentication validator, err := newProtoValidator() if err != nil { @@ -310,6 +311,10 @@ var ( // wire.go: +func authzManagedResources() []authz.Resource { + return authz.AuthzManagedResources +} + func newJWTConfig(conf2 *conf.Auth) *biz.APITokenJWTConfig { return &biz.APITokenJWTConfig{ SymmetricHmacKey: conf2.GeneratedJwsHmacSecret, diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index cf8e0a1d8..05ce21a73 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -29,9 +29,11 @@ import ( fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" entadapter "github.com/casbin/ent-adapter" config "github.com/chainloop-dev/chainloop/app/controlplane/pkg/conf/controlplane/config/v1" + "golang.org/x/exp/slices" ) type Role string +type Resource string const ( // Actions @@ -44,22 +46,20 @@ const ( // Resources - ResourceWorkflowContract = "workflow_contract" - ResourceCASArtifact = "cas_artifact" - ResourceCASBackend = "cas_backend" - ResourceReferrer = "referrer" - ResourceAvailableIntegration = "integration_available" - ResourceRegisteredIntegration = "integration_registered" - ResourceAttachedIntegration = "integration_attached" - ResourceOrgMetric = "metrics_org" - ResourceRobotAccount = "robot_account" - ResourceWorkflowRun = "workflow_run" - ResourceWorkflow = "workflow" - UserMembership = "membership_user" - Organization = "organization" - ResourceProject = "project" - ResourceGroup = "group" - ResourceGroupMembership = "group_membership" + ResourceWorkflowContract Resource = "workflow_contract" + ResourceCASArtifact Resource = "cas_artifact" + ResourceCASBackend Resource = "cas_backend" + ResourceReferrer Resource = "referrer" + ResourceAvailableIntegration Resource = "integration_available" + ResourceRegisteredIntegration Resource = "integration_registered" + ResourceAttachedIntegration Resource = "integration_attached" + ResourceOrgMetric Resource = "metrics_org" + ResourceRobotAccount Resource = "robot_account" + ResourceWorkflowRun Resource = "workflow_run" + ResourceWorkflow Resource = "workflow" + Organization Resource = "organization" + ResourceGroup Resource = "group" + ResourceGroupMembership Resource = "group_membership" // We have for now three roles, viewer, admin and owner // The owner of an org @@ -83,63 +83,88 @@ const ( RoleProjectViewer Role = "role:project:viewer" ) +// AuthzManagedResources are the resources that are managed by Chainloop, considered during permissions sync +var AuthzManagedResources = []Resource{ + ResourceWorkflowContract, + ResourceCASArtifact, + ResourceCASBackend, + ResourceReferrer, + ResourceAvailableIntegration, + ResourceRegisteredIntegration, + ResourceAttachedIntegration, + ResourceOrgMetric, + ResourceRobotAccount, + ResourceWorkflowRun, + ResourceWorkflow, + Organization, + ResourceGroup, + ResourceGroupMembership, +} + // resource, action tuple type Policy struct { Resource string Action string } +func NewPolicy(r Resource, action string) *Policy { + return &Policy{ + Resource: string(r), + Action: action, + } +} + var ( // Referrer - PolicyReferrerRead = &Policy{ResourceReferrer, ActionRead} + PolicyReferrerRead = NewPolicy(ResourceReferrer, ActionRead) // Artifact - PolicyArtifactDownload = &Policy{ResourceCASArtifact, ActionRead} - PolicyArtifactUpload = &Policy{ResourceCASArtifact, ActionCreate} + PolicyArtifactDownload = NewPolicy(ResourceCASArtifact, ActionRead) + PolicyArtifactUpload = NewPolicy(ResourceCASArtifact, ActionCreate) // CAS backend - PolicyCASBackendList = &Policy{ResourceCASBackend, ActionList} + PolicyCASBackendList = NewPolicy(ResourceCASBackend, ActionList) // Available integrations - PolicyAvailableIntegrationList = &Policy{ResourceAvailableIntegration, ActionList} - PolicyAvailableIntegrationRead = &Policy{ResourceAvailableIntegration, ActionRead} + PolicyAvailableIntegrationList = NewPolicy(ResourceAvailableIntegration, ActionList) + PolicyAvailableIntegrationRead = NewPolicy(ResourceAvailableIntegration, ActionRead) // Registered integrations - PolicyRegisteredIntegrationList = &Policy{ResourceRegisteredIntegration, ActionList} - PolicyRegisteredIntegrationRead = &Policy{ResourceRegisteredIntegration, ActionRead} - PolicyRegisteredIntegrationAdd = &Policy{ResourceRegisteredIntegration, ActionCreate} + PolicyRegisteredIntegrationList = NewPolicy(ResourceRegisteredIntegration, ActionList) + PolicyRegisteredIntegrationRead = NewPolicy(ResourceRegisteredIntegration, ActionRead) + PolicyRegisteredIntegrationAdd = NewPolicy(ResourceRegisteredIntegration, ActionCreate) // Attached integrations - PolicyAttachedIntegrationList = &Policy{ResourceAttachedIntegration, ActionList} - PolicyAttachedIntegrationAttach = &Policy{ResourceAttachedIntegration, ActionCreate} - PolicyAttachedIntegrationDetach = &Policy{ResourceAttachedIntegration, ActionDelete} + PolicyAttachedIntegrationList = NewPolicy(ResourceAttachedIntegration, ActionList) + PolicyAttachedIntegrationAttach = NewPolicy(ResourceAttachedIntegration, ActionCreate) + PolicyAttachedIntegrationDetach = NewPolicy(ResourceAttachedIntegration, ActionDelete) // Org Metrics - PolicyOrgMetricsRead = &Policy{ResourceOrgMetric, ActionList} + PolicyOrgMetricsRead = NewPolicy(ResourceOrgMetric, ActionList) // Robot Account - PolicyRobotAccountList = &Policy{ResourceRobotAccount, ActionList} - PolicyRobotAccountCreate = &Policy{ResourceRobotAccount, ActionCreate} + PolicyRobotAccountList = NewPolicy(ResourceRobotAccount, ActionList) + PolicyRobotAccountCreate = NewPolicy(ResourceRobotAccount, ActionCreate) // Workflow Contract - PolicyWorkflowContractList = &Policy{ResourceWorkflowContract, ActionList} - PolicyWorkflowContractRead = &Policy{ResourceWorkflowContract, ActionRead} - PolicyWorkflowContractUpdate = &Policy{ResourceWorkflowContract, ActionUpdate} - PolicyWorkflowContractCreate = &Policy{ResourceWorkflowContract, ActionCreate} + PolicyWorkflowContractList = NewPolicy(ResourceWorkflowContract, ActionList) + PolicyWorkflowContractRead = NewPolicy(ResourceWorkflowContract, ActionRead) + PolicyWorkflowContractUpdate = NewPolicy(ResourceWorkflowContract, ActionUpdate) + PolicyWorkflowContractCreate = NewPolicy(ResourceWorkflowContract, ActionCreate) // WorkflowRun - PolicyWorkflowRunList = &Policy{ResourceWorkflowRun, ActionList} - PolicyWorkflowRunRead = &Policy{ResourceWorkflowRun, ActionRead} - PolicyWorkflowRunCreate = &Policy{ResourceWorkflowRun, ActionCreate} - PolicyWorkflowRunUpdate = &Policy{ResourceWorkflowRun, ActionUpdate} + PolicyWorkflowRunList = NewPolicy(ResourceWorkflowRun, ActionList) + PolicyWorkflowRunRead = NewPolicy(ResourceWorkflowRun, ActionRead) + PolicyWorkflowRunCreate = NewPolicy(ResourceWorkflowRun, ActionCreate) + PolicyWorkflowRunUpdate = NewPolicy(ResourceWorkflowRun, ActionUpdate) // Workflow - PolicyWorkflowList = &Policy{ResourceWorkflow, ActionList} - PolicyWorkflowRead = &Policy{ResourceWorkflow, ActionRead} - PolicyWorkflowCreate = &Policy{ResourceWorkflow, ActionCreate} - PolicyWorkflowUpdate = &Policy{ResourceWorkflow, ActionUpdate} - PolicyWorkflowDelete = &Policy{ResourceWorkflow, ActionDelete} + PolicyWorkflowList = NewPolicy(ResourceWorkflow, ActionList) + PolicyWorkflowRead = NewPolicy(ResourceWorkflow, ActionRead) + PolicyWorkflowCreate = NewPolicy(ResourceWorkflow, ActionCreate) + PolicyWorkflowUpdate = NewPolicy(ResourceWorkflow, ActionUpdate) + PolicyWorkflowDelete = NewPolicy(ResourceWorkflow, ActionDelete) // User Membership - PolicyOrganizationRead = &Policy{Organization, ActionRead} - PolicyOrganizationListMemberships = &Policy{Organization, ActionRead} + PolicyOrganizationRead = NewPolicy(Organization, ActionRead) + PolicyOrganizationListMemberships = NewPolicy(Organization, ActionRead) // Groups - PolicyGroupCreate = &Policy{ResourceGroup, ActionCreate} - PolicyGroupUpdate = &Policy{ResourceGroup, ActionUpdate} - PolicyGroupDelete = &Policy{ResourceGroup, ActionDelete} - PolicyGroupList = &Policy{ResourceGroup, ActionList} - PolicyGroupRead = &Policy{ResourceGroup, ActionRead} + PolicyGroupCreate = NewPolicy(ResourceGroup, ActionCreate) + PolicyGroupUpdate = NewPolicy(ResourceGroup, ActionUpdate) + PolicyGroupDelete = NewPolicy(ResourceGroup, ActionDelete) + PolicyGroupList = NewPolicy(ResourceGroup, ActionList) + PolicyGroupRead = NewPolicy(ResourceGroup, ActionRead) // Group Memberships - PolicyGroupListMemberships = &Policy{ResourceGroupMembership, ActionList} + PolicyGroupListMemberships = NewPolicy(ResourceGroupMembership, ActionList) ) // List of policies for each role @@ -388,14 +413,14 @@ func (e *Enforcer) ClearPolicies(sub *SubjectAPIToken) error { // NewDatabaseEnforcer creates a new casbin authorization enforcer // based on a database backend as policies storage backend -func NewDatabaseEnforcer(c *config.DatabaseConfig) (*Enforcer, error) { +func NewDatabaseEnforcer(c *config.DatabaseConfig, managedResources []Resource) (*Enforcer, error) { // policy storage in database a, err := entadapter.NewAdapter(c.Driver, c.Source) if err != nil { return nil, fmt.Errorf("failed to create adapter: %w", err) } - e, err := newEnforcer(a) + e, err := newEnforcer(a, managedResources) if err != nil { return nil, fmt.Errorf("failed to create enforcer: %w", err) } @@ -424,10 +449,10 @@ func NewDatabaseEnforcer(c *config.DatabaseConfig) (*Enforcer, error) { // NewFileAdapter creates a new casbin authorization enforcer // based on a CSV file as policies storage backend -func NewFiletypeEnforcer(path string) (*Enforcer, error) { +func NewFiletypeEnforcer(path string, managedResources []Resource) (*Enforcer, error) { // policy storage in filesystem a := fileadapter.NewAdapter(path) - e, err := newEnforcer(a) + e, err := newEnforcer(a, managedResources) if err != nil { return nil, fmt.Errorf("failed to create enforcer: %w", err) } @@ -437,7 +462,7 @@ func NewFiletypeEnforcer(path string) (*Enforcer, error) { // NewEnforcer creates a new casbin authorization enforcer for the policies stored // in the database and the model defined in model.conf -func newEnforcer(a persist.Adapter) (*Enforcer, error) { +func newEnforcer(a persist.Adapter, managedResources []Resource) (*Enforcer, error) { // load model defined in model.conf m, err := model.NewModelFromString(string(modelFile)) if err != nil { @@ -451,7 +476,7 @@ func newEnforcer(a persist.Adapter) (*Enforcer, error) { } // Initialize the enforcer with the roles map - if err := syncRBACRoles(&Enforcer{enforcer}); err != nil { + if err := syncRBACRoles(&Enforcer{enforcer}, managedResources); err != nil { return nil, fmt.Errorf("failed to sync roles: %w", err) } @@ -461,11 +486,11 @@ func newEnforcer(a persist.Adapter) (*Enforcer, error) { // Load the roles map into the enforcer // This is done by adding all the policies defined in the roles map // and removing all the policies that are not -func syncRBACRoles(e *Enforcer) error { - return doSync(e, rolesMap) +func syncRBACRoles(e *Enforcer, managedResources []Resource) error { + return doSync(e, rolesMap, managedResources) } -func doSync(e *Enforcer, rolesMap map[Role][]*Policy) error { +func doSync(e *Enforcer, rolesMap map[Role][]*Policy, managedResources []Resource) error { // Add all the defined policies if they don't exist for role, policies := range rolesMap { for _, p := range policies { @@ -488,7 +513,14 @@ func doSync(e *Enforcer, rolesMap map[Role][]*Policy) error { for _, gotPolicies := range policies { role := gotPolicies[0] - policy := &Policy{Resource: gotPolicies[1], Action: gotPolicies[2]} + resource := gotPolicies[1] + action := gotPolicies[2] + policy := &Policy{Resource: resource, Action: action} + + // if it's not a managed resource, skip deletion + if !slices.Contains(managedResources, Resource(resource)) { + continue + } wantPolicies, ok := rolesMap[Role(role)] // if the role does not exist in the map, we can delete the policy diff --git a/app/controlplane/pkg/authz/authz_integration_test.go b/app/controlplane/pkg/authz/authz_integration_test.go index 6018deab5..4f3e3a253 100644 --- a/app/controlplane/pkg/authz/authz_integration_test.go +++ b/app/controlplane/pkg/authz/authz_integration_test.go @@ -33,9 +33,9 @@ func TestMultiReplicaPropagation(t *testing.T) { db := testhelpers.NewTestDatabase(t) defer db.Close(t) - enforcerA, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t))) + enforcerA, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)), []authz.Resource{}) require.NoError(t, err) - enforcerB, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t))) + enforcerB, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)), []authz.Resource{}) require.NoError(t, err) // Subject and policies to add diff --git a/app/controlplane/pkg/authz/authz_test.go b/app/controlplane/pkg/authz/authz_test.go index d97d98509..ffd60222d 100644 --- a/app/controlplane/pkg/authz/authz_test.go +++ b/app/controlplane/pkg/authz/authz_test.go @@ -134,7 +134,7 @@ func TestSyncRBACRoles(t *testing.T) { defer closer.Close() // load all the roles - err := syncRBACRoles(e) + err := syncRBACRoles(e, []Resource{}) assert.NoError(t, err) // Check the inherited roles owner -> admin -> viewer @@ -181,7 +181,7 @@ func TestDoSync(t *testing.T) { } // load custom policies - err := doSync(e, policiesM) + err := doSync(e, policiesM, []Resource{}) assert.NoError(t, err) got, err := e.GetPolicy() assert.NoError(t, err) @@ -197,7 +197,7 @@ func TestDoSync(t *testing.T) { }, } - err = doSync(e, policiesM) + err = doSync(e, policiesM, []Resource{ResourceWorkflowContract, ResourceCASArtifact}) assert.NoError(t, err) got, err = e.GetPolicy() assert.NoError(t, err) @@ -210,7 +210,7 @@ func TestDoSync(t *testing.T) { }, } - err = doSync(e, policiesM) + err = doSync(e, policiesM, []Resource{ResourceWorkflowContract, ResourceCASArtifact}) assert.NoError(t, err) got, err = e.GetPolicy() assert.NoError(t, err) @@ -257,7 +257,7 @@ func testEnforcer(t *testing.T) (*Enforcer, io.Closer) { require.FailNow(t, err.Error()) } - enforcer, err := NewFiletypeEnforcer(f.Name()) + enforcer, err := NewFiletypeEnforcer(f.Name(), []Resource{}) require.NoError(t, err) return enforcer, f } diff --git a/app/controlplane/pkg/biz/testhelpers/wire.go b/app/controlplane/pkg/biz/testhelpers/wire.go index ffe6f9436..c8d361e08 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire.go +++ b/app/controlplane/pkg/biz/testhelpers/wire.go @@ -61,10 +61,15 @@ func WireTestData(*TestDatabase, *testing.T, log.Logger, credentials.ReaderWrite NewCASServerOptions, newAuthAllowList, newJWTConfig, + authzManagedResources, ), ) } +func authzManagedResources() []authz.Resource { + return authz.AuthzManagedResources +} + func newJWTConfig(conf *conf.Auth) *biz.APITokenJWTConfig { return &biz.APITokenJWTConfig{ SymmetricHmacKey: conf.GeneratedJwsHmacSecret, diff --git a/app/controlplane/pkg/biz/testhelpers/wire_gen.go b/app/controlplane/pkg/biz/testhelpers/wire_gen.go index 3c5e93da5..84cfd32d5 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire_gen.go +++ b/app/controlplane/pkg/biz/testhelpers/wire_gen.go @@ -132,7 +132,8 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r } apiTokenRepo := data.NewAPITokenRepo(dataData, logger) apiTokenJWTConfig := newJWTConfig(auth) - enforcer, err := authz.NewDatabaseEnforcer(databaseConfig) + v3 := authzManagedResources() + enforcer, err := authz.NewDatabaseEnforcer(databaseConfig, v3) if err != nil { cleanup() return nil, nil, err @@ -200,6 +201,10 @@ var ( // wire.go: +func authzManagedResources() []authz.Resource { + return authz.AuthzManagedResources +} + func newJWTConfig(conf2 *conf.Auth) *biz.APITokenJWTConfig { return &biz.APITokenJWTConfig{ SymmetricHmacKey: conf2.GeneratedJwsHmacSecret, From 96b922e1656f1246a88faf2fe5aeb75b8d221860 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Fri, 27 Jun 2025 01:55:37 +0200 Subject: [PATCH 2/7] add test Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/controlplane/pkg/authz/authz_test.go b/app/controlplane/pkg/authz/authz_test.go index ffd60222d..151e69d61 100644 --- a/app/controlplane/pkg/authz/authz_test.go +++ b/app/controlplane/pkg/authz/authz_test.go @@ -215,6 +215,19 @@ func TestDoSync(t *testing.T) { got, err = e.GetPolicy() assert.NoError(t, err) assert.Len(t, got, 1) + + // add additional policy, only deletes policies for "known" resources + // or deleting a whole section + policiesM = map[Role][]*Policy{ + "bar": { + PolicyAttachedIntegrationDetach, + }, + } + err = doSync(e, policiesM, []Resource{}) + assert.NoError(t, err) + got, err = e.GetPolicy() + assert.NoError(t, err) + assert.Len(t, got, 2) } func TestClearPolicies(t *testing.T) { From d957f0b0d6e1736b20f4dafaf3536ebb453ab7ef Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Fri, 27 Jun 2025 09:50:08 +0200 Subject: [PATCH 3/7] create config struct Signed-off-by: Jose I. Paris --- app/controlplane/cmd/wire.go | 6 +- app/controlplane/cmd/wire_gen.go | 62 +++---- app/controlplane/pkg/authz/authz.go | 159 +++++++++--------- .../pkg/authz/authz_integration_test.go | 4 +- app/controlplane/pkg/authz/authz_test.go | 14 +- app/controlplane/pkg/biz/testhelpers/wire.go | 6 +- .../pkg/biz/testhelpers/wire_gen.go | 8 +- 7 files changed, 128 insertions(+), 131 deletions(-) diff --git a/app/controlplane/cmd/wire.go b/app/controlplane/cmd/wire.go index b3bd9f931..96c060462 100644 --- a/app/controlplane/cmd/wire.go +++ b/app/controlplane/cmd/wire.go @@ -64,13 +64,13 @@ func wireApp(*conf.Bootstrap, credentials.ReaderWriter, log.Logger, sdk.Availabl newCASServerOptions, newAuthAllowList, newJWTConfig, - authzManagedResources, + authzConfig, ), ) } -func authzManagedResources() []authz.Resource { - return authz.AuthzManagedResources +func authzConfig() *authz.Config { + return &authz.Config{ManagedResources: authz.ManagedResources, RolesMap: authz.RolesMap} } func newJWTConfig(conf *conf.Auth) *biz.APITokenJWTConfig { diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 4dca004ba..75e30189f 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -107,8 +107,8 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l } apiTokenRepo := data.NewAPITokenRepo(dataData, logger) apiTokenJWTConfig := newJWTConfig(auth) - v3 := authzManagedResources() - enforcer, err := authz.NewDatabaseEnforcer(databaseConfig, v3) + config := authzConfig() + enforcer, err := authz.NewDatabaseEnforcer(databaseConfig, config) if err != nil { cleanup() return nil, nil, err @@ -119,9 +119,9 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l return nil, nil, err } workflowContractRepo := data.NewWorkflowContractRepo(dataData, logger) - v4 := bootstrap.PolicyProviders - v5 := newPolicyProviderConfig(v4) - registry, err := policies.NewRegistry(logger, v5...) + v3 := bootstrap.PolicyProviders + v4 := newPolicyProviderConfig(v3) + registry, err := policies.NewRegistry(logger, v4...) if err != nil { cleanup() return nil, nil, err @@ -129,8 +129,8 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l workflowContractUseCase := biz.NewWorkflowContractUseCase(workflowContractRepo, registry, auditorUseCase, logger) workflowUseCase := biz.NewWorkflowUsecase(workflowRepo, projectsRepo, workflowContractUseCase, auditorUseCase, membershipUseCase, logger) projectUseCase := biz.NewProjectsUseCase(logger, projectsRepo) - v6 := serviceOpts(logger, enforcer, projectUseCase) - workflowService := service.NewWorkflowService(workflowUseCase, workflowContractUseCase, projectUseCase, v6...) + v5 := serviceOpts(logger, enforcer, projectUseCase) + workflowService := service.NewWorkflowService(workflowUseCase, workflowContractUseCase, projectUseCase, v5...) orgInvitationRepo := data.NewOrgInvitation(dataData, logger) orgInvitationUseCase, err := biz.NewOrgInvitationUseCase(orgInvitationRepo, membershipRepo, userRepo, auditorUseCase, logger) if err != nil { @@ -138,12 +138,12 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l return nil, nil, err } confServer := bootstrap.Server - authService, err := service.NewAuthService(userUseCase, organizationUseCase, membershipUseCase, orgInvitationUseCase, auth, confServer, auditorUseCase, v6...) + authService, err := service.NewAuthService(userUseCase, organizationUseCase, membershipUseCase, orgInvitationUseCase, auth, confServer, auditorUseCase, v5...) if err != nil { cleanup() return nil, nil, err } - robotAccountService := service.NewRobotAccountService(robotAccountUseCase, v6...) + robotAccountService := service.NewRobotAccountService(robotAccountUseCase, v5...) workflowRunRepo := data.NewWorkflowRunRepo(dataData, logger) signingUseCase, err := biz.NewChainloopSigningUseCase(bootstrap, logger) if err != nil { @@ -160,21 +160,21 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l WorkflowUC: workflowUseCase, WorkflowContractUC: workflowContractUseCase, CredsReader: readerWriter, - Opts: v6, + Opts: v5, } workflowRunService := service.NewWorkflowRunService(newWorkflowRunServiceOpts) attestationUseCase := biz.NewAttestationUseCase(casClientUseCase, logger) fanOutDispatcher := dispatcher.New(integrationUseCase, workflowUseCase, workflowRunUseCase, readerWriter, casClientUseCase, availablePlugins, logger) casMappingRepo := data.NewCASMappingRepo(dataData, casBackendRepo, logger) casMappingUseCase := biz.NewCASMappingUseCase(casMappingRepo, membershipRepo, projectsRepo, logger) - v7 := bootstrap.PrometheusIntegration + v6 := bootstrap.PrometheusIntegration orgMetricsRepo := data.NewOrgMetricsRepo(dataData, logger) orgMetricsUseCase, err := biz.NewOrgMetricsUseCase(orgMetricsRepo, organizationRepo, workflowUseCase, logger) if err != nil { cleanup() return nil, nil, err } - prometheusUseCase := biz.NewPrometheusUseCase(v7, organizationUseCase, orgMetricsUseCase, logger) + prometheusUseCase := biz.NewPrometheusUseCase(v6, organizationUseCase, orgMetricsUseCase, logger) projectVersionRepo := data.NewProjectVersionRepo(dataData, logger) projectVersionUseCase := biz.NewProjectVersionUseCase(projectVersionRepo, logger) newAttestationServiceOpts := &service.NewAttestationServiceOpts{ @@ -194,24 +194,24 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l ProjectUC: projectUseCase, ProjectVersionUC: projectVersionUseCase, SigningUseCase: signingUseCase, - Opts: v6, + Opts: v5, } attestationService := service.NewAttestationService(newAttestationServiceOpts) - workflowContractService := service.NewWorkflowSchemaService(workflowContractUseCase, v6...) - contextService := service.NewContextService(casBackendUseCase, userUseCase, v6...) - casCredentialsService := service.NewCASCredentialsService(casCredentialsUseCase, casMappingUseCase, casBackendUseCase, enforcer, v6...) - orgMetricsService := service.NewOrgMetricsService(orgMetricsUseCase, v6...) - integrationsService := service.NewIntegrationsService(integrationUseCase, workflowUseCase, availablePlugins, v6...) - organizationService := service.NewOrganizationService(membershipUseCase, organizationUseCase, v6...) - casBackendService := service.NewCASBackendService(casBackendUseCase, providers, v6...) - casRedirectService, err := service.NewCASRedirectService(casMappingUseCase, casCredentialsUseCase, bootstrap_CASServer, v6...) + workflowContractService := service.NewWorkflowSchemaService(workflowContractUseCase, v5...) + contextService := service.NewContextService(casBackendUseCase, userUseCase, v5...) + casCredentialsService := service.NewCASCredentialsService(casCredentialsUseCase, casMappingUseCase, casBackendUseCase, enforcer, v5...) + orgMetricsService := service.NewOrgMetricsService(orgMetricsUseCase, v5...) + integrationsService := service.NewIntegrationsService(integrationUseCase, workflowUseCase, availablePlugins, v5...) + organizationService := service.NewOrganizationService(membershipUseCase, organizationUseCase, v5...) + casBackendService := service.NewCASBackendService(casBackendUseCase, providers, v5...) + casRedirectService, err := service.NewCASRedirectService(casMappingUseCase, casCredentialsUseCase, bootstrap_CASServer, v5...) if err != nil { cleanup() return nil, nil, err } - orgInvitationService := service.NewOrgInvitationService(orgInvitationUseCase, v6...) - referrerService := service.NewReferrerService(referrerUseCase, v6...) - apiTokenService := service.NewAPITokenService(apiTokenUseCase, v6...) + orgInvitationService := service.NewOrgInvitationService(orgInvitationUseCase, v5...) + referrerService := service.NewReferrerService(referrerUseCase, v5...) + apiTokenService := service.NewAPITokenService(apiTokenUseCase, v5...) attestationStateRepo := data.NewAttestationStateRepo(dataData, logger) attestationStateUseCase, err := biz.NewAttestationStateUseCase(attestationStateRepo, workflowRunRepo) if err != nil { @@ -222,15 +222,15 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l AttestationStateUseCase: attestationStateUseCase, WorkflowUseCase: workflowUseCase, WorkflowRunUseCase: workflowRunUseCase, - Opts: v6, + Opts: v5, } attestationStateService := service.NewAttestationStateService(newAttestationStateServiceOpt) - userService := service.NewUserService(membershipUseCase, organizationUseCase, v6...) - signingService := service.NewSigningService(signingUseCase, v6...) - prometheusService := service.NewPrometheusService(organizationUseCase, prometheusUseCase, v6...) + userService := service.NewUserService(membershipUseCase, organizationUseCase, v5...) + signingService := service.NewSigningService(signingUseCase, v5...) + prometheusService := service.NewPrometheusService(organizationUseCase, prometheusUseCase, v5...) groupRepo := data.NewGroupRepo(dataData, logger) groupUseCase := biz.NewGroupUseCase(logger, groupRepo, membershipRepo, auditorUseCase) - groupService := service.NewGroupService(groupUseCase, v6...) + groupService := service.NewGroupService(groupUseCase, v5...) federatedAuthentication := bootstrap.FederatedAuthentication validator, err := newProtoValidator() if err != nil { @@ -311,8 +311,8 @@ var ( // wire.go: -func authzManagedResources() []authz.Resource { - return authz.AuthzManagedResources +func authzConfig() *authz.Config { + return &authz.Config{ManagedResources: authz.ManagedResources, RolesMap: authz.RolesMap} } func newJWTConfig(conf2 *conf.Auth) *biz.APITokenJWTConfig { diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 05ce21a73..62fbe7353 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -32,8 +32,18 @@ import ( "golang.org/x/exp/slices" ) +// resource, action tuple +type Policy struct { + Resource string + Action string +} + type Role string -type Resource string + +type Config struct { + ManagedResources []string + RolesMap map[Role][]*Policy +} const ( // Actions @@ -46,20 +56,20 @@ const ( // Resources - ResourceWorkflowContract Resource = "workflow_contract" - ResourceCASArtifact Resource = "cas_artifact" - ResourceCASBackend Resource = "cas_backend" - ResourceReferrer Resource = "referrer" - ResourceAvailableIntegration Resource = "integration_available" - ResourceRegisteredIntegration Resource = "integration_registered" - ResourceAttachedIntegration Resource = "integration_attached" - ResourceOrgMetric Resource = "metrics_org" - ResourceRobotAccount Resource = "robot_account" - ResourceWorkflowRun Resource = "workflow_run" - ResourceWorkflow Resource = "workflow" - Organization Resource = "organization" - ResourceGroup Resource = "group" - ResourceGroupMembership Resource = "group_membership" + ResourceWorkflowContract = "workflow_contract" + ResourceCASArtifact = "cas_artifact" + ResourceCASBackend = "cas_backend" + ResourceReferrer = "referrer" + ResourceAvailableIntegration = "integration_available" + ResourceRegisteredIntegration = "integration_registered" + ResourceAttachedIntegration = "integration_attached" + ResourceOrgMetric = "metrics_org" + ResourceRobotAccount = "robot_account" + ResourceWorkflowRun = "workflow_run" + ResourceWorkflow = "workflow" + Organization = "organization" + ResourceGroup = "group" + ResourceGroupMembership = "group_membership" // We have for now three roles, viewer, admin and owner // The owner of an org @@ -83,8 +93,8 @@ const ( RoleProjectViewer Role = "role:project:viewer" ) -// AuthzManagedResources are the resources that are managed by Chainloop, considered during permissions sync -var AuthzManagedResources = []Resource{ +// ManagedResources are the resources that are managed by Chainloop, considered during permissions sync +var ManagedResources = []string{ ResourceWorkflowContract, ResourceCASArtifact, ResourceCASBackend, @@ -101,76 +111,63 @@ var AuthzManagedResources = []Resource{ ResourceGroupMembership, } -// resource, action tuple -type Policy struct { - Resource string - Action string -} - -func NewPolicy(r Resource, action string) *Policy { - return &Policy{ - Resource: string(r), - Action: action, - } -} - var ( // Referrer - PolicyReferrerRead = NewPolicy(ResourceReferrer, ActionRead) + PolicyReferrerRead = &Policy{ResourceReferrer, ActionRead} // Artifact - PolicyArtifactDownload = NewPolicy(ResourceCASArtifact, ActionRead) - PolicyArtifactUpload = NewPolicy(ResourceCASArtifact, ActionCreate) + PolicyArtifactDownload = &Policy{ResourceCASArtifact, ActionRead} + PolicyArtifactUpload = &Policy{ResourceCASArtifact, ActionCreate} // CAS backend - PolicyCASBackendList = NewPolicy(ResourceCASBackend, ActionList) + PolicyCASBackendList = &Policy{ResourceCASBackend, ActionList} // Available integrations - PolicyAvailableIntegrationList = NewPolicy(ResourceAvailableIntegration, ActionList) - PolicyAvailableIntegrationRead = NewPolicy(ResourceAvailableIntegration, ActionRead) + PolicyAvailableIntegrationList = &Policy{ResourceAvailableIntegration, ActionList} + PolicyAvailableIntegrationRead = &Policy{ResourceAvailableIntegration, ActionRead} // Registered integrations - PolicyRegisteredIntegrationList = NewPolicy(ResourceRegisteredIntegration, ActionList) - PolicyRegisteredIntegrationRead = NewPolicy(ResourceRegisteredIntegration, ActionRead) - PolicyRegisteredIntegrationAdd = NewPolicy(ResourceRegisteredIntegration, ActionCreate) + PolicyRegisteredIntegrationList = &Policy{ResourceRegisteredIntegration, ActionList} + PolicyRegisteredIntegrationRead = &Policy{ResourceRegisteredIntegration, ActionRead} + PolicyRegisteredIntegrationAdd = &Policy{ResourceRegisteredIntegration, ActionCreate} // Attached integrations - PolicyAttachedIntegrationList = NewPolicy(ResourceAttachedIntegration, ActionList) - PolicyAttachedIntegrationAttach = NewPolicy(ResourceAttachedIntegration, ActionCreate) - PolicyAttachedIntegrationDetach = NewPolicy(ResourceAttachedIntegration, ActionDelete) + PolicyAttachedIntegrationList = &Policy{ResourceAttachedIntegration, ActionList} + PolicyAttachedIntegrationAttach = &Policy{ResourceAttachedIntegration, ActionCreate} + PolicyAttachedIntegrationDetach = &Policy{ResourceAttachedIntegration, ActionDelete} // Org Metrics - PolicyOrgMetricsRead = NewPolicy(ResourceOrgMetric, ActionList) + PolicyOrgMetricsRead = &Policy{ResourceOrgMetric, ActionList} // Robot Account - PolicyRobotAccountList = NewPolicy(ResourceRobotAccount, ActionList) - PolicyRobotAccountCreate = NewPolicy(ResourceRobotAccount, ActionCreate) + PolicyRobotAccountList = &Policy{ResourceRobotAccount, ActionList} + PolicyRobotAccountCreate = &Policy{ResourceRobotAccount, ActionCreate} // Workflow Contract - PolicyWorkflowContractList = NewPolicy(ResourceWorkflowContract, ActionList) - PolicyWorkflowContractRead = NewPolicy(ResourceWorkflowContract, ActionRead) - PolicyWorkflowContractUpdate = NewPolicy(ResourceWorkflowContract, ActionUpdate) - PolicyWorkflowContractCreate = NewPolicy(ResourceWorkflowContract, ActionCreate) + PolicyWorkflowContractList = &Policy{ResourceWorkflowContract, ActionList} + PolicyWorkflowContractRead = &Policy{ResourceWorkflowContract, ActionRead} + PolicyWorkflowContractUpdate = &Policy{ResourceWorkflowContract, ActionUpdate} + PolicyWorkflowContractCreate = &Policy{ResourceWorkflowContract, ActionCreate} // WorkflowRun - PolicyWorkflowRunList = NewPolicy(ResourceWorkflowRun, ActionList) - PolicyWorkflowRunRead = NewPolicy(ResourceWorkflowRun, ActionRead) - PolicyWorkflowRunCreate = NewPolicy(ResourceWorkflowRun, ActionCreate) - PolicyWorkflowRunUpdate = NewPolicy(ResourceWorkflowRun, ActionUpdate) + PolicyWorkflowRunList = &Policy{ResourceWorkflowRun, ActionList} + PolicyWorkflowRunRead = &Policy{ResourceWorkflowRun, ActionRead} + PolicyWorkflowRunCreate = &Policy{ResourceWorkflowRun, ActionCreate} + PolicyWorkflowRunUpdate = &Policy{ResourceWorkflowRun, ActionUpdate} // Workflow - PolicyWorkflowList = NewPolicy(ResourceWorkflow, ActionList) - PolicyWorkflowRead = NewPolicy(ResourceWorkflow, ActionRead) - PolicyWorkflowCreate = NewPolicy(ResourceWorkflow, ActionCreate) - PolicyWorkflowUpdate = NewPolicy(ResourceWorkflow, ActionUpdate) - PolicyWorkflowDelete = NewPolicy(ResourceWorkflow, ActionDelete) + PolicyWorkflowList = &Policy{ResourceWorkflow, ActionList} + PolicyWorkflowRead = &Policy{ResourceWorkflow, ActionRead} + PolicyWorkflowCreate = &Policy{ResourceWorkflow, ActionCreate} + PolicyWorkflowUpdate = &Policy{ResourceWorkflow, ActionUpdate} + PolicyWorkflowDelete = &Policy{ResourceWorkflow, ActionDelete} // User Membership - PolicyOrganizationRead = NewPolicy(Organization, ActionRead) - PolicyOrganizationListMemberships = NewPolicy(Organization, ActionRead) + PolicyOrganizationRead = &Policy{Organization, ActionRead} + PolicyOrganizationListMemberships = &Policy{Organization, ActionRead} // Groups - PolicyGroupCreate = NewPolicy(ResourceGroup, ActionCreate) - PolicyGroupUpdate = NewPolicy(ResourceGroup, ActionUpdate) - PolicyGroupDelete = NewPolicy(ResourceGroup, ActionDelete) - PolicyGroupList = NewPolicy(ResourceGroup, ActionList) - PolicyGroupRead = NewPolicy(ResourceGroup, ActionRead) + PolicyGroupCreate = &Policy{ResourceGroup, ActionCreate} + PolicyGroupUpdate = &Policy{ResourceGroup, ActionUpdate} + PolicyGroupDelete = &Policy{ResourceGroup, ActionDelete} + PolicyGroupList = &Policy{ResourceGroup, ActionList} + PolicyGroupRead = &Policy{ResourceGroup, ActionRead} // Group Memberships - PolicyGroupListMemberships = NewPolicy(ResourceGroupMembership, ActionList) + PolicyGroupListMemberships = &Policy{ResourceGroupMembership, ActionList} ) -// List of policies for each role -// NOTE: roles are hierarchical, this means that the Admin Role can inherit all the policies from the Viewer Role +// RolesMap The default list of policies for each role +// NOTE: roles are not necessarily hierarchical, however the Admin Role inherits all the policies from the Viewer Role // so we do not need to add them as well. -var rolesMap = map[Role][]*Policy{ +var RolesMap = map[Role][]*Policy{ // RoleViewer is an org-scoped role that provides read-only access to all resources RoleViewer: { // Referrer @@ -413,14 +410,14 @@ func (e *Enforcer) ClearPolicies(sub *SubjectAPIToken) error { // NewDatabaseEnforcer creates a new casbin authorization enforcer // based on a database backend as policies storage backend -func NewDatabaseEnforcer(c *config.DatabaseConfig, managedResources []Resource) (*Enforcer, error) { +func NewDatabaseEnforcer(c *config.DatabaseConfig, config *Config) (*Enforcer, error) { // policy storage in database a, err := entadapter.NewAdapter(c.Driver, c.Source) if err != nil { return nil, fmt.Errorf("failed to create adapter: %w", err) } - e, err := newEnforcer(a, managedResources) + e, err := newEnforcer(a, config) if err != nil { return nil, fmt.Errorf("failed to create enforcer: %w", err) } @@ -449,10 +446,10 @@ func NewDatabaseEnforcer(c *config.DatabaseConfig, managedResources []Resource) // NewFileAdapter creates a new casbin authorization enforcer // based on a CSV file as policies storage backend -func NewFiletypeEnforcer(path string, managedResources []Resource) (*Enforcer, error) { +func NewFiletypeEnforcer(path string, config *Config) (*Enforcer, error) { // policy storage in filesystem a := fileadapter.NewAdapter(path) - e, err := newEnforcer(a, managedResources) + e, err := newEnforcer(a, config) if err != nil { return nil, fmt.Errorf("failed to create enforcer: %w", err) } @@ -462,7 +459,7 @@ func NewFiletypeEnforcer(path string, managedResources []Resource) (*Enforcer, e // NewEnforcer creates a new casbin authorization enforcer for the policies stored // in the database and the model defined in model.conf -func newEnforcer(a persist.Adapter, managedResources []Resource) (*Enforcer, error) { +func newEnforcer(a persist.Adapter, config *Config) (*Enforcer, error) { // load model defined in model.conf m, err := model.NewModelFromString(string(modelFile)) if err != nil { @@ -476,7 +473,7 @@ func newEnforcer(a persist.Adapter, managedResources []Resource) (*Enforcer, err } // Initialize the enforcer with the roles map - if err := syncRBACRoles(&Enforcer{enforcer}, managedResources); err != nil { + if err := syncRBACRoles(&Enforcer{enforcer}, config); err != nil { return nil, fmt.Errorf("failed to sync roles: %w", err) } @@ -486,13 +483,13 @@ func newEnforcer(a persist.Adapter, managedResources []Resource) (*Enforcer, err // Load the roles map into the enforcer // This is done by adding all the policies defined in the roles map // and removing all the policies that are not -func syncRBACRoles(e *Enforcer, managedResources []Resource) error { - return doSync(e, rolesMap, managedResources) +func syncRBACRoles(e *Enforcer, config *Config) error { + return doSync(e, config) } -func doSync(e *Enforcer, rolesMap map[Role][]*Policy, managedResources []Resource) error { +func doSync(e *Enforcer, config *Config) error { // Add all the defined policies if they don't exist - for role, policies := range rolesMap { + for role, policies := range config.RolesMap { for _, p := range policies { // Add policies one by one to skip existing ones. // This is because the bulk method AddPoliciesEx does not work well with the ent adapter @@ -518,11 +515,11 @@ func doSync(e *Enforcer, rolesMap map[Role][]*Policy, managedResources []Resourc policy := &Policy{Resource: resource, Action: action} // if it's not a managed resource, skip deletion - if !slices.Contains(managedResources, Resource(resource)) { + if !slices.Contains(config.ManagedResources, resource) { continue } - wantPolicies, ok := rolesMap[Role(role)] + wantPolicies, ok := config.RolesMap[Role(role)] // if the role does not exist in the map, we can delete the policy if !ok { _, err := e.RemovePolicy(role, policy.Resource, policy.Action) diff --git a/app/controlplane/pkg/authz/authz_integration_test.go b/app/controlplane/pkg/authz/authz_integration_test.go index 4f3e3a253..b661c58b3 100644 --- a/app/controlplane/pkg/authz/authz_integration_test.go +++ b/app/controlplane/pkg/authz/authz_integration_test.go @@ -33,9 +33,9 @@ func TestMultiReplicaPropagation(t *testing.T) { db := testhelpers.NewTestDatabase(t) defer db.Close(t) - enforcerA, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)), []authz.Resource{}) + enforcerA, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)), &authz.Config{}) require.NoError(t, err) - enforcerB, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)), []authz.Resource{}) + enforcerB, err := authz.NewDatabaseEnforcer(testhelpers.NewDataConfig(testhelpers.NewConfData(db, t)), &authz.Config{}) require.NoError(t, err) // Subject and policies to add diff --git a/app/controlplane/pkg/authz/authz_test.go b/app/controlplane/pkg/authz/authz_test.go index 151e69d61..b8f53a528 100644 --- a/app/controlplane/pkg/authz/authz_test.go +++ b/app/controlplane/pkg/authz/authz_test.go @@ -134,7 +134,7 @@ func TestSyncRBACRoles(t *testing.T) { defer closer.Close() // load all the roles - err := syncRBACRoles(e, []Resource{}) + err := syncRBACRoles(e, &Config{RolesMap: RolesMap}) assert.NoError(t, err) // Check the inherited roles owner -> admin -> viewer @@ -148,7 +148,7 @@ func TestSyncRBACRoles(t *testing.T) { assert.Equal(t, string(RoleOwner), u[0]) // Make sure we are adding all the policies for the listed roles - for r, policies := range rolesMap { + for r, policies := range RolesMap { got, err := e.GetFilteredPolicy(0, string(r)) assert.NoError(t, err) assert.Len(t, got, len(policies)) @@ -181,7 +181,7 @@ func TestDoSync(t *testing.T) { } // load custom policies - err := doSync(e, policiesM, []Resource{}) + err := doSync(e, &Config{RolesMap: policiesM}) assert.NoError(t, err) got, err := e.GetPolicy() assert.NoError(t, err) @@ -197,7 +197,7 @@ func TestDoSync(t *testing.T) { }, } - err = doSync(e, policiesM, []Resource{ResourceWorkflowContract, ResourceCASArtifact}) + err = doSync(e, &Config{RolesMap: policiesM, ManagedResources: []string{ResourceWorkflowContract, ResourceCASArtifact}}) assert.NoError(t, err) got, err = e.GetPolicy() assert.NoError(t, err) @@ -210,7 +210,7 @@ func TestDoSync(t *testing.T) { }, } - err = doSync(e, policiesM, []Resource{ResourceWorkflowContract, ResourceCASArtifact}) + err = doSync(e, &Config{RolesMap: policiesM, ManagedResources: []string{ResourceWorkflowContract, ResourceCASArtifact}}) assert.NoError(t, err) got, err = e.GetPolicy() assert.NoError(t, err) @@ -223,7 +223,7 @@ func TestDoSync(t *testing.T) { PolicyAttachedIntegrationDetach, }, } - err = doSync(e, policiesM, []Resource{}) + err = doSync(e, &Config{RolesMap: policiesM}) assert.NoError(t, err) got, err = e.GetPolicy() assert.NoError(t, err) @@ -270,7 +270,7 @@ func testEnforcer(t *testing.T) (*Enforcer, io.Closer) { require.FailNow(t, err.Error()) } - enforcer, err := NewFiletypeEnforcer(f.Name(), []Resource{}) + enforcer, err := NewFiletypeEnforcer(f.Name(), &Config{}) require.NoError(t, err) return enforcer, f } diff --git a/app/controlplane/pkg/biz/testhelpers/wire.go b/app/controlplane/pkg/biz/testhelpers/wire.go index c8d361e08..68d3a74a9 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire.go +++ b/app/controlplane/pkg/biz/testhelpers/wire.go @@ -61,13 +61,13 @@ func WireTestData(*TestDatabase, *testing.T, log.Logger, credentials.ReaderWrite NewCASServerOptions, newAuthAllowList, newJWTConfig, - authzManagedResources, + authzConfig, ), ) } -func authzManagedResources() []authz.Resource { - return authz.AuthzManagedResources +func authzConfig() *authz.Config { + return &authz.Config{ManagedResources: authz.ManagedResources, RolesMap: authz.RolesMap} } func newJWTConfig(conf *conf.Auth) *biz.APITokenJWTConfig { diff --git a/app/controlplane/pkg/biz/testhelpers/wire_gen.go b/app/controlplane/pkg/biz/testhelpers/wire_gen.go index 84cfd32d5..720770173 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire_gen.go +++ b/app/controlplane/pkg/biz/testhelpers/wire_gen.go @@ -132,8 +132,8 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r } apiTokenRepo := data.NewAPITokenRepo(dataData, logger) apiTokenJWTConfig := newJWTConfig(auth) - v3 := authzManagedResources() - enforcer, err := authz.NewDatabaseEnforcer(databaseConfig, v3) + config := authzConfig() + enforcer, err := authz.NewDatabaseEnforcer(databaseConfig, config) if err != nil { cleanup() return nil, nil, err @@ -201,8 +201,8 @@ var ( // wire.go: -func authzManagedResources() []authz.Resource { - return authz.AuthzManagedResources +func authzConfig() *authz.Config { + return &authz.Config{ManagedResources: authz.ManagedResources, RolesMap: authz.RolesMap} } func newJWTConfig(conf2 *conf.Auth) *biz.APITokenJWTConfig { From a5f4ab9728abca60b491d991ff0da516732ba72d Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Fri, 27 Jun 2025 13:55:33 +0200 Subject: [PATCH 4/7] move enforcer to its own file Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 234 -------------------- app/controlplane/pkg/authz/authz_test.go | 8 + app/controlplane/pkg/authz/enforcer.go | 260 +++++++++++++++++++++++ 3 files changed, 268 insertions(+), 234 deletions(-) create mode 100644 app/controlplane/pkg/authz/enforcer.go diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 62fbe7353..53e27594d 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -16,22 +16,6 @@ // Authorization package package authz -import ( - "context" - _ "embed" - "errors" - "fmt" - - psqlwatcher "github.com/IguteChung/casbin-psql-watcher" - "github.com/casbin/casbin/v2" - "github.com/casbin/casbin/v2/model" - "github.com/casbin/casbin/v2/persist" - fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" - entadapter "github.com/casbin/ent-adapter" - config "github.com/chainloop-dev/chainloop/app/controlplane/pkg/conf/controlplane/config/v1" - "golang.org/x/exp/slices" -) - // resource, action tuple type Policy struct { Resource string @@ -40,11 +24,6 @@ type Policy struct { type Role string -type Config struct { - ManagedResources []string - RolesMap map[Role][]*Policy -} - const ( // Actions @@ -349,219 +328,6 @@ var ServerOperationsMap = map[string][]*Policy{ "/controlplane.v1.GroupService/ListMembers": {PolicyGroupListMemberships}, } -type SubjectAPIToken struct { - ID string -} - -func (t *SubjectAPIToken) String() string { - return fmt.Sprintf("api-token:%s", t.ID) -} - -//go:embed model.conf -var modelFile []byte - -type Enforcer struct { - *casbin.Enforcer -} - -func (e *Enforcer) AddPolicies(sub *SubjectAPIToken, policies ...*Policy) error { - if len(policies) == 0 { - return errors.New("no policies to add") - } - - if sub == nil { - return errors.New("no subject provided") - } - - for _, p := range policies { - casbinPolicy := []string{sub.String(), p.Resource, p.Action} - // Add policies one by one to skip existing ones. - // This is because the bulk method AddPoliciesEx does not work well with the ent adapter - if _, err := e.AddPolicy(casbinPolicy); err != nil { - return fmt.Errorf("failed to add policy: %w", err) - } - } - - return nil -} - -func (e *Enforcer) Enforce(sub string, p *Policy) (bool, error) { - return e.Enforcer.Enforce(sub, p.Resource, p.Action) -} - -// Remove all the policies for the given subject -func (e *Enforcer) ClearPolicies(sub *SubjectAPIToken) error { - if sub == nil { - return errors.New("no subject provided") - } - - // Get all the policies for the subject - policies, err := e.GetFilteredPolicy(0, sub.String()) - if err != nil { - return fmt.Errorf("failed to get policies: %w", err) - } - - if _, err := e.Enforcer.RemovePolicies(policies); err != nil { - return fmt.Errorf("failed to remove policies: %w", err) - } - - return nil -} - -// NewDatabaseEnforcer creates a new casbin authorization enforcer -// based on a database backend as policies storage backend -func NewDatabaseEnforcer(c *config.DatabaseConfig, config *Config) (*Enforcer, error) { - // policy storage in database - a, err := entadapter.NewAdapter(c.Driver, c.Source) - if err != nil { - return nil, fmt.Errorf("failed to create adapter: %w", err) - } - - e, err := newEnforcer(a, config) - if err != nil { - return nil, fmt.Errorf("failed to create enforcer: %w", err) - } - - // watch for policy changes in database and update enforcer - w, err := psqlwatcher.NewWatcherWithConnString(context.Background(), c.Source, psqlwatcher.Option{}) - if err != nil { - return nil, fmt.Errorf("failed to create watcher: %w", err) - } - - if err = e.SetWatcher(w); err != nil { - return nil, fmt.Errorf("failed to set watcher: %w", err) - } - - if err = w.SetUpdateCallback(func(string) { - // When there is a change in the policy, we load the in-memory policy for the current enforcer - if err := e.LoadPolicy(); err != nil { - fmt.Printf("failed to load policy: %v", err) - } - }); err != nil { - return nil, fmt.Errorf("failed to set update callback: %w", err) - } - - return e, nil -} - -// NewFileAdapter creates a new casbin authorization enforcer -// based on a CSV file as policies storage backend -func NewFiletypeEnforcer(path string, config *Config) (*Enforcer, error) { - // policy storage in filesystem - a := fileadapter.NewAdapter(path) - e, err := newEnforcer(a, config) - if err != nil { - return nil, fmt.Errorf("failed to create enforcer: %w", err) - } - - return e, nil -} - -// NewEnforcer creates a new casbin authorization enforcer for the policies stored -// in the database and the model defined in model.conf -func newEnforcer(a persist.Adapter, config *Config) (*Enforcer, error) { - // load model defined in model.conf - m, err := model.NewModelFromString(string(modelFile)) - if err != nil { - return nil, fmt.Errorf("failed to create model: %w", err) - } - - // create enforcer for authorization - enforcer, err := casbin.NewEnforcer(m, a) - if err != nil { - return nil, fmt.Errorf("failed to create enforcer: %w", err) - } - - // Initialize the enforcer with the roles map - if err := syncRBACRoles(&Enforcer{enforcer}, config); err != nil { - return nil, fmt.Errorf("failed to sync roles: %w", err) - } - - return &Enforcer{enforcer}, nil -} - -// Load the roles map into the enforcer -// This is done by adding all the policies defined in the roles map -// and removing all the policies that are not -func syncRBACRoles(e *Enforcer, config *Config) error { - return doSync(e, config) -} - -func doSync(e *Enforcer, config *Config) error { - // Add all the defined policies if they don't exist - for role, policies := range config.RolesMap { - for _, p := range policies { - // Add policies one by one to skip existing ones. - // This is because the bulk method AddPoliciesEx does not work well with the ent adapter - casbinPolicy := []string{string(role), p.Resource, p.Action} - _, err := e.AddPolicy(casbinPolicy) - if err != nil { - return fmt.Errorf("failed to add policy: %w", err) - } - } - } - - // Delete all the policies that are not in the roles map - // 1 - load the policies from the enforcer DB - policies, err := e.GetPolicy() - if err != nil { - return fmt.Errorf("failed to get policies: %w", err) - } - - for _, gotPolicies := range policies { - role := gotPolicies[0] - resource := gotPolicies[1] - action := gotPolicies[2] - policy := &Policy{Resource: resource, Action: action} - - // if it's not a managed resource, skip deletion - if !slices.Contains(config.ManagedResources, resource) { - continue - } - - wantPolicies, ok := config.RolesMap[Role(role)] - // if the role does not exist in the map, we can delete the policy - if !ok { - _, err := e.RemovePolicy(role, policy.Resource, policy.Action) - if err != nil { - return fmt.Errorf("failed to remove policy: %w", err) - } - continue - } - - // We have the role in the map, so we now compare the policies - found := false - for _, p := range wantPolicies { - if p.Resource == policy.Resource && p.Action == policy.Action { - found = true - break - } - } - - // If the policy is not in the map, we remove it - if !found { - _, err := e.RemovePolicy(gotPolicies) - if err != nil { - return fmt.Errorf("failed to remove policy: %w", err) - } - } - } - - // To finish we make sure that the admin role inherit all the policies from the viewer role - _, err = e.AddGroupingPolicy(string(RoleAdmin), string(RoleViewer)) - if err != nil { - return fmt.Errorf("failed to add grouping policy: %w", err) - } - - // same for the owner - _, err = e.AddGroupingPolicy(string(RoleOwner), string(RoleAdmin)) - if err != nil { - return fmt.Errorf("failed to add grouping policy: %w", err) - } - - return nil -} - // Implements https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues // so they can be added to the database schema func (Role) Values() (roles []string) { diff --git a/app/controlplane/pkg/authz/authz_test.go b/app/controlplane/pkg/authz/authz_test.go index b8f53a528..aee9f4117 100644 --- a/app/controlplane/pkg/authz/authz_test.go +++ b/app/controlplane/pkg/authz/authz_test.go @@ -103,6 +103,14 @@ func TestAddPolicies(t *testing.T) { } } +func TestSyncMultipleEnforcers(t *testing.T) { + e1, c := testEnforcer(t) + defer c.Close() + e2, c := testEnforcer(t) + defer c.Close() + +} + func TestAddPoliciesDuplication(t *testing.T) { want := []*Policy{ PolicyWorkflowContractList, diff --git a/app/controlplane/pkg/authz/enforcer.go b/app/controlplane/pkg/authz/enforcer.go new file mode 100644 index 000000000..40fcdad95 --- /dev/null +++ b/app/controlplane/pkg/authz/enforcer.go @@ -0,0 +1,260 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authz + +import ( + "context" + _ "embed" + "errors" + "fmt" + "slices" + + psqlwatcher "github.com/IguteChung/casbin-psql-watcher" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + "github.com/casbin/casbin/v2/persist" + fileadapter "github.com/casbin/casbin/v2/persist/file-adapter" + entadapter "github.com/casbin/ent-adapter" + config "github.com/chainloop-dev/chainloop/app/controlplane/pkg/conf/controlplane/config/v1" +) + +type SubjectAPIToken struct { + ID string +} + +func (t *SubjectAPIToken) String() string { + return fmt.Sprintf("api-token:%s", t.ID) +} + +//go:embed model.conf +var modelFile []byte + +type Config struct { + ManagedResources []string + RolesMap map[Role][]*Policy +} + +type Enforcer struct { + *casbin.Enforcer + + config *Config +} + +func (e *Enforcer) AddPolicies(sub *SubjectAPIToken, policies ...*Policy) error { + if len(policies) == 0 { + return errors.New("no policies to add") + } + + if sub == nil { + return errors.New("no subject provided") + } + + for _, p := range policies { + casbinPolicy := []string{sub.String(), p.Resource, p.Action} + // Add policies one by one to skip existing ones. + // This is because the bulk method AddPoliciesEx does not work well with the ent adapter + if _, err := e.AddPolicy(casbinPolicy); err != nil { + return fmt.Errorf("failed to add policy: %w", err) + } + } + + return nil +} + +func (e *Enforcer) Enforce(sub string, p *Policy) (bool, error) { + return e.Enforcer.Enforce(sub, p.Resource, p.Action) +} + +// Remove all the policies for the given subject +func (e *Enforcer) ClearPolicies(sub *SubjectAPIToken) error { + if sub == nil { + return errors.New("no subject provided") + } + + // Get all the policies for the subject + policies, err := e.GetFilteredPolicy(0, sub.String()) + if err != nil { + return fmt.Errorf("failed to get policies: %w", err) + } + + if _, err := e.Enforcer.RemovePolicies(policies); err != nil { + return fmt.Errorf("failed to remove policies: %w", err) + } + + return nil +} + +// NewDatabaseEnforcer creates a new casbin authorization enforcer +// based on a database backend as policies storage backend +func NewDatabaseEnforcer(c *config.DatabaseConfig, config *Config) (*Enforcer, error) { + // policy storage in database + a, err := entadapter.NewAdapter(c.Driver, c.Source) + if err != nil { + return nil, fmt.Errorf("failed to create adapter: %w", err) + } + + e, err := newEnforcer(a, config) + if err != nil { + return nil, fmt.Errorf("failed to create enforcer: %w", err) + } + + // watch for policy changes in database and update enforcer + w, err := psqlwatcher.NewWatcherWithConnString(context.Background(), c.Source, psqlwatcher.Option{}) + if err != nil { + return nil, fmt.Errorf("failed to create watcher: %w", err) + } + + if err = e.SetWatcher(w); err != nil { + return nil, fmt.Errorf("failed to set watcher: %w", err) + } + + if err = w.SetUpdateCallback(func(string) { + // When there is a change in the policy, we load the in-memory policy for the current enforcer + if err := e.LoadPolicy(); err != nil { + fmt.Printf("failed to load policy: %v", err) + } + }); err != nil { + return nil, fmt.Errorf("failed to set update callback: %w", err) + } + + return e, nil +} + +// NewFileAdapter creates a new casbin authorization enforcer +// based on a CSV file as policies storage backend +func NewFiletypeEnforcer(path string, config *Config) (*Enforcer, error) { + // policy storage in filesystem + a := fileadapter.NewAdapter(path) + e, err := newEnforcer(a, config) + if err != nil { + return nil, fmt.Errorf("failed to create enforcer: %w", err) + } + + return e, nil +} + +// NewEnforcer creates a new casbin authorization enforcer for the policies stored +// in the database and the model defined in model.conf +func newEnforcer(a persist.Adapter, config *Config) (*Enforcer, error) { + // load model defined in model.conf + m, err := model.NewModelFromString(string(modelFile)) + if err != nil { + return nil, fmt.Errorf("failed to create model: %w", err) + } + + // create enforcer for authorization + enforcer, err := casbin.NewEnforcer(m, a) + if err != nil { + return nil, fmt.Errorf("failed to create enforcer: %w", err) + } + + e := &Enforcer{enforcer, config} + + // Initialize the enforcer with the roles map + if err := syncRBACRoles(e, config); err != nil { + return nil, fmt.Errorf("failed to sync roles: %w", err) + } + + return e, nil +} + +// Load the roles map into the enforcer +// This is done by adding all the policies defined in the roles map +// and removing all the policies that are not +func syncRBACRoles(e *Enforcer, config *Config) error { + return doSync(e, config) +} + +func doSync(e *Enforcer, c *Config) error { + // allow to override config during sync + conf := c + if conf == nil { + conf = e.config + } + + // Add all the defined policies if they don't exist + for role, policies := range conf.RolesMap { + for _, p := range policies { + // Add policies one by one to skip existing ones. + // This is because the bulk method AddPoliciesEx does not work well with the ent adapter + casbinPolicy := []string{string(role), p.Resource, p.Action} + _, err := e.AddPolicy(casbinPolicy) + if err != nil { + return fmt.Errorf("failed to add policy: %w", err) + } + } + } + + // Delete all the policies that are not in the roles map + // 1 - load the policies from the enforcer DB + policies, err := e.GetPolicy() + if err != nil { + return fmt.Errorf("failed to get policies: %w", err) + } + + for _, gotPolicies := range policies { + role := gotPolicies[0] + resource := gotPolicies[1] + action := gotPolicies[2] + policy := &Policy{Resource: resource, Action: action} + + // if it's not a managed resource, skip deletion + if !slices.Contains(conf.ManagedResources, resource) { + continue + } + + wantPolicies, ok := conf.RolesMap[Role(role)] + // if the role does not exist in the map, we can delete the policy + if !ok { + _, err := e.RemovePolicy(role, policy.Resource, policy.Action) + if err != nil { + return fmt.Errorf("failed to remove policy: %w", err) + } + continue + } + + // We have the role in the map, so we now compare the policies + found := false + for _, p := range wantPolicies { + if p.Resource == policy.Resource && p.Action == policy.Action { + found = true + break + } + } + + // If the policy is not in the map, we remove it + if !found { + _, err := e.RemovePolicy(gotPolicies) + if err != nil { + return fmt.Errorf("failed to remove policy: %w", err) + } + } + } + + // To finish we make sure that the admin role inherit all the policies from the viewer role + _, err = e.AddGroupingPolicy(string(RoleAdmin), string(RoleViewer)) + if err != nil { + return fmt.Errorf("failed to add grouping policy: %w", err) + } + + // same for the owner + _, err = e.AddGroupingPolicy(string(RoleOwner), string(RoleAdmin)) + if err != nil { + return fmt.Errorf("failed to add grouping policy: %w", err) + } + + return nil +} From b2492d9f09bde8179899889ade4b8f75bd8d0add Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Fri, 27 Jun 2025 15:03:31 +0200 Subject: [PATCH 5/7] add tests for collisions Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz_test.go | 116 ++++++++++++++++++++++- 1 file changed, 112 insertions(+), 4 deletions(-) diff --git a/app/controlplane/pkg/authz/authz_test.go b/app/controlplane/pkg/authz/authz_test.go index aee9f4117..1043fe31e 100644 --- a/app/controlplane/pkg/authz/authz_test.go +++ b/app/controlplane/pkg/authz/authz_test.go @@ -103,12 +103,120 @@ func TestAddPolicies(t *testing.T) { } } +// simulate 2 enforcers on the same database (by acting on the same file enforcer) func TestSyncMultipleEnforcers(t *testing.T) { - e1, c := testEnforcer(t) - defer c.Close() - e2, c := testEnforcer(t) - defer c.Close() + testCases := []struct { + name string + newEnforcerConfig *Config + expectErr bool + numPolicies int + numSubjects int + numAdminActions int + }{ + { + name: "empty config", + newEnforcerConfig: &Config{}, + expectErr: false, + numPolicies: 3, + numSubjects: 2, + numAdminActions: 2, + }, + { + name: "new actions on different resources for same roles", + newEnforcerConfig: &Config{ + ManagedResources: []string{ResourceGroup}, + RolesMap: map[Role][]*Policy{ + RoleAdmin: {{ + Resource: ResourceGroup, + Action: ActionCreate, + }}, + }, + }, + expectErr: false, + numPolicies: 4, + numSubjects: 2, + numAdminActions: 3, + }, + { + name: "new actions on different resources for new roles", + newEnforcerConfig: &Config{ + ManagedResources: []string{ResourceGroup}, + RolesMap: map[Role][]*Policy{ + RoleProjectAdmin: {{ + Resource: ResourceGroup, + Action: ActionCreate, + }}, + }, + }, + expectErr: false, + numSubjects: 3, + numPolicies: 4, + numAdminActions: 2, + }, + { + name: "reset admin actions on same resource, collision", + newEnforcerConfig: &Config{ + ManagedResources: []string{ResourceWorkflow}, + RolesMap: map[Role][]*Policy{ + RoleAdmin: {}, // this should remove all admin actions from enforcer + }, + }, + expectErr: false, + numSubjects: 1, + numPolicies: 1, + numAdminActions: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e, c := testEnforcer(t) + defer c.Close() + + // initial import + err := syncRBACRoles(e, &Config{ + ManagedResources: []string{ResourceWorkflow, ResourceWorkflowRun}, + RolesMap: map[Role][]*Policy{ + RoleAdmin: {{ + Resource: ResourceWorkflow, + Action: ActionCreate, + }, { + Resource: ResourceWorkflow, + Action: ActionDelete, + }}, + RoleOrgMember: {{ + Resource: ResourceWorkflowRun, + Action: ActionList, + }}, + }, + }) + require.NoError(t, err) + + // sync with test case config + err = syncRBACRoles(e, tc.newEnforcerConfig) + if tc.expectErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + policies, err := e.GetPolicy() + assert.NoError(t, err) + assert.Len(t, policies, tc.numPolicies) + + adminCount := 0 + for _, r := range policies { + if r[0] == string(RoleAdmin) { + adminCount++ + } + } + assert.Equal(t, tc.numAdminActions, adminCount) + + subs, err := e.GetAllSubjects() + assert.NoError(t, err) + assert.Len(t, subs, tc.numSubjects) // We need to count the Viewer role + }) + } } func TestAddPoliciesDuplication(t *testing.T) { From 7856d2aff00dccb87923af78d4146c11df64322e Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Fri, 27 Jun 2025 16:01:51 +0200 Subject: [PATCH 6/7] lint Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/enforcer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controlplane/pkg/authz/enforcer.go b/app/controlplane/pkg/authz/enforcer.go index 40fcdad95..207f2fdbc 100644 --- a/app/controlplane/pkg/authz/enforcer.go +++ b/app/controlplane/pkg/authz/enforcer.go @@ -90,7 +90,7 @@ func (e *Enforcer) ClearPolicies(sub *SubjectAPIToken) error { return fmt.Errorf("failed to get policies: %w", err) } - if _, err := e.Enforcer.RemovePolicies(policies); err != nil { + if _, err := e.RemovePolicies(policies); err != nil { return fmt.Errorf("failed to remove policies: %w", err) } From 4e600b25659ab19417cc29b346d573fd72256469 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Fri, 27 Jun 2025 17:23:04 +0200 Subject: [PATCH 7/7] fix deletion in casbin Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/enforcer.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/controlplane/pkg/authz/enforcer.go b/app/controlplane/pkg/authz/enforcer.go index 207f2fdbc..2301c90fe 100644 --- a/app/controlplane/pkg/authz/enforcer.go +++ b/app/controlplane/pkg/authz/enforcer.go @@ -205,11 +205,13 @@ func doSync(e *Enforcer, c *Config) error { return fmt.Errorf("failed to get policies: %w", err) } - for _, gotPolicies := range policies { - role := gotPolicies[0] - resource := gotPolicies[1] - action := gotPolicies[2] - policy := &Policy{Resource: resource, Action: action} + // clone policies, as delete operations in CasBin alters the "policies" slice + clonedPolicies := slices.Clone(policies) + + for _, p := range clonedPolicies { + role := p[0] + resource := p[1] + action := p[2] // if it's not a managed resource, skip deletion if !slices.Contains(conf.ManagedResources, resource) { @@ -219,7 +221,7 @@ func doSync(e *Enforcer, c *Config) error { wantPolicies, ok := conf.RolesMap[Role(role)] // if the role does not exist in the map, we can delete the policy if !ok { - _, err := e.RemovePolicy(role, policy.Resource, policy.Action) + _, err := e.RemovePolicy(role, resource, action) if err != nil { return fmt.Errorf("failed to remove policy: %w", err) } @@ -229,7 +231,7 @@ func doSync(e *Enforcer, c *Config) error { // We have the role in the map, so we now compare the policies found := false for _, p := range wantPolicies { - if p.Resource == policy.Resource && p.Action == policy.Action { + if p.Resource == resource && p.Action == action { found = true break } @@ -237,7 +239,7 @@ func doSync(e *Enforcer, c *Config) error { // If the policy is not in the map, we remove it if !found { - _, err := e.RemovePolicy(gotPolicies) + _, err := e.RemovePolicy(p) if err != nil { return fmt.Errorf("failed to remove policy: %w", err) }