@@ -33,17 +33,40 @@ import (
3333 "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
3434)
3535
36+ type Role string
37+
3638const (
3739 // Actions
3840 ActionRead = "read"
3941 ActionList = "list"
42+ ActionCreate = "create"
4043 ActionUpdate = "update"
4144 ActionDelete = "delete"
4245
4346 // Resources
44- ResourceWorkflowContract = "workflow_contract"
45- ResourceCASArtifact = "cas_artifact"
46- ResourceReferrer = "referrer"
47+ ResourceWorkflowContract = "workflow_contract"
48+ ResourceCASArtifact = "cas_artifact"
49+ ResourceCASBackend = "cas_backend"
50+ ResourceReferrer = "referrer"
51+ ResourceAvailableIntegration = "integration_available"
52+ ResourceRegisteredIntegration = "integration_registered"
53+ ResourceAttachedIntegration = "integration_attached"
54+ ResourceOrgMetric = "metrics_org"
55+ ResourceRobotAccount = "robot_account"
56+ ResourceWorkflowRun = "workflow_run"
57+ ResourceWorkflow = "workflow"
58+ UserMembership = "membership_user"
59+ Organization = "organization"
60+
61+ // We have for now three roles, viewer, admin and owner
62+ // The owner of an org
63+ // The administrator of an org
64+ // The read only viewer of an org
65+ // These roles are hierarchical
66+ // This means that the Owner role inherits all the policies from Admin so from the Viewer Role
67+ RoleOwner Role = "role:org:owner"
68+ RoleAdmin Role = "role:org:admin"
69+ RoleViewer Role = "role:org:viewer"
4770)
4871
4972// resource, action tuple
@@ -53,13 +76,119 @@ type Policy struct {
5376}
5477
5578var (
79+ // Referrer
80+ PolicyReferrerRead = & Policy {ResourceReferrer , ActionRead }
81+ // Artifact
82+ PolicyArtifactDownload = & Policy {ResourceCASArtifact , ActionRead }
83+ PolicyArtifactUpload = & Policy {ResourceCASArtifact , ActionCreate }
84+ // CAS backend
85+ PolicyCASBackendList = & Policy {ResourceCASBackend , ActionList }
86+ // Available integrations
87+ PolicyAvailableIntegrationList = & Policy {ResourceAvailableIntegration , ActionList }
88+ PolicyAvailableIntegrationRead = & Policy {ResourceAvailableIntegration , ActionRead }
89+ // Registered integrations
90+ PolicyRegisteredIntegrationList = & Policy {ResourceRegisteredIntegration , ActionList }
91+ // Attached integrations
92+ PolicyAttachedIntegrationList = & Policy {ResourceAttachedIntegration , ActionList }
93+ // Org Metrics
94+ PolicyOrgMetricsRead = & Policy {ResourceOrgMetric , ActionList }
95+ // Robot Account
96+ PolicyRobotAccountList = & Policy {ResourceRobotAccount , ActionList }
97+ // Workflow Contract
5698 PolicyWorkflowContractList = & Policy {ResourceWorkflowContract , ActionList }
5799 PolicyWorkflowContractRead = & Policy {ResourceWorkflowContract , ActionRead }
58100 PolicyWorkflowContractUpdate = & Policy {ResourceWorkflowContract , ActionUpdate }
59- PolicyArtifactDownload = & Policy {ResourceCASArtifact , ActionRead }
60- PolicyReferrerRead = & Policy {ResourceReferrer , ActionRead }
101+ // WorkflowRun
102+ PolicyWorkflowRunList = & Policy {ResourceWorkflowRun , ActionList }
103+ PolicyWorkflowRunRead = & Policy {ResourceWorkflowRun , ActionRead }
104+ // Workflow
105+ PolicyWorkflowList = & Policy {ResourceWorkflow , ActionList }
106+ // User Membership
107+ PolicyOrganizationRead = & Policy {Organization , ActionRead }
61108)
62109
110+ // List of policies for each role
111+ // NOTE: roles are hierarchical, this means that the Admin Role can inherit all the policies from the Viewer Role
112+ // so we do not need to add them as well.
113+ var rolesMap = map [Role ][]* Policy {
114+ RoleViewer : {
115+ // Referrer
116+ PolicyReferrerRead ,
117+ // Artifact
118+ PolicyArtifactDownload ,
119+ // CAS backend
120+ PolicyCASBackendList ,
121+ // Available integrations
122+ PolicyAvailableIntegrationList ,
123+ PolicyAvailableIntegrationRead ,
124+ // Registered integrations
125+ PolicyRegisteredIntegrationList ,
126+ // Attached integrations
127+ PolicyAttachedIntegrationList ,
128+ // Metrics
129+ PolicyOrgMetricsRead ,
130+ // Robot Account
131+ PolicyRobotAccountList ,
132+ // Workflow Contract
133+ PolicyWorkflowContractList ,
134+ PolicyWorkflowContractRead ,
135+ // WorkflowRun
136+ PolicyWorkflowRunList ,
137+ PolicyWorkflowRunRead ,
138+ // Workflow
139+ PolicyWorkflowList ,
140+ // Organization
141+ PolicyOrganizationRead ,
142+ },
143+ RoleAdmin : {
144+ // We do a manual check in the artifact upload endpoint
145+ // so we need the actual policy in place skipping it is not enough
146+ PolicyArtifactUpload ,
147+ // + all the policies from the viewer role inherited automatically
148+ },
149+ }
150+
151+ // ServerOperationsMap is a map of server operations to the ResourceAction tuples that are
152+ // required to perform the operation
153+ // If it contains more than one policy, all of them need to be true
154+ var ServerOperationsMap = map [string ][]* Policy {
155+ // Discover endpoint
156+ "/controlplane.v1.ReferrerService/DiscoverPrivate" : {PolicyReferrerRead },
157+ // Download/Uploading artifacts
158+ // There are no policies for the download endpoint, we do a manual check in the service layer
159+ // to differentiate between upload and download requests
160+ "/controlplane.v1.CASCredentialsService/Get" : {},
161+ // CAS Backend listing
162+ "/controlplane.v1.CASBackendService/List" : {PolicyCASBackendList },
163+ // Available integrations
164+ "/controlplane.v1.IntegrationsService/ListAvailable" : {PolicyAvailableIntegrationList , PolicyAvailableIntegrationRead },
165+ // Registered integrations
166+ "/controlplane.v1.IntegrationsService/ListRegistrations" : {PolicyRegisteredIntegrationList },
167+ // Attached integrations
168+ "/controlplane.v1.IntegrationsService/ListAttachments" : {PolicyAttachedIntegrationList },
169+ // Metrics
170+ "/controlplane.v1.OrgMetricsService/.*" : {PolicyOrgMetricsRead },
171+ // Robot Account
172+ "/controlplane.v1.RobotAccountService/List" : {PolicyRobotAccountList },
173+ // Workflows
174+ "/controlplane.v1.WorkflowService/List" : {PolicyWorkflowList },
175+ // WorkflowRun
176+ "/controlplane.v1.WorkflowRunService/List" : {PolicyWorkflowRunList },
177+ "/controlplane.v1.WorkflowRunService/View" : {PolicyWorkflowRunRead },
178+ // Workflow Contracts
179+ "/controlplane.v1.WorkflowContractService/List" : {PolicyWorkflowContractList },
180+ "/controlplane.v1.WorkflowContractService/Describe" : {PolicyWorkflowContractRead },
181+ "/controlplane.v1.WorkflowContractService/Update" : {PolicyWorkflowContractUpdate },
182+ // Get current information about an organization
183+ "/controlplane.v1.ContextService/Current" : {PolicyOrganizationRead , PolicyCASBackendList },
184+ // Listing, create or selecting an organization does not have any required permissions,
185+ // since all the permissions here are in the context of an organization
186+ "/controlplane.v1.OrganizationService/Create" : {},
187+ "/controlplane.v1.OrganizationService/SetCurrentMembership" : {},
188+ // NOTE: this is about listing my own memberships, not about listing all the memberships in the organization
189+ "/controlplane.v1.OrganizationService/ListMemberships" : {},
190+ }
191+
63192type SubjectAPIToken struct {
64193 ID string
65194}
@@ -96,6 +225,10 @@ func (e *Enforcer) AddPolicies(sub *SubjectAPIToken, policies ...*Policy) error
96225 return nil
97226}
98227
228+ func (e * Enforcer ) Enforce (sub string , p * Policy ) (bool , error ) {
229+ return e .Enforcer .Enforce (sub , p .Resource , p .Action )
230+ }
231+
99232// Remove all the policies for the given subject
100233func (e * Enforcer ) ClearPolicies (sub * SubjectAPIToken ) error {
101234 if sub == nil {
@@ -176,5 +309,94 @@ func newEnforcer(a persist.Adapter) (*Enforcer, error) {
176309 return nil , fmt .Errorf ("failed to create enforcer: %w" , err )
177310 }
178311
312+ // Initialize the enforcer with the roles map
313+ if err := syncRBACRoles (& Enforcer {enforcer }); err != nil {
314+ return nil , fmt .Errorf ("failed to sync roles: %w" , err )
315+ }
316+
179317 return & Enforcer {enforcer }, nil
180318}
319+
320+ // Load the roles map into the enforcer
321+ // This is done by adding all the policies defined in the roles map
322+ // and removing all the policies that are not
323+ func syncRBACRoles (e * Enforcer ) error {
324+ return doSync (e , rolesMap )
325+ }
326+
327+ func doSync (e * Enforcer , rolesMap map [Role ][]* Policy ) error {
328+ // Add all the defined policies if they don't exist
329+ for role , policies := range rolesMap {
330+ for _ , p := range policies {
331+ // Add policies one by one to skip existing ones.
332+ // This is because the bulk method AddPoliciesEx does not work well with the ent adapter
333+ casbinPolicy := []string {string (role ), p .Resource , p .Action }
334+ _ , err := e .AddPolicy (casbinPolicy )
335+ if err != nil {
336+ return fmt .Errorf ("failed to add policy: %w" , err )
337+ }
338+ }
339+ }
340+
341+ // Delete all the policies that are not in the roles map
342+ // 1 - load the policies from the enforcer DB
343+ for _ , gotPolicies := range e .GetPolicy () {
344+ role := gotPolicies [0 ]
345+ policy := & Policy {Resource : gotPolicies [1 ], Action : gotPolicies [2 ]}
346+
347+ wantPolicies , ok := rolesMap [Role (role )]
348+ // if the role does not exist in the map, we can delete the policy
349+ if ! ok {
350+ _ , err := e .RemovePolicy (role , policy .Resource , policy .Action )
351+ if err != nil {
352+ return fmt .Errorf ("failed to remove policy: %w" , err )
353+ }
354+ continue
355+ }
356+
357+ // We have the role in the map, so we now compare the policies
358+ found := false
359+ for _ , p := range wantPolicies {
360+ if p .Resource == policy .Resource && p .Action == policy .Action {
361+ found = true
362+ break
363+ }
364+ }
365+
366+ // If the policy is not in the map, we remove it
367+ if ! found {
368+ _ , err := e .RemovePolicy (gotPolicies )
369+ if err != nil {
370+ return fmt .Errorf ("failed to remove policy: %w" , err )
371+ }
372+ }
373+ }
374+
375+ // To finish we make sure that the admin role inherit all the policies from the viewer role
376+ _ , err := e .AddGroupingPolicy (string (RoleAdmin ), string (RoleViewer ))
377+ if err != nil {
378+ return fmt .Errorf ("failed to add grouping policy: %w" , err )
379+ }
380+
381+ // same for the owner
382+ _ , err = e .AddGroupingPolicy (string (RoleOwner ), string (RoleAdmin ))
383+ if err != nil {
384+ return fmt .Errorf ("failed to add grouping policy: %w" , err )
385+ }
386+
387+ return nil
388+ }
389+
390+ // Implements https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues
391+ // so they can be added to the database schema
392+ func (Role ) Values () (roles []string ) {
393+ for _ , s := range []Role {
394+ RoleOwner ,
395+ RoleAdmin ,
396+ RoleViewer ,
397+ } {
398+ roles = append (roles , string (s ))
399+ }
400+
401+ return
402+ }
0 commit comments