From 1edf06a8b7759522cdb2804229038063e15617a0 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 17 Jun 2025 16:38:44 +0200 Subject: [PATCH 01/48] create org member role and add project permissions Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 5236bff5d..1d612170b 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -35,6 +35,7 @@ type Role string const ( // Actions + ActionRead = "read" ActionList = "list" ActionCreate = "create" @@ -42,6 +43,7 @@ const ( ActionDelete = "delete" // Resources + ResourceWorkflowContract = "workflow_contract" ResourceCASArtifact = "cas_artifact" ResourceCASBackend = "cas_backend" @@ -55,6 +57,7 @@ const ( ResourceWorkflow = "workflow" UserMembership = "membership_user" Organization = "organization" + ResourceProject = "project" // We have for now three roles, viewer, admin and owner // The owner of an org @@ -65,6 +68,14 @@ const ( RoleOwner Role = "role:org:owner" RoleAdmin Role = "role:org:admin" RoleViewer Role = "role:org:viewer" + + // New RBAC roles + + // RoleOrgMember is the role that users get by default when they join an organization. + // They cannot see projects until they are invited. However, they are able to create their own projects, + // so Casbin rules (role, resource-type, action) are NOT enough to check for permission, since we must check for ownership as well. + // That last check will be done at the service level. + RoleOrgMember Role = "role:org:member" ) // resource, action tuple @@ -111,6 +122,14 @@ var ( // User Membership PolicyOrganizationRead = &Policy{Organization, ActionRead} + + // Projects + + PolicyProjectList = &Policy{ResourceProject, ActionList} + PolicyProjectRead = &Policy{ResourceProject, ActionRead} + PolicyProjectCreate = &Policy{ResourceProject, ActionCreate} + PolicyProjectUpdate = &Policy{ResourceProject, ActionUpdate} + PolicyProjectDelete = &Policy{ResourceProject, ActionDelete} ) // List of policies for each role @@ -153,6 +172,13 @@ var rolesMap = map[Role][]*Policy{ PolicyArtifactUpload, // + all the policies from the viewer role inherited automatically }, + RoleOrgMember: { + PolicyProjectList, + PolicyProjectRead, + PolicyProjectCreate, + PolicyProjectUpdate, + PolicyProjectDelete, + }, } // ServerOperationsMap is a map of server operations to the ResourceAction tuples that are From afc90014fb8e93dd8581966155d6c4dc7b0c2de5 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 18 Jun 2025 11:04:46 +0200 Subject: [PATCH 02/48] wip Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 6 +++ app/controlplane/pkg/biz/membership.go | 14 +++++ .../pkg/data/ent/membership_create.go | 22 +++++--- .../pkg/data/ent/membership_update.go | 44 ++++++++++----- .../ent/migrate/migrations/20250617182716.sql | 6 +++ .../pkg/data/ent/migrate/migrations/atlas.sum | 3 +- .../pkg/data/ent/migrate/schema.go | 6 +-- .../pkg/data/ent/schema/membership.go | 6 +-- app/controlplane/pkg/data/membership.go | 53 ++++++++++++++----- 9 files changed, 123 insertions(+), 37 deletions(-) create mode 100644 app/controlplane/pkg/data/ent/migrate/migrations/20250617182716.sql diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 1d612170b..93f36b4e8 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -441,6 +441,12 @@ func doSync(e *Enforcer, rolesMap map[Role][]*Policy) error { return fmt.Errorf("failed to add grouping policy: %w", err) } + // org member inherits permissions, but in their own resources + _, err = e.AddGroupingPolicy(string(RoleOrgMember), string(RoleViewer)) + if err != nil { + return fmt.Errorf("failed to add grouping policy: %w", err) + } + return nil } diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index 0127705b1..38efbbb49 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -51,6 +51,11 @@ type MembershipRepo interface { SetRole(ctx context.Context, ID uuid.UUID, role authz.Role) (*Membership, error) Create(ctx context.Context, orgID, userID uuid.UUID, current bool, role authz.Role) (*Membership, error) Delete(ctx context.Context, ID uuid.UUID) error + + // RBAC methods + + ListAllByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) + GetMembershipByUserAndResource(ctx context.Context, userID uuid.UUID, resourceType ResourceType, resourceID uuid.UUID) (*Membership, error) } type MembershipUseCase struct { @@ -247,6 +252,15 @@ func (uc *MembershipUseCase) ByOrg(ctx context.Context, orgID string) ([]*Member return uc.repo.FindByOrg(ctx, orgUUID) } +// ListAllMembershipsForUser retrieves all membership records by resource type +func (uc *MembershipUseCase) ListAllMembershipsForUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) { + return uc.repo.ListAllByUser(ctx, userID) +} + +func (uc *MembershipUseCase) GetMembershipForResource(ctx context.Context, userID uuid.UUID, resourceType ResourceType, resourceID uuid.UUID) (*Membership, error) { + return uc.repo.GetMembershipByUserAndResource(ctx, userID, resourceType, resourceID) +} + // SetCurrent sets the current membership for the user // and unsets the previous one func (uc *MembershipUseCase) SetCurrent(ctx context.Context, userID, membershipID string) (*Membership, error) { diff --git a/app/controlplane/pkg/data/ent/membership_create.go b/app/controlplane/pkg/data/ent/membership_create.go index 040f4dc05..f1fb1291b 100644 --- a/app/controlplane/pkg/data/ent/membership_create.go +++ b/app/controlplane/pkg/data/ent/membership_create.go @@ -152,6 +152,14 @@ func (mc *MembershipCreate) SetOrganizationID(id uuid.UUID) *MembershipCreate { return mc } +// SetNillableOrganizationID sets the "organization" edge to the Organization entity by ID if the given value is not nil. +func (mc *MembershipCreate) SetNillableOrganizationID(id *uuid.UUID) *MembershipCreate { + if id != nil { + mc = mc.SetOrganizationID(*id) + } + return mc +} + // SetOrganization sets the "organization" edge to the Organization entity. func (mc *MembershipCreate) SetOrganization(o *Organization) *MembershipCreate { return mc.SetOrganizationID(o.ID) @@ -163,6 +171,14 @@ func (mc *MembershipCreate) SetUserID(id uuid.UUID) *MembershipCreate { return mc } +// SetNillableUserID sets the "user" edge to the User entity by ID if the given value is not nil. +func (mc *MembershipCreate) SetNillableUserID(id *uuid.UUID) *MembershipCreate { + if id != nil { + mc = mc.SetUserID(*id) + } + return mc +} + // SetUser sets the "user" edge to the User entity. func (mc *MembershipCreate) SetUser(u *User) *MembershipCreate { return mc.SetUserID(u.ID) @@ -250,12 +266,6 @@ func (mc *MembershipCreate) check() error { return &ValidationError{Name: "resource_type", err: fmt.Errorf(`ent: validator failed for field "Membership.resource_type": %w`, err)} } } - if len(mc.mutation.OrganizationIDs()) == 0 { - return &ValidationError{Name: "organization", err: errors.New(`ent: missing required edge "Membership.organization"`)} - } - if len(mc.mutation.UserIDs()) == 0 { - return &ValidationError{Name: "user", err: errors.New(`ent: missing required edge "Membership.user"`)} - } return nil } diff --git a/app/controlplane/pkg/data/ent/membership_update.go b/app/controlplane/pkg/data/ent/membership_update.go index f2ed80018..3f3ec5a3d 100644 --- a/app/controlplane/pkg/data/ent/membership_update.go +++ b/app/controlplane/pkg/data/ent/membership_update.go @@ -162,6 +162,14 @@ func (mu *MembershipUpdate) SetOrganizationID(id uuid.UUID) *MembershipUpdate { return mu } +// SetNillableOrganizationID sets the "organization" edge to the Organization entity by ID if the given value is not nil. +func (mu *MembershipUpdate) SetNillableOrganizationID(id *uuid.UUID) *MembershipUpdate { + if id != nil { + mu = mu.SetOrganizationID(*id) + } + return mu +} + // SetOrganization sets the "organization" edge to the Organization entity. func (mu *MembershipUpdate) SetOrganization(o *Organization) *MembershipUpdate { return mu.SetOrganizationID(o.ID) @@ -173,6 +181,14 @@ func (mu *MembershipUpdate) SetUserID(id uuid.UUID) *MembershipUpdate { return mu } +// SetNillableUserID sets the "user" edge to the User entity by ID if the given value is not nil. +func (mu *MembershipUpdate) SetNillableUserID(id *uuid.UUID) *MembershipUpdate { + if id != nil { + mu = mu.SetUserID(*id) + } + return mu +} + // SetUser sets the "user" edge to the User entity. func (mu *MembershipUpdate) SetUser(u *User) *MembershipUpdate { return mu.SetUserID(u.ID) @@ -239,12 +255,6 @@ func (mu *MembershipUpdate) check() error { return &ValidationError{Name: "resource_type", err: fmt.Errorf(`ent: validator failed for field "Membership.resource_type": %w`, err)} } } - if mu.mutation.OrganizationCleared() && len(mu.mutation.OrganizationIDs()) > 0 { - return errors.New(`ent: clearing a required unique edge "Membership.organization"`) - } - if mu.mutation.UserCleared() && len(mu.mutation.UserIDs()) > 0 { - return errors.New(`ent: clearing a required unique edge "Membership.user"`) - } return nil } @@ -507,6 +517,14 @@ func (muo *MembershipUpdateOne) SetOrganizationID(id uuid.UUID) *MembershipUpdat return muo } +// SetNillableOrganizationID sets the "organization" edge to the Organization entity by ID if the given value is not nil. +func (muo *MembershipUpdateOne) SetNillableOrganizationID(id *uuid.UUID) *MembershipUpdateOne { + if id != nil { + muo = muo.SetOrganizationID(*id) + } + return muo +} + // SetOrganization sets the "organization" edge to the Organization entity. func (muo *MembershipUpdateOne) SetOrganization(o *Organization) *MembershipUpdateOne { return muo.SetOrganizationID(o.ID) @@ -518,6 +536,14 @@ func (muo *MembershipUpdateOne) SetUserID(id uuid.UUID) *MembershipUpdateOne { return muo } +// SetNillableUserID sets the "user" edge to the User entity by ID if the given value is not nil. +func (muo *MembershipUpdateOne) SetNillableUserID(id *uuid.UUID) *MembershipUpdateOne { + if id != nil { + muo = muo.SetUserID(*id) + } + return muo +} + // SetUser sets the "user" edge to the User entity. func (muo *MembershipUpdateOne) SetUser(u *User) *MembershipUpdateOne { return muo.SetUserID(u.ID) @@ -597,12 +623,6 @@ func (muo *MembershipUpdateOne) check() error { return &ValidationError{Name: "resource_type", err: fmt.Errorf(`ent: validator failed for field "Membership.resource_type": %w`, err)} } } - if muo.mutation.OrganizationCleared() && len(muo.mutation.OrganizationIDs()) > 0 { - return errors.New(`ent: clearing a required unique edge "Membership.organization"`) - } - if muo.mutation.UserCleared() && len(muo.mutation.UserIDs()) > 0 { - return errors.New(`ent: clearing a required unique edge "Membership.user"`) - } return nil } diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250617182716.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250617182716.sql new file mode 100644 index 000000000..930767264 --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250617182716.sql @@ -0,0 +1,6 @@ +-- Drop index "membership_organization_memberships_user_memberships" from table: "memberships" +DROP INDEX "membership_organization_memberships_user_memberships"; +-- Modify "memberships" table +ALTER TABLE "memberships" ALTER COLUMN "organization_memberships" DROP NOT NULL, ALTER COLUMN "user_memberships" DROP NOT NULL; +-- Create index "membership_organization_memberships_user_memberships" to table: "memberships" +CREATE INDEX "membership_organization_memberships_user_memberships" ON "memberships" ("organization_memberships", "user_memberships"); diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index b2b13b30d..3045a7375 100644 --- a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum +++ b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:HH2RjuVCRM/lxsR9wm7EzfcodxS5+iPLQEFr45F31pU= +h1:h/gZpSCHr10MO/31l3/0hc+a9Gq7JOAN1HBdLSYujZ4= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -87,3 +87,4 @@ h1:HH2RjuVCRM/lxsR9wm7EzfcodxS5+iPLQEFr45F31pU= 20250605095559.sql h1:4rArZutoX6LKVxjGx3pwdFWbWM12X0Vo+rg5nE2L1m8= 20250616182009.sql h1:xmLOXknF6GTJP6UgYj6nO6NY0qQ3xfpQ4Awx/KqkSTA= 20250616182058.sql h1:fg5r2AZPj/n9y+FeCRaieUrj0UdFzBooLg7xJsNz8P0= +20250617182716.sql h1:APJGiHfWf95qNV622cc367xde2kEn217BUH+za58vxc= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index 84e48732e..c9d4f1b8d 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -227,8 +227,8 @@ var ( {Name: "member_id", Type: field.TypeUUID, Nullable: true}, {Name: "resource_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"organization", "project"}}, {Name: "resource_id", Type: field.TypeUUID, Nullable: true}, - {Name: "organization_memberships", Type: field.TypeUUID}, - {Name: "user_memberships", Type: field.TypeUUID}, + {Name: "organization_memberships", Type: field.TypeUUID, Nullable: true}, + {Name: "user_memberships", Type: field.TypeUUID, Nullable: true}, } // MembershipsTable holds the schema information for the "memberships" table. MembershipsTable = &schema.Table{ @@ -252,7 +252,7 @@ var ( Indexes: []*schema.Index{ { Name: "membership_organization_memberships_user_memberships", - Unique: true, + Unique: false, Columns: []*schema.Column{MembershipsColumns[9], MembershipsColumns[10]}, }, { diff --git a/app/controlplane/pkg/data/ent/schema/membership.go b/app/controlplane/pkg/data/ent/schema/membership.go index 179a48092..4b6d1a738 100644 --- a/app/controlplane/pkg/data/ent/schema/membership.go +++ b/app/controlplane/pkg/data/ent/schema/membership.go @@ -63,15 +63,15 @@ func (Membership) Fields() []ent.Field { func (Membership) Edges() []ent.Edge { return []ent.Edge{ // Deprecated: use polymorphic membership instead - edge.From("organization", Organization.Type).Ref("memberships").Unique().Required(), + edge.From("organization", Organization.Type).Ref("memberships").Unique(), // Deprecated: use polymorphic membership instead - edge.From("user", User.Type).Ref("memberships").Unique().Required(), + edge.From("user", User.Type).Ref("memberships").Unique(), } } func (Membership) Indexes() []ent.Index { return []ent.Index{ - index.Edges("organization", "user").Unique(), + index.Edges("organization", "user"), index.Fields("membership_type", "member_id", "resource_type", "resource_id").Unique(), } } diff --git a/app/controlplane/pkg/data/membership.go b/app/controlplane/pkg/data/membership.go index bb9046914..3c06a9dd4 100644 --- a/app/controlplane/pkg/data/membership.go +++ b/app/controlplane/pkg/data/membership.go @@ -78,12 +78,7 @@ func (r *MembershipRepo) FindByUser(ctx context.Context, userID uuid.UUID) ([]*b return nil, err } - result := make([]*biz.Membership, 0, len(memberships)) - for _, m := range memberships { - result = append(result, entMembershipToBiz(m)) - } - - return result, nil + return entMembershipsToBiz(memberships), nil } // FindByOrg finds all memberships for a given organization @@ -97,12 +92,7 @@ func (r *MembershipRepo) FindByOrg(ctx context.Context, orgID uuid.UUID) ([]*biz return nil, err } - result := make([]*biz.Membership, 0, len(memberships)) - for _, m := range memberships { - result = append(result, entMembershipToBiz(m)) - } - - return result, nil + return entMembershipsToBiz(memberships), nil } // FindByOrgAndUser finds the membership for a given organization and user @@ -155,6 +145,36 @@ func (r *MembershipRepo) FindByIDInUser(ctx context.Context, userID, membershipI return entMembershipToBiz(m), nil } +func (r *MembershipRepo) ListAllByUser(ctx context.Context, userID uuid.UUID) ([]*biz.Membership, error) { + mm, err := r.data.DB.Membership.Query().Where( + membership.MembershipTypeEQ(biz.MembershipTypeUser), + membership.MemberID(userID), + ).All(ctx) + + if err != nil { + return nil, fmt.Errorf("failed to query memberships: %v", err) + } + + return entMembershipsToBiz(mm), nil +} + +func (r *MembershipRepo) GetMembershipByUserAndResource(ctx context.Context, userID uuid.UUID, resourceType biz.ResourceType, resourceID uuid.UUID) (*biz.Membership, error) { + m, err := r.data.DB.Membership.Query().Where( + membership.MembershipTypeEQ(biz.MembershipTypeUser), + membership.MemberID(userID), + membership.ResourceTypeEQ(resourceType), + membership.ResourceID(resourceID), + ).Only(ctx) + + if err != nil { + if ent.IsNotFound(err) { + return nil, biz.NewErrNotFound(fmt.Sprintf("resource %s not found", resourceID)) + } + return nil, fmt.Errorf("failed to query memberships: %v", err) + } + return entMembershipToBiz(m), nil +} + func (r *MembershipRepo) FindByIDInOrg(ctx context.Context, orgID, membershipID uuid.UUID) (*biz.Membership, error) { m, err := r.data.DB.Membership.Query().Where( membership.MembershipTypeEQ(biz.MembershipTypeUser), @@ -222,6 +242,15 @@ func (r *MembershipRepo) Delete(ctx context.Context, id uuid.UUID) error { return r.data.DB.Membership.DeleteOneID(id).Exec(ctx) } +func entMembershipsToBiz(memberships []*ent.Membership) []*biz.Membership { + result := make([]*biz.Membership, 0, len(memberships)) + for _, m := range memberships { + result = append(result, entMembershipToBiz(m)) + } + + return result +} + func entMembershipToBiz(m *ent.Membership) *biz.Membership { if m == nil { return nil From d7a127126010a9065d6eba43ff0c4bb33b18f212 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 18 Jun 2025 17:59:49 +0200 Subject: [PATCH 03/48] expose role through CLI Signed-off-by: Jose I. Paris --- app/cli/internal/action/membership_list.go | 6 ++++++ .../api/controlplane/v1/response_messages.pb.go | 9 +++++++-- .../api/controlplane/v1/response_messages.proto | 1 + .../gen/frontend/controlplane/v1/response_messages.ts | 6 ++++++ .../controlplane.v1.OrgInvitationItem.jsonschema.json | 3 ++- .../controlplane.v1.OrgInvitationItem.schema.json | 3 ++- ....v1.OrgInvitationServiceCreateRequest.jsonschema.json | 3 ++- ...lane.v1.OrgInvitationServiceCreateRequest.schema.json | 3 ++- .../controlplane.v1.OrgMembershipItem.jsonschema.json | 3 ++- .../controlplane.v1.OrgMembershipItem.schema.json | 3 ++- ...izationServiceUpdateMembershipRequest.jsonschema.json | 3 ++- ...rganizationServiceUpdateMembershipRequest.schema.json | 3 ++- app/controlplane/internal/service/user.go | 2 ++ .../internal/usercontext/currentuser_middleware.go | 4 ++-- app/controlplane/pkg/authz/authz.go | 7 +------ 15 files changed, 41 insertions(+), 18 deletions(-) diff --git a/app/cli/internal/action/membership_list.go b/app/cli/internal/action/membership_list.go index c07534767..7a9b207cd 100644 --- a/app/cli/internal/action/membership_list.go +++ b/app/cli/internal/action/membership_list.go @@ -117,6 +117,7 @@ const ( RoleAdmin Role = "admin" RoleOwner Role = "owner" RoleViewer Role = "viewer" + RoleMember Role = "member" ) type Roles []Role @@ -125,6 +126,7 @@ var AvailableRoles = Roles{ RoleAdmin, RoleOwner, RoleViewer, + RoleMember, } func (roles Roles) String() string { @@ -143,6 +145,8 @@ func pbRoleToString(role pb.MembershipRole) Role { return RoleViewer case pb.MembershipRole_MEMBERSHIP_ROLE_ORG_OWNER: return RoleOwner + case pb.MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER: + return RoleMember } return "" } @@ -155,6 +159,8 @@ func stringToPbRole(role Role) pb.MembershipRole { return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_VIEWER case RoleOwner: return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_OWNER + case RoleMember: + return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER } return pb.MembershipRole_MEMBERSHIP_ROLE_UNSPECIFIED } diff --git a/app/controlplane/api/controlplane/v1/response_messages.pb.go b/app/controlplane/api/controlplane/v1/response_messages.pb.go index f7de461fa..657014923 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.pb.go +++ b/app/controlplane/api/controlplane/v1/response_messages.pb.go @@ -104,6 +104,7 @@ const ( MembershipRole_MEMBERSHIP_ROLE_ORG_VIEWER MembershipRole = 1 MembershipRole_MEMBERSHIP_ROLE_ORG_ADMIN MembershipRole = 2 MembershipRole_MEMBERSHIP_ROLE_ORG_OWNER MembershipRole = 3 + MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER MembershipRole = 4 ) // Enum value maps for MembershipRole. @@ -113,12 +114,14 @@ var ( 1: "MEMBERSHIP_ROLE_ORG_VIEWER", 2: "MEMBERSHIP_ROLE_ORG_ADMIN", 3: "MEMBERSHIP_ROLE_ORG_OWNER", + 4: "MEMBERSHIP_ROLE_ORG_MEMBER", } MembershipRole_value = map[string]int32{ "MEMBERSHIP_ROLE_UNSPECIFIED": 0, "MEMBERSHIP_ROLE_ORG_VIEWER": 1, "MEMBERSHIP_ROLE_ORG_ADMIN": 2, "MEMBERSHIP_ROLE_ORG_OWNER": 3, + "MEMBERSHIP_ROLE_ORG_MEMBER": 4, } ) @@ -2817,7 +2820,7 @@ var file_controlplane_v1_response_messages_proto_rawDesc = []byte{ 0x03, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x45, 0x44, 0x10, 0x04, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, - 0x44, 0x10, 0x05, 0x2a, 0x8f, 0x01, 0x0a, 0x0e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, + 0x44, 0x10, 0x05, 0x2a, 0xaf, 0x01, 0x0a, 0x0e, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1e, 0x0a, 0x1a, 0x4d, 0x45, 0x4d, 0x42, 0x45, @@ -2826,7 +2829,9 @@ var file_controlplane_v1_response_messages_proto_rawDesc = []byte{ 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x41, 0x44, 0x4d, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x1d, 0x0a, 0x19, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x4f, 0x57, - 0x4e, 0x45, 0x52, 0x10, 0x03, 0x2a, 0x60, 0x0a, 0x0e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x4c, 0x69, + 0x4e, 0x45, 0x52, 0x10, 0x03, 0x12, 0x1e, 0x0a, 0x1a, 0x4d, 0x45, 0x4d, 0x42, 0x45, 0x52, 0x53, + 0x48, 0x49, 0x50, 0x5f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x4f, 0x52, 0x47, 0x5f, 0x4d, 0x45, 0x4d, + 0x42, 0x45, 0x52, 0x10, 0x04, 0x2a, 0x60, 0x0a, 0x0e, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x4c, 0x69, 0x73, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x4c, 0x4c, 0x4f, 0x57, 0x5f, 0x4c, 0x49, 0x53, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x26, 0x0a, 0x1c, 0x41, 0x4c, 0x4c, diff --git a/app/controlplane/api/controlplane/v1/response_messages.proto b/app/controlplane/api/controlplane/v1/response_messages.proto index e0d2e0202..93d179e58 100644 --- a/app/controlplane/api/controlplane/v1/response_messages.proto +++ b/app/controlplane/api/controlplane/v1/response_messages.proto @@ -241,6 +241,7 @@ enum MembershipRole { MEMBERSHIP_ROLE_ORG_VIEWER = 1; MEMBERSHIP_ROLE_ORG_ADMIN = 2; MEMBERSHIP_ROLE_ORG_OWNER = 3; + MEMBERSHIP_ROLE_ORG_MEMBER = 4; } message OrgItem { diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts index cd10606b1..075c5c27f 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts @@ -73,6 +73,7 @@ export enum MembershipRole { MEMBERSHIP_ROLE_ORG_VIEWER = 1, MEMBERSHIP_ROLE_ORG_ADMIN = 2, MEMBERSHIP_ROLE_ORG_OWNER = 3, + MEMBERSHIP_ROLE_ORG_MEMBER = 4, UNRECOGNIZED = -1, } @@ -90,6 +91,9 @@ export function membershipRoleFromJSON(object: any): MembershipRole { case 3: case "MEMBERSHIP_ROLE_ORG_OWNER": return MembershipRole.MEMBERSHIP_ROLE_ORG_OWNER; + case 4: + case "MEMBERSHIP_ROLE_ORG_MEMBER": + return MembershipRole.MEMBERSHIP_ROLE_ORG_MEMBER; case -1: case "UNRECOGNIZED": default: @@ -107,6 +111,8 @@ export function membershipRoleToJSON(object: MembershipRole): string { return "MEMBERSHIP_ROLE_ORG_ADMIN"; case MembershipRole.MEMBERSHIP_ROLE_ORG_OWNER: return "MEMBERSHIP_ROLE_ORG_OWNER"; + case MembershipRole.MEMBERSHIP_ROLE_ORG_MEMBER: + return "MEMBERSHIP_ROLE_ORG_MEMBER"; case MembershipRole.UNRECOGNIZED: default: return "UNRECOGNIZED"; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.jsonschema.json index 470ebeb29..667d4a813 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.jsonschema.json @@ -30,7 +30,8 @@ "MEMBERSHIP_ROLE_UNSPECIFIED", "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", - "MEMBERSHIP_ROLE_ORG_OWNER" + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.schema.json index ec32d7d50..686d42376 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationItem.schema.json @@ -30,7 +30,8 @@ "MEMBERSHIP_ROLE_UNSPECIFIED", "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", - "MEMBERSHIP_ROLE_ORG_OWNER" + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.jsonschema.json index 78790df0a..44e076d7e 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.jsonschema.json @@ -28,7 +28,8 @@ "MEMBERSHIP_ROLE_UNSPECIFIED", "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", - "MEMBERSHIP_ROLE_ORG_OWNER" + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.schema.json index f53a63e94..4dbed371a 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgInvitationServiceCreateRequest.schema.json @@ -28,7 +28,8 @@ "MEMBERSHIP_ROLE_UNSPECIFIED", "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", - "MEMBERSHIP_ROLE_ORG_OWNER" + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.jsonschema.json index b0a7bd3e2..10af855d4 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.jsonschema.json @@ -30,7 +30,8 @@ "MEMBERSHIP_ROLE_UNSPECIFIED", "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", - "MEMBERSHIP_ROLE_ORG_OWNER" + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.schema.json index d585a3896..dffa4d149 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrgMembershipItem.schema.json @@ -30,7 +30,8 @@ "MEMBERSHIP_ROLE_UNSPECIFIED", "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", - "MEMBERSHIP_ROLE_ORG_OWNER" + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.jsonschema.json index 93c284d8d..4777c055d 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.jsonschema.json @@ -20,7 +20,8 @@ "MEMBERSHIP_ROLE_UNSPECIFIED", "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", - "MEMBERSHIP_ROLE_ORG_OWNER" + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.schema.json index cc707ba76..6c30732d5 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.OrganizationServiceUpdateMembershipRequest.schema.json @@ -20,7 +20,8 @@ "MEMBERSHIP_ROLE_UNSPECIFIED", "MEMBERSHIP_ROLE_ORG_VIEWER", "MEMBERSHIP_ROLE_ORG_ADMIN", - "MEMBERSHIP_ROLE_ORG_OWNER" + "MEMBERSHIP_ROLE_ORG_OWNER", + "MEMBERSHIP_ROLE_ORG_MEMBER" ], "title": "Membership Role", "type": "string" diff --git a/app/controlplane/internal/service/user.go b/app/controlplane/internal/service/user.go index 2d55b17fd..ae12dc08e 100644 --- a/app/controlplane/internal/service/user.go +++ b/app/controlplane/internal/service/user.go @@ -122,6 +122,8 @@ func bizRoleToPb(r authz.Role) pb.MembershipRole { return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_ADMIN case authz.RoleViewer: return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_VIEWER + case authz.RoleOrgMember: + return pb.MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER default: return pb.MembershipRole_MEMBERSHIP_ROLE_UNSPECIFIED } diff --git a/app/controlplane/internal/usercontext/currentuser_middleware.go b/app/controlplane/internal/usercontext/currentuser_middleware.go index ba5fc58c2..69e2d2837 100644 --- a/app/controlplane/internal/usercontext/currentuser_middleware.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware.go @@ -32,7 +32,7 @@ import ( jwtMiddleware "github.com/go-kratos/kratos/v2/middleware/auth/jwt" ) -// Middleware that injects the current user + organization to the context +// WithCurrentUserMiddleware injects the current user + organization to the context func WithCurrentUserMiddleware(userUseCase biz.UserOrgFinder, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { @@ -85,7 +85,7 @@ func setCurrentUser(ctx context.Context, userUC biz.UserOrgFinder, userID string return entities.WithCurrentUser(ctx, &entities.User{Email: u.Email, ID: u.ID, FirstName: u.FirstName, LastName: u.LastName, CreatedAt: u.CreatedAt}), nil } -// Middleware that injects the current user + organization to the context during the attestation process +// WithAttestationContextFromUser injects the current user + organization to the context during the attestation process // it leverages the existing middlewares to set the current user and organization // but with a skipping behavior since that's the one required by the attMiddleware multi-selector func WithAttestationContextFromUser(userUC *biz.UserUseCase, logger *log.Helper) middleware.Middleware { diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 93f36b4e8..9c4162278 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -178,6 +178,7 @@ var rolesMap = map[Role][]*Policy{ PolicyProjectCreate, PolicyProjectUpdate, PolicyProjectDelete, + PolicyWorkflowCreate, }, } @@ -441,12 +442,6 @@ func doSync(e *Enforcer, rolesMap map[Role][]*Policy) error { return fmt.Errorf("failed to add grouping policy: %w", err) } - // org member inherits permissions, but in their own resources - _, err = e.AddGroupingPolicy(string(RoleOrgMember), string(RoleViewer)) - if err != nil { - return fmt.Errorf("failed to add grouping policy: %w", err) - } - return nil } From f583b8f3e38e7c408ae8991082d4836258c90388 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 18 Jun 2025 18:54:02 +0200 Subject: [PATCH 04/48] set memberships Signed-off-by: Jose I. Paris --- app/controlplane/cmd/wire_gen.go | 1 + app/controlplane/internal/server/grpc.go | 5 +- .../currentorganization_middleware.go | 35 ++++++++++++- .../usercontext/currentuser_middleware.go | 4 +- .../usercontext/entities/memberships.go | 47 ++++++++++++++++++ app/controlplane/pkg/authz/membership.go | 49 +++++++++++++++++++ app/controlplane/pkg/biz/membership.go | 41 ++-------------- app/controlplane/pkg/data/ent/membership.go | 9 ++-- .../pkg/data/ent/membership/membership.go | 5 +- .../pkg/data/ent/membership/where.go | 17 +++---- .../pkg/data/ent/membership_create.go | 33 ++++++------- .../pkg/data/ent/membership_update.go | 41 ++++++++-------- app/controlplane/pkg/data/ent/mutation.go | 24 ++++----- app/controlplane/pkg/data/ent/schema-viz.html | 2 +- .../pkg/data/ent/schema/membership.go | 5 +- app/controlplane/pkg/data/membership.go | 38 +++++++------- 16 files changed, 224 insertions(+), 132 deletions(-) create mode 100644 app/controlplane/internal/usercontext/entities/memberships.go create mode 100644 app/controlplane/pkg/authz/membership.go diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 66a897dd7..fb9f1d334 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -242,6 +242,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l APITokenUseCase: apiTokenUseCase, OrganizationUseCase: organizationUseCase, WorkflowUseCase: workflowUseCase, + MembershipUseCase: membershipUseCase, WorkflowSvc: workflowService, AuthSvc: authService, RobotAccountSvc: robotAccountService, diff --git a/app/controlplane/internal/server/grpc.go b/app/controlplane/internal/server/grpc.go index 75a0a62e3..fa3abb656 100644 --- a/app/controlplane/internal/server/grpc.go +++ b/app/controlplane/internal/server/grpc.go @@ -60,6 +60,7 @@ type Opts struct { APITokenUseCase *biz.APITokenUseCase OrganizationUseCase *biz.OrganizationUseCase WorkflowUseCase *biz.WorkflowUseCase + MembershipUseCase *biz.MembershipUseCase // Services WorkflowSvc *service.WorkflowService AuthSvc *service.AuthService @@ -188,7 +189,7 @@ func craftMiddleware(opts *Opts) []middleware.Middleware { usercontext.WithCurrentUserMiddleware(opts.UserUseCase, logHelper), selector.Server( // 2.c - Set its organization - usercontext.WithCurrentOrganizationMiddleware(opts.UserUseCase, logHelper), + usercontext.WithCurrentOrganizationMiddleware(opts.UserUseCase, opts.MembershipUseCase, logHelper), // 3 - Check user/token authorization authzMiddleware.WithAuthzMiddleware(opts.Enforcer, logHelper), ).Match(requireAllButOrganizationOperationsMatcher()).Build(), @@ -223,7 +224,7 @@ func craftMiddleware(opts *Opts) []middleware.Middleware { // 2.b - Set its API token and Robot Account as alternative to the user usercontext.WithAttestationContextFromAPIToken(opts.APITokenUseCase, opts.OrganizationUseCase, logHelper), // 2.c - Set Attestation context from user token - usercontext.WithAttestationContextFromUser(opts.UserUseCase, logHelper), + usercontext.WithAttestationContextFromUser(opts.UserUseCase, opts.MembershipUseCase, logHelper), // 2.d - Set its robot account from federated delegation usercontext.WithAttestationContextFromFederatedInfo(opts.OrganizationUseCase, logHelper), ).Match(requireRobotAccountMatcher()).Build(), diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware.go b/app/controlplane/internal/usercontext/currentorganization_middleware.go index 46793ce0b..9c2128042 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware.go @@ -22,12 +22,14 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/middleware" + "github.com/google/uuid" ) -func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, logger *log.Helper) middleware.Middleware { +func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, membershipUC *biz.MembershipUseCase, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { // Get the current user and return if not found, meaning we are probably coming from an API Token @@ -55,6 +57,15 @@ func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, logger *lo } } + orgRole := CurrentAuthzSubject(ctx) + if orgRole == string(authz.RoleOrgMember) { + // Org member enables the new RBAC behavior. Let's store all memberships in the context. + ctx, err = setCurrentMembershipsForUser(ctx, u, membershipUC, logger); + if err != nil { + return nil, fmt.Errorf("error setting current org membership: %w", err) + } + } + org := entities.CurrentOrg(ctx) if org == nil { return nil, errors.New("org not found") @@ -67,6 +78,28 @@ func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, logger *lo } } +func setCurrentMembershipsForUser(ctx context.Context, u *entities.User, membershipUC *biz.MembershipUseCase, logger *log.Helper) (context.Context, error) { + uid, err := uuid.Parse(u.ID) + if err != nil { + return nil, err + } + + mm, err := membershipUC.ListAllMembershipsForUser(ctx, uid) + if err != nil { + return nil, fmt.Errorf("error getting membership list: %w", err) + } + + resourceMemberships := make([]*entities.ResourceMembership, 0, len(mm)) + for _, m := range mm { + resourceMemberships = append(resourceMemberships, &entities.ResourceMembership{ + Role: m.Role, + ResourceType: m.ResourceType, + ResourceID: m.ResourceID, + }) + } + return entities.WithMembership(ctx, &entities.Membership{Resources: resourceMemberships}), nil +} + func setCurrentOrganizationFromHeader(ctx context.Context, user *entities.User, orgName string, userUC biz.UserOrgFinder) (context.Context, error) { membership, err := userUC.MembershipInOrg(ctx, user.ID, orgName) if err != nil { diff --git a/app/controlplane/internal/usercontext/currentuser_middleware.go b/app/controlplane/internal/usercontext/currentuser_middleware.go index 69e2d2837..8d82f4f95 100644 --- a/app/controlplane/internal/usercontext/currentuser_middleware.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware.go @@ -88,7 +88,7 @@ func setCurrentUser(ctx context.Context, userUC biz.UserOrgFinder, userID string // WithAttestationContextFromUser injects the current user + organization to the context during the attestation process // it leverages the existing middlewares to set the current user and organization // but with a skipping behavior since that's the one required by the attMiddleware multi-selector -func WithAttestationContextFromUser(userUC *biz.UserUseCase, logger *log.Helper) middleware.Middleware { +func WithAttestationContextFromUser(userUC *biz.UserUseCase, membershipUC *biz.MembershipUseCase, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { // If the token is not an user token, we don't need to do anything @@ -114,7 +114,7 @@ func WithAttestationContextFromUser(userUC *biz.UserUseCase, logger *log.Helper) // NOTE: we reuse the existing middlewares to set the current user and organization by wrapping the call // Now we can load the organization using the other middleware we have set return WithCurrentUserMiddleware(userUC, logger)(func(ctx context.Context, req any) (any, error) { - return WithCurrentOrganizationMiddleware(userUC, logger)(func(ctx context.Context, req any) (any, error) { + return WithCurrentOrganizationMiddleware(userUC, membershipUC, logger)(func(ctx context.Context, req any) (any, error) { org := entities.CurrentOrg(ctx) if org == nil { return nil, errors.New("organization not found") diff --git a/app/controlplane/internal/usercontext/entities/memberships.go b/app/controlplane/internal/usercontext/entities/memberships.go new file mode 100644 index 000000000..b8cd9cc9c --- /dev/null +++ b/app/controlplane/internal/usercontext/entities/memberships.go @@ -0,0 +1,47 @@ +// +// 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 entities + +import ( + "context" + + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" + "github.com/google/uuid" +) + +type Membership struct { + Resources []*ResourceMembership +} + +type ResourceMembership struct { + Role authz.Role + ResourceType authz.ResourceType + ResourceID uuid.UUID +} + +func WithMembership(ctx context.Context, m *Membership) context.Context { + return context.WithValue(ctx, membershipCtxKey{}, m) +} + +func CurrentMembership(ctx context.Context) *Membership { + res := ctx.Value(membershipCtxKey{}) + if res == nil { + return nil + } + return res.(*Membership) +} + +type membershipCtxKey struct{} diff --git a/app/controlplane/pkg/authz/membership.go b/app/controlplane/pkg/authz/membership.go new file mode 100644 index 000000000..095df69a8 --- /dev/null +++ b/app/controlplane/pkg/authz/membership.go @@ -0,0 +1,49 @@ +// +// 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 + +// Polymorphic membership + +type MembershipType string +type ResourceType string + +const ( + MembershipTypeUser MembershipType = "user" + MembershipTypeGroup MembershipType = "group" + + ResourceTypeOrganization ResourceType = "organization" + ResourceTypeProject ResourceType = "project" +) + +// Values implement https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues +func (MembershipType) Values() (values []string) { + values = append(values, + string(MembershipTypeUser), + string(MembershipTypeGroup), + ) + + return +} + +// Values implement https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues +func (ResourceType) Values() (values []string) { + values = append(values, + string(ResourceTypeOrganization), + string(ResourceTypeProject), + ) + + return +} diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index 38efbbb49..60783fa65 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -34,9 +34,9 @@ type Membership struct { User *User Role authz.Role // polymorphic membership - MembershipType MembershipType + MembershipType authz.MembershipType MemberID uuid.UUID - ResourceType ResourceType + ResourceType authz.ResourceType ResourceID uuid.UUID } @@ -55,7 +55,7 @@ type MembershipRepo interface { // RBAC methods ListAllByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) - GetMembershipByUserAndResource(ctx context.Context, userID uuid.UUID, resourceType ResourceType, resourceID uuid.UUID) (*Membership, error) + GetMembershipByUserAndResource(ctx context.Context, userID uuid.UUID, resourceType authz.ResourceType, resourceID uuid.UUID) (*Membership, error) } type MembershipUseCase struct { @@ -257,7 +257,7 @@ func (uc *MembershipUseCase) ListAllMembershipsForUser(ctx context.Context, user return uc.repo.ListAllByUser(ctx, userID) } -func (uc *MembershipUseCase) GetMembershipForResource(ctx context.Context, userID uuid.UUID, resourceType ResourceType, resourceID uuid.UUID) (*Membership, error) { +func (uc *MembershipUseCase) GetMembershipForResource(ctx context.Context, userID uuid.UUID, resourceType authz.ResourceType, resourceID uuid.UUID) (*Membership, error) { return uc.repo.GetMembershipByUserAndResource(ctx, userID, resourceType, resourceID) } @@ -320,36 +320,3 @@ func (uc *MembershipUseCase) FindByOrgNameAndUser(ctx context.Context, orgName, return m, nil } - -// Polymorphic membership - -type MembershipType string -type ResourceType string - -const ( - MembershipTypeUser MembershipType = "user" - MembershipTypeGroup MembershipType = "group" - - ResourceTypeOrganization ResourceType = "organization" - ResourceTypeProject ResourceType = "project" -) - -// Values implement https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues -func (MembershipType) Values() (values []string) { - values = append(values, - string(MembershipTypeUser), - string(MembershipTypeGroup), - ) - - return -} - -// Values implement https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues -func (ResourceType) Values() (values []string) { - values = append(values, - string(ResourceTypeOrganization), - string(ResourceTypeProject), - ) - - return -} diff --git a/app/controlplane/pkg/data/ent/membership.go b/app/controlplane/pkg/data/ent/membership.go index 797ce60c0..871176a99 100644 --- a/app/controlplane/pkg/data/ent/membership.go +++ b/app/controlplane/pkg/data/ent/membership.go @@ -10,7 +10,6 @@ import ( "entgo.io/ent" "entgo.io/ent/dialect/sql" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/membership" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/user" @@ -31,11 +30,11 @@ type Membership struct { // Role holds the value of the "role" field. Role authz.Role `json:"role,omitempty"` // MembershipType holds the value of the "membership_type" field. - MembershipType biz.MembershipType `json:"membership_type,omitempty"` + MembershipType authz.MembershipType `json:"membership_type,omitempty"` // MemberID holds the value of the "member_id" field. MemberID uuid.UUID `json:"member_id,omitempty"` // ResourceType holds the value of the "resource_type" field. - ResourceType biz.ResourceType `json:"resource_type,omitempty"` + ResourceType authz.ResourceType `json:"resource_type,omitempty"` // ResourceID holds the value of the "resource_id" field. ResourceID uuid.UUID `json:"resource_id,omitempty"` // Edges holds the relations/edges for other nodes in the graph. @@ -145,7 +144,7 @@ func (m *Membership) assignValues(columns []string, values []any) error { if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field membership_type", values[i]) } else if value.Valid { - m.MembershipType = biz.MembershipType(value.String) + m.MembershipType = authz.MembershipType(value.String) } case membership.FieldMemberID: if value, ok := values[i].(*uuid.UUID); !ok { @@ -157,7 +156,7 @@ func (m *Membership) assignValues(columns []string, values []any) error { if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field resource_type", values[i]) } else if value.Valid { - m.ResourceType = biz.ResourceType(value.String) + m.ResourceType = authz.ResourceType(value.String) } case membership.FieldResourceID: if value, ok := values[i].(*uuid.UUID); !ok { diff --git a/app/controlplane/pkg/data/ent/membership/membership.go b/app/controlplane/pkg/data/ent/membership/membership.go index b5a0e56c7..0c32936fb 100644 --- a/app/controlplane/pkg/data/ent/membership/membership.go +++ b/app/controlplane/pkg/data/ent/membership/membership.go @@ -9,7 +9,6 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/google/uuid" ) @@ -113,7 +112,7 @@ func RoleValidator(r authz.Role) error { } // MembershipTypeValidator is a validator for the "membership_type" field enum values. It is called by the builders before save. -func MembershipTypeValidator(mt biz.MembershipType) error { +func MembershipTypeValidator(mt authz.MembershipType) error { switch mt { case "user", "group": return nil @@ -123,7 +122,7 @@ func MembershipTypeValidator(mt biz.MembershipType) error { } // ResourceTypeValidator is a validator for the "resource_type" field enum values. It is called by the builders before save. -func ResourceTypeValidator(rt biz.ResourceType) error { +func ResourceTypeValidator(rt authz.ResourceType) error { switch rt { case "organization", "project": return nil diff --git a/app/controlplane/pkg/data/ent/membership/where.go b/app/controlplane/pkg/data/ent/membership/where.go index b8dc35377..4fc5011da 100644 --- a/app/controlplane/pkg/data/ent/membership/where.go +++ b/app/controlplane/pkg/data/ent/membership/where.go @@ -8,7 +8,6 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate" "github.com/google/uuid" ) @@ -204,19 +203,19 @@ func RoleNotIn(vs ...authz.Role) predicate.Membership { } // MembershipTypeEQ applies the EQ predicate on the "membership_type" field. -func MembershipTypeEQ(v biz.MembershipType) predicate.Membership { +func MembershipTypeEQ(v authz.MembershipType) predicate.Membership { vc := v return predicate.Membership(sql.FieldEQ(FieldMembershipType, vc)) } // MembershipTypeNEQ applies the NEQ predicate on the "membership_type" field. -func MembershipTypeNEQ(v biz.MembershipType) predicate.Membership { +func MembershipTypeNEQ(v authz.MembershipType) predicate.Membership { vc := v return predicate.Membership(sql.FieldNEQ(FieldMembershipType, vc)) } // MembershipTypeIn applies the In predicate on the "membership_type" field. -func MembershipTypeIn(vs ...biz.MembershipType) predicate.Membership { +func MembershipTypeIn(vs ...authz.MembershipType) predicate.Membership { v := make([]any, len(vs)) for i := range v { v[i] = vs[i] @@ -225,7 +224,7 @@ func MembershipTypeIn(vs ...biz.MembershipType) predicate.Membership { } // MembershipTypeNotIn applies the NotIn predicate on the "membership_type" field. -func MembershipTypeNotIn(vs ...biz.MembershipType) predicate.Membership { +func MembershipTypeNotIn(vs ...authz.MembershipType) predicate.Membership { v := make([]any, len(vs)) for i := range v { v[i] = vs[i] @@ -294,19 +293,19 @@ func MemberIDNotNil() predicate.Membership { } // ResourceTypeEQ applies the EQ predicate on the "resource_type" field. -func ResourceTypeEQ(v biz.ResourceType) predicate.Membership { +func ResourceTypeEQ(v authz.ResourceType) predicate.Membership { vc := v return predicate.Membership(sql.FieldEQ(FieldResourceType, vc)) } // ResourceTypeNEQ applies the NEQ predicate on the "resource_type" field. -func ResourceTypeNEQ(v biz.ResourceType) predicate.Membership { +func ResourceTypeNEQ(v authz.ResourceType) predicate.Membership { vc := v return predicate.Membership(sql.FieldNEQ(FieldResourceType, vc)) } // ResourceTypeIn applies the In predicate on the "resource_type" field. -func ResourceTypeIn(vs ...biz.ResourceType) predicate.Membership { +func ResourceTypeIn(vs ...authz.ResourceType) predicate.Membership { v := make([]any, len(vs)) for i := range v { v[i] = vs[i] @@ -315,7 +314,7 @@ func ResourceTypeIn(vs ...biz.ResourceType) predicate.Membership { } // ResourceTypeNotIn applies the NotIn predicate on the "resource_type" field. -func ResourceTypeNotIn(vs ...biz.ResourceType) predicate.Membership { +func ResourceTypeNotIn(vs ...authz.ResourceType) predicate.Membership { v := make([]any, len(vs)) for i := range v { v[i] = vs[i] diff --git a/app/controlplane/pkg/data/ent/membership_create.go b/app/controlplane/pkg/data/ent/membership_create.go index f1fb1291b..2724a655a 100644 --- a/app/controlplane/pkg/data/ent/membership_create.go +++ b/app/controlplane/pkg/data/ent/membership_create.go @@ -13,7 +13,6 @@ import ( "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/membership" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/user" @@ -77,15 +76,15 @@ func (mc *MembershipCreate) SetRole(a authz.Role) *MembershipCreate { } // SetMembershipType sets the "membership_type" field. -func (mc *MembershipCreate) SetMembershipType(bt biz.MembershipType) *MembershipCreate { - mc.mutation.SetMembershipType(bt) +func (mc *MembershipCreate) SetMembershipType(at authz.MembershipType) *MembershipCreate { + mc.mutation.SetMembershipType(at) return mc } // SetNillableMembershipType sets the "membership_type" field if the given value is not nil. -func (mc *MembershipCreate) SetNillableMembershipType(bt *biz.MembershipType) *MembershipCreate { - if bt != nil { - mc.SetMembershipType(*bt) +func (mc *MembershipCreate) SetNillableMembershipType(at *authz.MembershipType) *MembershipCreate { + if at != nil { + mc.SetMembershipType(*at) } return mc } @@ -105,15 +104,15 @@ func (mc *MembershipCreate) SetNillableMemberID(u *uuid.UUID) *MembershipCreate } // SetResourceType sets the "resource_type" field. -func (mc *MembershipCreate) SetResourceType(bt biz.ResourceType) *MembershipCreate { - mc.mutation.SetResourceType(bt) +func (mc *MembershipCreate) SetResourceType(at authz.ResourceType) *MembershipCreate { + mc.mutation.SetResourceType(at) return mc } // SetNillableResourceType sets the "resource_type" field if the given value is not nil. -func (mc *MembershipCreate) SetNillableResourceType(bt *biz.ResourceType) *MembershipCreate { - if bt != nil { - mc.SetResourceType(*bt) +func (mc *MembershipCreate) SetNillableResourceType(at *authz.ResourceType) *MembershipCreate { + if at != nil { + mc.SetResourceType(*at) } return mc } @@ -457,7 +456,7 @@ func (u *MembershipUpsert) UpdateRole() *MembershipUpsert { } // SetMembershipType sets the "membership_type" field. -func (u *MembershipUpsert) SetMembershipType(v biz.MembershipType) *MembershipUpsert { +func (u *MembershipUpsert) SetMembershipType(v authz.MembershipType) *MembershipUpsert { u.Set(membership.FieldMembershipType, v) return u } @@ -493,7 +492,7 @@ func (u *MembershipUpsert) ClearMemberID() *MembershipUpsert { } // SetResourceType sets the "resource_type" field. -func (u *MembershipUpsert) SetResourceType(v biz.ResourceType) *MembershipUpsert { +func (u *MembershipUpsert) SetResourceType(v authz.ResourceType) *MembershipUpsert { u.Set(membership.FieldResourceType, v) return u } @@ -622,7 +621,7 @@ func (u *MembershipUpsertOne) UpdateRole() *MembershipUpsertOne { } // SetMembershipType sets the "membership_type" field. -func (u *MembershipUpsertOne) SetMembershipType(v biz.MembershipType) *MembershipUpsertOne { +func (u *MembershipUpsertOne) SetMembershipType(v authz.MembershipType) *MembershipUpsertOne { return u.Update(func(s *MembershipUpsert) { s.SetMembershipType(v) }) @@ -664,7 +663,7 @@ func (u *MembershipUpsertOne) ClearMemberID() *MembershipUpsertOne { } // SetResourceType sets the "resource_type" field. -func (u *MembershipUpsertOne) SetResourceType(v biz.ResourceType) *MembershipUpsertOne { +func (u *MembershipUpsertOne) SetResourceType(v authz.ResourceType) *MembershipUpsertOne { return u.Update(func(s *MembershipUpsert) { s.SetResourceType(v) }) @@ -966,7 +965,7 @@ func (u *MembershipUpsertBulk) UpdateRole() *MembershipUpsertBulk { } // SetMembershipType sets the "membership_type" field. -func (u *MembershipUpsertBulk) SetMembershipType(v biz.MembershipType) *MembershipUpsertBulk { +func (u *MembershipUpsertBulk) SetMembershipType(v authz.MembershipType) *MembershipUpsertBulk { return u.Update(func(s *MembershipUpsert) { s.SetMembershipType(v) }) @@ -1008,7 +1007,7 @@ func (u *MembershipUpsertBulk) ClearMemberID() *MembershipUpsertBulk { } // SetResourceType sets the "resource_type" field. -func (u *MembershipUpsertBulk) SetResourceType(v biz.ResourceType) *MembershipUpsertBulk { +func (u *MembershipUpsertBulk) SetResourceType(v authz.ResourceType) *MembershipUpsertBulk { return u.Update(func(s *MembershipUpsert) { s.SetResourceType(v) }) diff --git a/app/controlplane/pkg/data/ent/membership_update.go b/app/controlplane/pkg/data/ent/membership_update.go index 3f3ec5a3d..b0afedb85 100644 --- a/app/controlplane/pkg/data/ent/membership_update.go +++ b/app/controlplane/pkg/data/ent/membership_update.go @@ -12,7 +12,6 @@ import ( "entgo.io/ent/dialect/sql/sqlgraph" "entgo.io/ent/schema/field" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/membership" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate" @@ -77,15 +76,15 @@ func (mu *MembershipUpdate) SetNillableRole(a *authz.Role) *MembershipUpdate { } // SetMembershipType sets the "membership_type" field. -func (mu *MembershipUpdate) SetMembershipType(bt biz.MembershipType) *MembershipUpdate { - mu.mutation.SetMembershipType(bt) +func (mu *MembershipUpdate) SetMembershipType(at authz.MembershipType) *MembershipUpdate { + mu.mutation.SetMembershipType(at) return mu } // SetNillableMembershipType sets the "membership_type" field if the given value is not nil. -func (mu *MembershipUpdate) SetNillableMembershipType(bt *biz.MembershipType) *MembershipUpdate { - if bt != nil { - mu.SetMembershipType(*bt) +func (mu *MembershipUpdate) SetNillableMembershipType(at *authz.MembershipType) *MembershipUpdate { + if at != nil { + mu.SetMembershipType(*at) } return mu } @@ -117,15 +116,15 @@ func (mu *MembershipUpdate) ClearMemberID() *MembershipUpdate { } // SetResourceType sets the "resource_type" field. -func (mu *MembershipUpdate) SetResourceType(bt biz.ResourceType) *MembershipUpdate { - mu.mutation.SetResourceType(bt) +func (mu *MembershipUpdate) SetResourceType(at authz.ResourceType) *MembershipUpdate { + mu.mutation.SetResourceType(at) return mu } // SetNillableResourceType sets the "resource_type" field if the given value is not nil. -func (mu *MembershipUpdate) SetNillableResourceType(bt *biz.ResourceType) *MembershipUpdate { - if bt != nil { - mu.SetResourceType(*bt) +func (mu *MembershipUpdate) SetNillableResourceType(at *authz.ResourceType) *MembershipUpdate { + if at != nil { + mu.SetResourceType(*at) } return mu } @@ -432,15 +431,15 @@ func (muo *MembershipUpdateOne) SetNillableRole(a *authz.Role) *MembershipUpdate } // SetMembershipType sets the "membership_type" field. -func (muo *MembershipUpdateOne) SetMembershipType(bt biz.MembershipType) *MembershipUpdateOne { - muo.mutation.SetMembershipType(bt) +func (muo *MembershipUpdateOne) SetMembershipType(at authz.MembershipType) *MembershipUpdateOne { + muo.mutation.SetMembershipType(at) return muo } // SetNillableMembershipType sets the "membership_type" field if the given value is not nil. -func (muo *MembershipUpdateOne) SetNillableMembershipType(bt *biz.MembershipType) *MembershipUpdateOne { - if bt != nil { - muo.SetMembershipType(*bt) +func (muo *MembershipUpdateOne) SetNillableMembershipType(at *authz.MembershipType) *MembershipUpdateOne { + if at != nil { + muo.SetMembershipType(*at) } return muo } @@ -472,15 +471,15 @@ func (muo *MembershipUpdateOne) ClearMemberID() *MembershipUpdateOne { } // SetResourceType sets the "resource_type" field. -func (muo *MembershipUpdateOne) SetResourceType(bt biz.ResourceType) *MembershipUpdateOne { - muo.mutation.SetResourceType(bt) +func (muo *MembershipUpdateOne) SetResourceType(at authz.ResourceType) *MembershipUpdateOne { + muo.mutation.SetResourceType(at) return muo } // SetNillableResourceType sets the "resource_type" field if the given value is not nil. -func (muo *MembershipUpdateOne) SetNillableResourceType(bt *biz.ResourceType) *MembershipUpdateOne { - if bt != nil { - muo.SetResourceType(*bt) +func (muo *MembershipUpdateOne) SetNillableResourceType(at *authz.ResourceType) *MembershipUpdateOne { + if at != nil { + muo.SetResourceType(*at) } return muo } diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index b8ddd1af2..b2d88d479 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -4586,9 +4586,9 @@ type MembershipMutation struct { created_at *time.Time updated_at *time.Time role *authz.Role - membership_type *biz.MembershipType + membership_type *authz.MembershipType member_id *uuid.UUID - resource_type *biz.ResourceType + resource_type *authz.ResourceType resource_id *uuid.UUID clearedFields map[string]struct{} organization *uuid.UUID @@ -4849,12 +4849,12 @@ func (m *MembershipMutation) ResetRole() { } // SetMembershipType sets the "membership_type" field. -func (m *MembershipMutation) SetMembershipType(bt biz.MembershipType) { - m.membership_type = &bt +func (m *MembershipMutation) SetMembershipType(at authz.MembershipType) { + m.membership_type = &at } // MembershipType returns the value of the "membership_type" field in the mutation. -func (m *MembershipMutation) MembershipType() (r biz.MembershipType, exists bool) { +func (m *MembershipMutation) MembershipType() (r authz.MembershipType, exists bool) { v := m.membership_type if v == nil { return @@ -4865,7 +4865,7 @@ func (m *MembershipMutation) MembershipType() (r biz.MembershipType, exists bool // OldMembershipType returns the old "membership_type" field's value of the Membership entity. // If the Membership object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *MembershipMutation) OldMembershipType(ctx context.Context) (v biz.MembershipType, err error) { +func (m *MembershipMutation) OldMembershipType(ctx context.Context) (v authz.MembershipType, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldMembershipType is only allowed on UpdateOne operations") } @@ -4947,12 +4947,12 @@ func (m *MembershipMutation) ResetMemberID() { } // SetResourceType sets the "resource_type" field. -func (m *MembershipMutation) SetResourceType(bt biz.ResourceType) { - m.resource_type = &bt +func (m *MembershipMutation) SetResourceType(at authz.ResourceType) { + m.resource_type = &at } // ResourceType returns the value of the "resource_type" field in the mutation. -func (m *MembershipMutation) ResourceType() (r biz.ResourceType, exists bool) { +func (m *MembershipMutation) ResourceType() (r authz.ResourceType, exists bool) { v := m.resource_type if v == nil { return @@ -4963,7 +4963,7 @@ func (m *MembershipMutation) ResourceType() (r biz.ResourceType, exists bool) { // OldResourceType returns the old "resource_type" field's value of the Membership entity. // If the Membership object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *MembershipMutation) OldResourceType(ctx context.Context) (v biz.ResourceType, err error) { +func (m *MembershipMutation) OldResourceType(ctx context.Context) (v authz.ResourceType, err error) { if !m.op.Is(OpUpdateOne) { return v, errors.New("OldResourceType is only allowed on UpdateOne operations") } @@ -5268,7 +5268,7 @@ func (m *MembershipMutation) SetField(name string, value ent.Value) error { m.SetRole(v) return nil case membership.FieldMembershipType: - v, ok := value.(biz.MembershipType) + v, ok := value.(authz.MembershipType) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } @@ -5282,7 +5282,7 @@ func (m *MembershipMutation) SetField(name string, value ent.Value) error { m.SetMemberID(v) return nil case membership.FieldResourceType: - v, ok := value.(biz.ResourceType) + v, ok := value.(authz.ResourceType) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } diff --git a/app/controlplane/pkg/data/ent/schema-viz.html b/app/controlplane/pkg/data/ent/schema-viz.html index cfd591370..02ef11249 100644 --- a/app/controlplane/pkg/data/ent/schema-viz.html +++ b/app/controlplane/pkg/data/ent/schema-viz.html @@ -70,7 +70,7 @@ } - const entGraph = JSON.parse("{\"nodes\":[{\"id\":\"APIToken\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"expires_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Attestation\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"bundle\",\"type\":\"[]byte\"},{\"name\":\"workflowrun_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"CASBackend\",\"fields\":[{\"name\":\"location\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"provider\",\"type\":\"biz.CASBackendProvider\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"validation_status\",\"type\":\"biz.CASBackendValidationStatus\"},{\"name\":\"validated_at\",\"type\":\"time.Time\"},{\"name\":\"default\",\"type\":\"bool\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"fallback\",\"type\":\"bool\"},{\"name\":\"max_blob_size_bytes\",\"type\":\"int64\"}]},{\"id\":\"CASMapping\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_run_id\",\"type\":\"uuid.UUID\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Integration\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"IntegrationAttachment\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Membership\",\"fields\":[{\"name\":\"current\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"role\",\"type\":\"authz.Role\"},{\"name\":\"membership_type\",\"type\":\"biz.MembershipType\"},{\"name\":\"member_id\",\"type\":\"uuid.UUID\"},{\"name\":\"resource_type\",\"type\":\"biz.ResourceType\"},{\"name\":\"resource_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"OrgInvitation\",\"fields\":[{\"name\":\"receiver_email\",\"type\":\"string\"},{\"name\":\"status\",\"type\":\"biz.OrgInvitationStatus\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"sender_id\",\"type\":\"uuid.UUID\"},{\"name\":\"role\",\"type\":\"authz.Role\"}]},{\"id\":\"Organization\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"block_on_policy_violation\",\"type\":\"bool\"}]},{\"id\":\"Project\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"ProjectVersion\",\"fields\":[{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"prerelease\",\"type\":\"bool\"},{\"name\":\"workflow_run_count\",\"type\":\"int\"},{\"name\":\"released_at\",\"type\":\"time.Time\"},{\"name\":\"latest\",\"type\":\"bool\"}]},{\"id\":\"Referrer\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"downloadable\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"metadata\",\"type\":\"map[string]string\"},{\"name\":\"annotations\",\"type\":\"map[string]string\"}]},{\"id\":\"RobotAccount\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"}]},{\"id\":\"User\",\"fields\":[{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"has_restricted_access\",\"type\":\"bool\"},{\"name\":\"first_name\",\"type\":\"string\"},{\"name\":\"last_name\",\"type\":\"string\"}]},{\"id\":\"Workflow\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"project_old\",\"type\":\"string\"},{\"name\":\"team\",\"type\":\"string\"},{\"name\":\"runs_count\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"public\",\"type\":\"bool\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"latest_run\",\"type\":\"uuid.UUID\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"metadata\",\"type\":\"map[string]interface {}\"}]},{\"id\":\"WorkflowContract\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"description\",\"type\":\"string\"}]},{\"id\":\"WorkflowContractVersion\",\"fields\":[{\"name\":\"body\",\"type\":\"[]byte\"},{\"name\":\"raw_body\",\"type\":\"[]byte\"},{\"name\":\"raw_body_format\",\"type\":\"unmarshal.RawFormat\"},{\"name\":\"revision\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowRun\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"finished_at\",\"type\":\"time.Time\"},{\"name\":\"state\",\"type\":\"biz.WorkflowRunStatus\"},{\"name\":\"reason\",\"type\":\"string\"},{\"name\":\"run_url\",\"type\":\"string\"},{\"name\":\"runner_type\",\"type\":\"string\"},{\"name\":\"attestation\",\"type\":\"*dsse.Envelope\"},{\"name\":\"attestation_digest\",\"type\":\"string\"},{\"name\":\"attestation_state\",\"type\":\"[]byte\"},{\"name\":\"contract_revision_used\",\"type\":\"int\"},{\"name\":\"contract_revision_latest\",\"type\":\"int\"},{\"name\":\"version_id\",\"type\":\"uuid.UUID\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]}],\"edges\":[{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Integration\",\"label\":\"integration\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Workflow\",\"label\":\"workflow\"},{\"from\":\"OrgInvitation\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"OrgInvitation\",\"to\":\"User\",\"label\":\"sender\"},{\"from\":\"Organization\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Organization\",\"to\":\"WorkflowContract\",\"label\":\"workflow_contracts\"},{\"from\":\"Organization\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Organization\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"Organization\",\"to\":\"Integration\",\"label\":\"integrations\"},{\"from\":\"Organization\",\"to\":\"APIToken\",\"label\":\"api_tokens\"},{\"from\":\"Organization\",\"to\":\"Project\",\"label\":\"projects\"},{\"from\":\"Project\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Project\",\"to\":\"ProjectVersion\",\"label\":\"versions\"},{\"from\":\"ProjectVersion\",\"to\":\"WorkflowRun\",\"label\":\"runs\"},{\"from\":\"Referrer\",\"to\":\"Referrer\",\"label\":\"references\"},{\"from\":\"Referrer\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"User\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Workflow\",\"to\":\"RobotAccount\",\"label\":\"robotaccounts\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"Workflow\",\"to\":\"WorkflowContract\",\"label\":\"contract\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"latest_workflow_run\"},{\"from\":\"WorkflowContract\",\"to\":\"WorkflowContractVersion\",\"label\":\"versions\"},{\"from\":\"WorkflowRun\",\"to\":\"WorkflowContractVersion\",\"label\":\"contract_version\"},{\"from\":\"WorkflowRun\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"WorkflowRun\",\"to\":\"Attestation\",\"label\":\"attestation_bundle\"}]}"); + const entGraph = JSON.parse("{\"nodes\":[{\"id\":\"APIToken\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"expires_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Attestation\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"bundle\",\"type\":\"[]byte\"},{\"name\":\"workflowrun_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"CASBackend\",\"fields\":[{\"name\":\"location\",\"type\":\"string\"},{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"provider\",\"type\":\"biz.CASBackendProvider\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"validation_status\",\"type\":\"biz.CASBackendValidationStatus\"},{\"name\":\"validated_at\",\"type\":\"time.Time\"},{\"name\":\"default\",\"type\":\"bool\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"fallback\",\"type\":\"bool\"},{\"name\":\"max_blob_size_bytes\",\"type\":\"int64\"}]},{\"id\":\"CASMapping\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_run_id\",\"type\":\"uuid.UUID\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Integration\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"secret_name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"}]},{\"id\":\"IntegrationAttachment\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"configuration\",\"type\":\"[]byte\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"Membership\",\"fields\":[{\"name\":\"current\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"role\",\"type\":\"authz.Role\"},{\"name\":\"membership_type\",\"type\":\"authz.MembershipType\"},{\"name\":\"member_id\",\"type\":\"uuid.UUID\"},{\"name\":\"resource_type\",\"type\":\"authz.ResourceType\"},{\"name\":\"resource_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"OrgInvitation\",\"fields\":[{\"name\":\"receiver_email\",\"type\":\"string\"},{\"name\":\"status\",\"type\":\"biz.OrgInvitationStatus\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"sender_id\",\"type\":\"uuid.UUID\"},{\"name\":\"role\",\"type\":\"authz.Role\"}]},{\"id\":\"Organization\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"block_on_policy_violation\",\"type\":\"bool\"}]},{\"id\":\"Project\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"}]},{\"id\":\"ProjectVersion\",\"fields\":[{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"prerelease\",\"type\":\"bool\"},{\"name\":\"workflow_run_count\",\"type\":\"int\"},{\"name\":\"released_at\",\"type\":\"time.Time\"},{\"name\":\"latest\",\"type\":\"bool\"}]},{\"id\":\"Referrer\",\"fields\":[{\"name\":\"digest\",\"type\":\"string\"},{\"name\":\"kind\",\"type\":\"string\"},{\"name\":\"downloadable\",\"type\":\"bool\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"metadata\",\"type\":\"map[string]string\"},{\"name\":\"annotations\",\"type\":\"map[string]string\"}]},{\"id\":\"RobotAccount\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"revoked_at\",\"type\":\"time.Time\"}]},{\"id\":\"User\",\"fields\":[{\"name\":\"email\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"has_restricted_access\",\"type\":\"bool\"},{\"name\":\"first_name\",\"type\":\"string\"},{\"name\":\"last_name\",\"type\":\"string\"}]},{\"id\":\"Workflow\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"project_old\",\"type\":\"string\"},{\"name\":\"team\",\"type\":\"string\"},{\"name\":\"runs_count\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"updated_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"public\",\"type\":\"bool\"},{\"name\":\"organization_id\",\"type\":\"uuid.UUID\"},{\"name\":\"project_id\",\"type\":\"uuid.UUID\"},{\"name\":\"latest_run\",\"type\":\"uuid.UUID\"},{\"name\":\"description\",\"type\":\"string\"},{\"name\":\"metadata\",\"type\":\"map[string]interface {}\"}]},{\"id\":\"WorkflowContract\",\"fields\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"deleted_at\",\"type\":\"time.Time\"},{\"name\":\"description\",\"type\":\"string\"}]},{\"id\":\"WorkflowContractVersion\",\"fields\":[{\"name\":\"body\",\"type\":\"[]byte\"},{\"name\":\"raw_body\",\"type\":\"[]byte\"},{\"name\":\"raw_body_format\",\"type\":\"unmarshal.RawFormat\"},{\"name\":\"revision\",\"type\":\"int\"},{\"name\":\"created_at\",\"type\":\"time.Time\"}]},{\"id\":\"WorkflowRun\",\"fields\":[{\"name\":\"created_at\",\"type\":\"time.Time\"},{\"name\":\"finished_at\",\"type\":\"time.Time\"},{\"name\":\"state\",\"type\":\"biz.WorkflowRunStatus\"},{\"name\":\"reason\",\"type\":\"string\"},{\"name\":\"run_url\",\"type\":\"string\"},{\"name\":\"runner_type\",\"type\":\"string\"},{\"name\":\"attestation\",\"type\":\"*dsse.Envelope\"},{\"name\":\"attestation_digest\",\"type\":\"string\"},{\"name\":\"attestation_state\",\"type\":\"[]byte\"},{\"name\":\"contract_revision_used\",\"type\":\"int\"},{\"name\":\"contract_revision_latest\",\"type\":\"int\"},{\"name\":\"version_id\",\"type\":\"uuid.UUID\"},{\"name\":\"workflow_id\",\"type\":\"uuid.UUID\"}]}],\"edges\":[{\"from\":\"CASMapping\",\"to\":\"CASBackend\",\"label\":\"cas_backend\"},{\"from\":\"CASMapping\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Integration\",\"label\":\"integration\"},{\"from\":\"IntegrationAttachment\",\"to\":\"Workflow\",\"label\":\"workflow\"},{\"from\":\"OrgInvitation\",\"to\":\"Organization\",\"label\":\"organization\"},{\"from\":\"OrgInvitation\",\"to\":\"User\",\"label\":\"sender\"},{\"from\":\"Organization\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Organization\",\"to\":\"WorkflowContract\",\"label\":\"workflow_contracts\"},{\"from\":\"Organization\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Organization\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"Organization\",\"to\":\"Integration\",\"label\":\"integrations\"},{\"from\":\"Organization\",\"to\":\"APIToken\",\"label\":\"api_tokens\"},{\"from\":\"Organization\",\"to\":\"Project\",\"label\":\"projects\"},{\"from\":\"Project\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"Project\",\"to\":\"ProjectVersion\",\"label\":\"versions\"},{\"from\":\"ProjectVersion\",\"to\":\"WorkflowRun\",\"label\":\"runs\"},{\"from\":\"Referrer\",\"to\":\"Referrer\",\"label\":\"references\"},{\"from\":\"Referrer\",\"to\":\"Workflow\",\"label\":\"workflows\"},{\"from\":\"User\",\"to\":\"Membership\",\"label\":\"memberships\"},{\"from\":\"Workflow\",\"to\":\"RobotAccount\",\"label\":\"robotaccounts\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"workflowruns\"},{\"from\":\"Workflow\",\"to\":\"WorkflowContract\",\"label\":\"contract\"},{\"from\":\"Workflow\",\"to\":\"WorkflowRun\",\"label\":\"latest_workflow_run\"},{\"from\":\"WorkflowContract\",\"to\":\"WorkflowContractVersion\",\"label\":\"versions\"},{\"from\":\"WorkflowRun\",\"to\":\"WorkflowContractVersion\",\"label\":\"contract_version\"},{\"from\":\"WorkflowRun\",\"to\":\"CASBackend\",\"label\":\"cas_backends\"},{\"from\":\"WorkflowRun\",\"to\":\"Attestation\",\"label\":\"attestation_bundle\"}]}"); const nodes = new vis.DataSet((entGraph.nodes || []).map(n => ({ id: n.id, diff --git a/app/controlplane/pkg/data/ent/schema/membership.go b/app/controlplane/pkg/data/ent/schema/membership.go index 4b6d1a738..76e26a96b 100644 --- a/app/controlplane/pkg/data/ent/schema/membership.go +++ b/app/controlplane/pkg/data/ent/schema/membership.go @@ -24,7 +24,6 @@ import ( "entgo.io/ent/schema/field" "entgo.io/ent/schema/index" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" - "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/google/uuid" ) @@ -52,10 +51,10 @@ func (Membership) Fields() []ent.Field { field.Enum("role").GoType(authz.Role("")), // polymorphic membership for RBAC - field.Enum("membership_type").GoType(biz.MembershipType("")).Optional(), + field.Enum("membership_type").GoType(authz.MembershipType("")).Optional(), field.UUID("member_id", uuid.UUID{}).Optional(), - field.Enum("resource_type").GoType(biz.ResourceType("")).Optional(), + field.Enum("resource_type").GoType(authz.ResourceType("")).Optional(), field.UUID("resource_id", uuid.UUID{}).Optional(), } } diff --git a/app/controlplane/pkg/data/membership.go b/app/controlplane/pkg/data/membership.go index 3c06a9dd4..8b6f9f17f 100644 --- a/app/controlplane/pkg/data/membership.go +++ b/app/controlplane/pkg/data/membership.go @@ -46,9 +46,9 @@ func (r *MembershipRepo) Create(ctx context.Context, orgID, userID uuid.UUID, cu SetOrganizationID(orgID). SetCurrent(current). SetRole(role). - SetMembershipType(biz.MembershipTypeUser). + SetMembershipType(authz.MembershipTypeUser). SetMemberID(userID). - SetResourceType(biz.ResourceTypeOrganization). + SetResourceType(authz.ResourceTypeOrganization). SetResourceID(orgID). Save(ctx) if err != nil { @@ -70,8 +70,8 @@ func (r *MembershipRepo) loadMembership(ctx context.Context, id uuid.UUID) (*ent func (r *MembershipRepo) FindByUser(ctx context.Context, userID uuid.UUID) ([]*biz.Membership, error) { memberships, err := r.data.DB.Membership.Query().Where( - membership.ResourceTypeEQ(biz.ResourceTypeOrganization), - membership.MembershipTypeEQ(biz.MembershipTypeUser), + membership.ResourceTypeEQ(authz.ResourceTypeOrganization), + membership.MembershipTypeEQ(authz.MembershipTypeUser), membership.MemberID(userID), ).WithOrganization().All(ctx) if err != nil { @@ -84,8 +84,8 @@ func (r *MembershipRepo) FindByUser(ctx context.Context, userID uuid.UUID) ([]*b // FindByOrg finds all memberships for a given organization func (r *MembershipRepo) FindByOrg(ctx context.Context, orgID uuid.UUID) ([]*biz.Membership, error) { memberships, err := r.data.DB.Membership.Query().Where( - membership.MembershipTypeEQ(biz.MembershipTypeUser), - membership.ResourceTypeEQ(biz.ResourceTypeOrganization), + membership.MembershipTypeEQ(authz.MembershipTypeUser), + membership.ResourceTypeEQ(authz.ResourceTypeOrganization), membership.ResourceIDEQ(orgID), ).WithUser().WithOrganization().All(ctx) if err != nil { @@ -98,9 +98,9 @@ func (r *MembershipRepo) FindByOrg(ctx context.Context, orgID uuid.UUID) ([]*biz // FindByOrgAndUser finds the membership for a given organization and user func (r *MembershipRepo) FindByOrgAndUser(ctx context.Context, orgID, userID uuid.UUID) (*biz.Membership, error) { m, err := r.data.DB.Membership.Query().Where( - membership.MembershipTypeEQ(biz.MembershipTypeUser), + membership.MembershipTypeEQ(authz.MembershipTypeUser), membership.MemberID(userID), - membership.ResourceTypeEQ(biz.ResourceTypeOrganization), + membership.ResourceTypeEQ(authz.ResourceTypeOrganization), membership.ResourceIDEQ(orgID), ).WithOrganization().WithUser().Only(ctx) if err != nil && !ent.IsNotFound(err) { @@ -119,9 +119,9 @@ func (r *MembershipRepo) FindByOrgNameAndUser(ctx context.Context, orgName strin } m, err := r.data.DB.Membership.Query().Where( - membership.MembershipTypeEQ(biz.MembershipTypeUser), + membership.MembershipTypeEQ(authz.MembershipTypeUser), membership.MemberID(userID), - membership.ResourceTypeEQ(biz.ResourceTypeOrganization), + membership.ResourceTypeEQ(authz.ResourceTypeOrganization), membership.ResourceID(org.ID), ).WithOrganization().WithUser().Only(ctx) if err != nil && !ent.IsNotFound(err) { @@ -133,9 +133,9 @@ func (r *MembershipRepo) FindByOrgNameAndUser(ctx context.Context, orgName strin func (r *MembershipRepo) FindByIDInUser(ctx context.Context, userID, membershipID uuid.UUID) (*biz.Membership, error) { m, err := r.data.DB.Membership.Query().Where( - membership.MembershipTypeEQ(biz.MembershipTypeUser), + membership.MembershipTypeEQ(authz.MembershipTypeUser), membership.MemberID(userID), - membership.ResourceTypeEQ(biz.ResourceTypeOrganization), + membership.ResourceTypeEQ(authz.ResourceTypeOrganization), membership.ID(membershipID), ).WithUser().WithOrganization().Only(ctx) if err != nil && !ent.IsNotFound(err) { @@ -147,7 +147,7 @@ func (r *MembershipRepo) FindByIDInUser(ctx context.Context, userID, membershipI func (r *MembershipRepo) ListAllByUser(ctx context.Context, userID uuid.UUID) ([]*biz.Membership, error) { mm, err := r.data.DB.Membership.Query().Where( - membership.MembershipTypeEQ(biz.MembershipTypeUser), + membership.MembershipTypeEQ(authz.MembershipTypeUser), membership.MemberID(userID), ).All(ctx) @@ -158,9 +158,9 @@ func (r *MembershipRepo) ListAllByUser(ctx context.Context, userID uuid.UUID) ([ return entMembershipsToBiz(mm), nil } -func (r *MembershipRepo) GetMembershipByUserAndResource(ctx context.Context, userID uuid.UUID, resourceType biz.ResourceType, resourceID uuid.UUID) (*biz.Membership, error) { +func (r *MembershipRepo) GetMembershipByUserAndResource(ctx context.Context, userID uuid.UUID, resourceType authz.ResourceType, resourceID uuid.UUID) (*biz.Membership, error) { m, err := r.data.DB.Membership.Query().Where( - membership.MembershipTypeEQ(biz.MembershipTypeUser), + membership.MembershipTypeEQ(authz.MembershipTypeUser), membership.MemberID(userID), membership.ResourceTypeEQ(resourceType), membership.ResourceID(resourceID), @@ -177,8 +177,8 @@ func (r *MembershipRepo) GetMembershipByUserAndResource(ctx context.Context, use func (r *MembershipRepo) FindByIDInOrg(ctx context.Context, orgID, membershipID uuid.UUID) (*biz.Membership, error) { m, err := r.data.DB.Membership.Query().Where( - membership.MembershipTypeEQ(biz.MembershipTypeUser), - membership.ResourceTypeEQ(biz.ResourceTypeOrganization), + membership.MembershipTypeEQ(authz.MembershipTypeUser), + membership.ResourceTypeEQ(authz.ResourceTypeOrganization), membership.ResourceIDEQ(orgID), membership.ID(membershipID), ).WithUser().WithOrganization().Only(ctx) @@ -199,8 +199,8 @@ func (r *MembershipRepo) SetCurrent(ctx context.Context, membershipID uuid.UUID) if err = WithTx(ctx, r.data.DB, func(tx *ent.Tx) error { // 1 - Set all the memberships to current=false if err = tx.Membership.Update().Where( - membership.ResourceTypeEQ(biz.ResourceTypeOrganization), - membership.MembershipTypeEQ(biz.MembershipTypeUser), + membership.ResourceTypeEQ(authz.ResourceTypeOrganization), + membership.MembershipTypeEQ(authz.MembershipTypeUser), membership.MemberID(m.MemberID)). SetCurrent(false).Exec(ctx); err != nil { return err From 38f1973f97acb2173ec47f899d950dc2b4a2cede Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 18 Jun 2025 19:15:57 +0200 Subject: [PATCH 05/48] cache membership data Signed-off-by: Jose I. Paris --- .../currentorganization_middleware.go | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware.go b/app/controlplane/internal/usercontext/currentorganization_middleware.go index 9c2128042..b3d9ae2a3 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware.go @@ -19,6 +19,7 @@ import ( "context" "errors" "fmt" + "time" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" @@ -27,8 +28,11 @@ import ( "github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/middleware" "github.com/google/uuid" + "github.com/hashicorp/golang-lru/v2/expirable" ) +var membershipsCache = expirable.NewLRU[string, *entities.Membership](0, nil, time.Second*10) + func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, membershipUC *biz.MembershipUseCase, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { @@ -60,7 +64,7 @@ func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, membership orgRole := CurrentAuthzSubject(ctx) if orgRole == string(authz.RoleOrgMember) { // Org member enables the new RBAC behavior. Let's store all memberships in the context. - ctx, err = setCurrentMembershipsForUser(ctx, u, membershipUC, logger); + ctx, err = setCurrentMembershipsForUser(ctx, u, membershipUC) if err != nil { return nil, fmt.Errorf("error setting current org membership: %w", err) } @@ -78,26 +82,36 @@ func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, membership } } -func setCurrentMembershipsForUser(ctx context.Context, u *entities.User, membershipUC *biz.MembershipUseCase, logger *log.Helper) (context.Context, error) { - uid, err := uuid.Parse(u.ID) - if err != nil { - return nil, err - } +// setCurrentMembershipsForUser retrieves all user memberships for RBAC +func setCurrentMembershipsForUser(ctx context.Context, u *entities.User, membershipUC *biz.MembershipUseCase) (context.Context, error) { + var membership *entities.Membership + var ok bool - mm, err := membershipUC.ListAllMembershipsForUser(ctx, uid) - if err != nil { - return nil, fmt.Errorf("error getting membership list: %w", err) - } + if membership, ok = membershipsCache.Get(u.ID); !ok { + uid, err := uuid.Parse(u.ID) + if err != nil { + return nil, err + } + + mm, err := membershipUC.ListAllMembershipsForUser(ctx, uid) + if err != nil { + return nil, fmt.Errorf("error getting membership list: %w", err) + } - resourceMemberships := make([]*entities.ResourceMembership, 0, len(mm)) - for _, m := range mm { - resourceMemberships = append(resourceMemberships, &entities.ResourceMembership{ - Role: m.Role, - ResourceType: m.ResourceType, - ResourceID: m.ResourceID, - }) + resourceMemberships := make([]*entities.ResourceMembership, 0, len(mm)) + for _, m := range mm { + resourceMemberships = append(resourceMemberships, &entities.ResourceMembership{ + Role: m.Role, + ResourceType: m.ResourceType, + ResourceID: m.ResourceID, + }) + } + + membership = &entities.Membership{Resources: resourceMemberships} + membershipsCache.Add(u.ID, membership) } - return entities.WithMembership(ctx, &entities.Membership{Resources: resourceMemberships}), nil + + return entities.WithMembership(ctx, membership), nil } func setCurrentOrganizationFromHeader(ctx context.Context, user *entities.User, orgName string, userUC biz.UserOrgFinder) (context.Context, error) { From c14816280765063fa1aa3c998167c3c1c659006c Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 18 Jun 2025 19:22:31 +0200 Subject: [PATCH 06/48] fix tests Signed-off-by: Jose I. Paris --- .../currentorganization_middleware.go | 4 +- .../currentorganization_middleware_test.go | 3 +- app/controlplane/pkg/biz/membership.go | 9 ++- .../pkg/biz/mocks/MembershipsRBAC.go | 62 +++++++++++++++++++ app/controlplane/pkg/data/membership.go | 17 ----- 5 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 app/controlplane/pkg/biz/mocks/MembershipsRBAC.go diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware.go b/app/controlplane/internal/usercontext/currentorganization_middleware.go index b3d9ae2a3..e7a06dadc 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware.go @@ -33,7 +33,7 @@ import ( var membershipsCache = expirable.NewLRU[string, *entities.Membership](0, nil, time.Second*10) -func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, membershipUC *biz.MembershipUseCase, logger *log.Helper) middleware.Middleware { +func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, membershipUC biz.MembershipsRBAC, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { // Get the current user and return if not found, meaning we are probably coming from an API Token @@ -83,7 +83,7 @@ func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, membership } // setCurrentMembershipsForUser retrieves all user memberships for RBAC -func setCurrentMembershipsForUser(ctx context.Context, u *entities.User, membershipUC *biz.MembershipUseCase) (context.Context, error) { +func setCurrentMembershipsForUser(ctx context.Context, u *entities.User, membershipUC biz.MembershipsRBAC) (context.Context, error) { var membership *entities.Membership var ok bool diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware_test.go b/app/controlplane/internal/usercontext/currentorganization_middleware_test.go index 709aeae77..06cd0c908 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware_test.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware_test.go @@ -65,6 +65,7 @@ func TestWithCurrentOrganizationMiddleware(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { usecase := bizMocks.NewUserOrgFinder(t) + msMock := bizMocks.NewMembershipsRBAC(t) ctx := context.Background() if tc.loggedIn { ctx = entities.WithCurrentUser(ctx, &entities.User{ID: wantUser.ID}) @@ -80,7 +81,7 @@ func TestWithCurrentOrganizationMiddleware(t *testing.T) { usecase.On("CurrentMembership", ctx, wantUser.ID).Maybe().Return(nil, nil) } - m := WithCurrentOrganizationMiddleware(usecase, logger) + m := WithCurrentOrganizationMiddleware(usecase, msMock, logger) _, err := m( func(ctx context.Context, _ interface{}) (interface{}, error) { if tc.wantErr { diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index 60783fa65..d5fd840e8 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -55,7 +55,10 @@ type MembershipRepo interface { // RBAC methods ListAllByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) - GetMembershipByUserAndResource(ctx context.Context, userID uuid.UUID, resourceType authz.ResourceType, resourceID uuid.UUID) (*Membership, error) +} + +type MembershipsRBAC interface { + ListAllMembershipsForUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) } type MembershipUseCase struct { @@ -257,10 +260,6 @@ func (uc *MembershipUseCase) ListAllMembershipsForUser(ctx context.Context, user return uc.repo.ListAllByUser(ctx, userID) } -func (uc *MembershipUseCase) GetMembershipForResource(ctx context.Context, userID uuid.UUID, resourceType authz.ResourceType, resourceID uuid.UUID) (*Membership, error) { - return uc.repo.GetMembershipByUserAndResource(ctx, userID, resourceType, resourceID) -} - // SetCurrent sets the current membership for the user // and unsets the previous one func (uc *MembershipUseCase) SetCurrent(ctx context.Context, userID, membershipID string) (*Membership, error) { diff --git a/app/controlplane/pkg/biz/mocks/MembershipsRBAC.go b/app/controlplane/pkg/biz/mocks/MembershipsRBAC.go new file mode 100644 index 000000000..5ec76d161 --- /dev/null +++ b/app/controlplane/pkg/biz/mocks/MembershipsRBAC.go @@ -0,0 +1,62 @@ +// Code generated by mockery v2.53.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + biz "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" + + mock "github.com/stretchr/testify/mock" + + uuid "github.com/google/uuid" +) + +// MembershipsRBAC is an autogenerated mock type for the MembershipsRBAC type +type MembershipsRBAC struct { + mock.Mock +} + +// ListAllMembershipsForUser provides a mock function with given fields: ctx, userID +func (_m *MembershipsRBAC) ListAllMembershipsForUser(ctx context.Context, userID uuid.UUID) ([]*biz.Membership, error) { + ret := _m.Called(ctx, userID) + + if len(ret) == 0 { + panic("no return value specified for ListAllMembershipsForUser") + } + + var r0 []*biz.Membership + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) ([]*biz.Membership, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) []*biz.Membership); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*biz.Membership) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewMembershipsRBAC creates a new instance of MembershipsRBAC. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMembershipsRBAC(t interface { + mock.TestingT + Cleanup(func()) +}) *MembershipsRBAC { + mock := &MembershipsRBAC{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/app/controlplane/pkg/data/membership.go b/app/controlplane/pkg/data/membership.go index 8b6f9f17f..d1cb4cda7 100644 --- a/app/controlplane/pkg/data/membership.go +++ b/app/controlplane/pkg/data/membership.go @@ -158,23 +158,6 @@ func (r *MembershipRepo) ListAllByUser(ctx context.Context, userID uuid.UUID) ([ return entMembershipsToBiz(mm), nil } -func (r *MembershipRepo) GetMembershipByUserAndResource(ctx context.Context, userID uuid.UUID, resourceType authz.ResourceType, resourceID uuid.UUID) (*biz.Membership, error) { - m, err := r.data.DB.Membership.Query().Where( - membership.MembershipTypeEQ(authz.MembershipTypeUser), - membership.MemberID(userID), - membership.ResourceTypeEQ(resourceType), - membership.ResourceID(resourceID), - ).Only(ctx) - - if err != nil { - if ent.IsNotFound(err) { - return nil, biz.NewErrNotFound(fmt.Sprintf("resource %s not found", resourceID)) - } - return nil, fmt.Errorf("failed to query memberships: %v", err) - } - return entMembershipToBiz(m), nil -} - func (r *MembershipRepo) FindByIDInOrg(ctx context.Context, orgID, membershipID uuid.UUID) (*biz.Membership, error) { m, err := r.data.DB.Membership.Query().Where( membership.MembershipTypeEQ(authz.MembershipTypeUser), From 8287d0c02ae19f5763b84cf31421dce560e0c73d Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 19 Jun 2025 11:11:49 +0200 Subject: [PATCH 07/48] add enforcer to services Signed-off-by: Jose I. Paris --- app/controlplane/cmd/wire.go | 3 +- app/controlplane/cmd/wire_gen.go | 7 ++-- .../internal/service/attestation.go | 14 ++++++++ app/controlplane/internal/service/service.go | 35 ++++++++++++++++++- .../usercontext/currentuser_middleware.go | 3 +- app/controlplane/pkg/authz/authz.go | 5 +++ 6 files changed, 61 insertions(+), 6 deletions(-) diff --git a/app/controlplane/cmd/wire.go b/app/controlplane/cmd/wire.go index 6012c8509..b56b78105 100644 --- a/app/controlplane/cmd/wire.go +++ b/app/controlplane/cmd/wire.go @@ -90,9 +90,10 @@ func newPolicyProviderConfig(in []*conf.PolicyProvider) []*policies.NewRegistryC return out } -func serviceOpts(l log.Logger) []service.NewOpt { +func serviceOpts(l log.Logger, enforcer *authz.Enforcer) []service.NewOpt { return []service.NewOpt{ service.WithLogger(l), + service.WithEnforcer(enforcer), } } diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index fb9f1d334..157360703 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -128,7 +128,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l workflowContractUseCase := biz.NewWorkflowContractUseCase(workflowContractRepo, registry, auditorUseCase, logger) workflowUseCase := biz.NewWorkflowUsecase(workflowRepo, projectsRepo, workflowContractUseCase, auditorUseCase, logger) projectUseCase := biz.NewProjectsUseCase(logger, projectsRepo) - v5 := serviceOpts(logger) + v5 := serviceOpts(logger, enforcer) workflowService := service.NewWorkflowService(workflowUseCase, workflowContractUseCase, projectUseCase, v5...) orgInvitationRepo := data.NewOrgInvitation(dataData, logger) orgInvitationUseCase, err := biz.NewOrgInvitationUseCase(orgInvitationRepo, membershipRepo, userRepo, auditorUseCase, logger) @@ -190,6 +190,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l ReferrerUC: referrerUseCase, OrgUC: organizationUseCase, PromUC: prometheusUseCase, + ProjectUC: projectUseCase, ProjectVersionUC: projectVersionUseCase, SigningUseCase: signingUseCase, Opts: v5, @@ -327,8 +328,8 @@ func newPolicyProviderConfig(in []*conf.PolicyProvider) []*policies.NewRegistryC return out } -func serviceOpts(l log.Logger) []service.NewOpt { - return []service.NewOpt{service.WithLogger(l)} +func serviceOpts(l log.Logger, enforcer *authz.Enforcer) []service.NewOpt { + return []service.NewOpt{service.WithLogger(l), service.WithEnforcer(enforcer)} } func newCASServerOptions(in *conf.Bootstrap_CASServer) *biz.CASServerDefaultOpts { diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 047185d42..9847f802a 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -27,6 +27,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/internal/dispatcher" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" "github.com/chainloop-dev/chainloop/pkg/attestation" @@ -54,6 +55,7 @@ type AttestationService struct { orgUseCase *biz.OrganizationUseCase prometheusUseCase *biz.PrometheusUseCase projectVersionUseCase *biz.ProjectVersionUseCase + projectUseCase *biz.ProjectUseCase signingUseCase *biz.SigningUseCase } @@ -71,6 +73,7 @@ type NewAttestationServiceOpts struct { ReferrerUC *biz.ReferrerUseCase OrgUC *biz.OrganizationUseCase PromUC *biz.PrometheusUseCase + ProjectUC *biz.ProjectUseCase ProjectVersionUC *biz.ProjectVersionUseCase SigningUseCase *biz.SigningUseCase Opts []NewOpt @@ -92,6 +95,7 @@ func NewAttestationService(opts *NewAttestationServiceOpts) *AttestationService referrerUseCase: opts.ReferrerUC, orgUseCase: opts.OrgUC, prometheusUseCase: opts.PromUC, + projectUseCase: opts.ProjectUC, projectVersionUseCase: opts.ProjectVersionUC, signingUseCase: opts.SigningUseCase, } @@ -642,6 +646,16 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP return nil, errors.NotFound("not found", "neither robot account nor API token found") } + // try to load project and apply RBAC if needed + p, err := s.projectUseCase.FindProjectByReference(ctx, apiToken.OrgID, &biz.EntityRef{Name: req.ProjectName}) + if err != nil { + if !biz.IsNotFound(err) { + return nil, handleUseCaseErr(err, s.log) + } + } else if err = s.authorizeResource(ctx, authz.PolicyWorkflowCreate, authz.ResourceTypeProject, p.ID); err != nil { + return nil, err + } + if wf, err := s.workflowUseCase.FindByNameInOrg(ctx, apiToken.OrgID, req.GetProjectName(), req.GetWorkflowName()); err != nil { if !biz.IsNotFound(err) { return nil, handleUseCaseErr(err, s.log) diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 8dada446b..ee34f3e0f 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -21,11 +21,13 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/chainloop-dev/chainloop/pkg/servicelogger" "github.com/go-kratos/kratos/v2/errors" "github.com/go-kratos/kratos/v2/log" + "github.com/google/uuid" "github.com/google/wire" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -125,7 +127,8 @@ func newService(opts ...NewOpt) *service { } type service struct { - log *log.Helper + log *log.Helper + enforcer *authz.Enforcer } type NewOpt func(s *service) @@ -136,6 +139,36 @@ func WithLogger(logger log.Logger) NewOpt { } } +func WithEnforcer(enforcer *authz.Enforcer) NewOpt { + return func(s *service) { + s.enforcer = enforcer + } +} + +func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resourceType authz.ResourceType, resourceID uuid.UUID) error { + sub := usercontext.CurrentAuthzSubject(ctx) + if sub != string(authz.RoleOrgMember) { + return nil + } + + // Apply RBAC + m := entities.CurrentMembership(ctx) + // check for specific resource role + for _, rm := range m.Resources { + if rm.ResourceType == resourceType && rm.ResourceID == resourceID { + pass, err := s.enforcer.Enforce(string(rm.Role), op) + if err != nil { + return handleUseCaseErr(err, s.log) + } + if !pass { + return errors.Forbidden("forbidden", "user not authorized") + } + return nil + } + } + return errors.Forbidden("forbidden", "user not authorized") +} + // NOTE: some of these http errors get automatically translated to gRPC status codes // because they implement the gRPC status error interface // so it is safe to return either a gRPC status error or a kratos error diff --git a/app/controlplane/internal/usercontext/currentuser_middleware.go b/app/controlplane/internal/usercontext/currentuser_middleware.go index 8d82f4f95..a4942588c 100644 --- a/app/controlplane/internal/usercontext/currentuser_middleware.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware.go @@ -128,7 +128,8 @@ func WithAttestationContextFromUser(userUC *biz.UserUseCase, membershipUC *biz.M // Load the authorization subject from the context which might be related to a currentUser or an APItoken // TODO: move to authz middleware once we add support for all the tokens // for now in that middleware we are not mapping admins nor owners to a specific role - if subject != string(authz.RoleAdmin) && subject != string(authz.RoleOwner) { + // Admins and Owners can perform any operation. Members will need additional RBAC behaviour at service layer + if subject != string(authz.RoleAdmin) && subject != string(authz.RoleOwner) && subject != string(authz.RoleOrgMember) { return nil, fmt.Errorf("your user doesn't have permissions to perform attestations in this organization, role=%s, orgID=%s", subject, org.ID) } diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 9c4162278..a712f9496 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -76,6 +76,9 @@ const ( // so Casbin rules (role, resource-type, action) are NOT enough to check for permission, since we must check for ownership as well. // That last check will be done at the service level. RoleOrgMember Role = "role:org:member" + + RoleProjectAdmin Role = "role:project:admin" + RoleProjectViewer Role = "role:project:viewer" ) // resource, action tuple @@ -178,6 +181,8 @@ var rolesMap = map[Role][]*Policy{ PolicyProjectCreate, PolicyProjectUpdate, PolicyProjectDelete, + }, + RoleProjectAdmin: { PolicyWorkflowCreate, }, } From 49ebeade0d2e22dbd7b95018331ac59786e7951e Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 12:56:39 +0200 Subject: [PATCH 08/48] attestation init Signed-off-by: Jose I. Paris --- app/cli/documentation/cli-reference.mdx | 4 +- app/controlplane/cmd/wire_gen.go | 2 +- .../internal/service/attestation.go | 17 +++++- app/controlplane/internal/service/service.go | 7 ++- .../internal/usercontext/entities/user.go | 3 +- app/controlplane/pkg/authz/authz.go | 8 ++- app/controlplane/pkg/biz/membership.go | 37 +++++++++-- .../pkg/biz/testhelpers/wire_gen.go | 2 +- app/controlplane/pkg/biz/workflow.go | 25 +++++--- .../pkg/data/ent/membership/membership.go | 2 +- .../pkg/data/ent/migrate/schema.go | 4 +- .../data/ent/orginvitation/orginvitation.go | 2 +- app/controlplane/pkg/data/membership.go | 61 +++++++++++++++---- 13 files changed, 133 insertions(+), 41 deletions(-) diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 20ac04df0..01987dfa2 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2282,7 +2282,7 @@ Options ``` -h, --help help for create --receiver string Email of the user to invite ---role string Role of the user in the organization, available admin, owner, viewer (default "viewer") +--role string Role of the user in the organization, available admin, owner, viewer, member (default "viewer") ``` Options inherited from parent commands @@ -2440,7 +2440,7 @@ Options ``` -h, --help help for update --id string Membership ID ---role string Role of the user in the organization, available admin, owner, viewer (default "viewer") +--role string Role of the user in the organization, available admin, owner, viewer, member (default "viewer") ``` Options inherited from parent commands diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 157360703..12a5b1611 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -126,7 +126,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l return nil, nil, err } workflowContractUseCase := biz.NewWorkflowContractUseCase(workflowContractRepo, registry, auditorUseCase, logger) - workflowUseCase := biz.NewWorkflowUsecase(workflowRepo, projectsRepo, workflowContractUseCase, auditorUseCase, logger) + workflowUseCase := biz.NewWorkflowUsecase(workflowRepo, projectsRepo, workflowContractUseCase, auditorUseCase, membershipUseCase, logger) projectUseCase := biz.NewProjectsUseCase(logger, projectsRepo) v5 := serviceOpts(logger, enforcer) workflowService := service.NewWorkflowService(workflowUseCase, workflowContractUseCase, projectUseCase, v5...) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 9847f802a..3cb25b724 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -27,6 +27,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/internal/dispatcher" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" @@ -35,6 +36,7 @@ import ( "github.com/chainloop-dev/chainloop/pkg/credentials" errors "github.com/go-kratos/kratos/v2/errors" v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/uuid" ) type AttestationService struct { @@ -652,8 +654,10 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP if !biz.IsNotFound(err) { return nil, handleUseCaseErr(err, s.log) } - } else if err = s.authorizeResource(ctx, authz.PolicyWorkflowCreate, authz.ResourceTypeProject, p.ID); err != nil { - return nil, err + } else if p != nil { + if err = s.authorizeResource(ctx, authz.PolicyWorkflowCreate, authz.ResourceTypeProject, p.ID); err != nil { + return nil, err + } } if wf, err := s.workflowUseCase.FindByNameInOrg(ctx, apiToken.OrgID, req.GetProjectName(), req.GetWorkflowName()); err != nil { @@ -672,6 +676,15 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP ContractName: req.GetContractName(), } + // set project owner if RBAC is enabled + if user := entities.CurrentUser(ctx); user != nil && rbacEnabled(ctx) { + userID, err := uuid.Parse(user.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + createOpts.Owner = &userID + } + wf, err := s.workflowUseCase.Create(ctx, createOpts) if err != nil { return nil, handleUseCaseErr(err, s.log) diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index ee34f3e0f..0dff2f77b 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -146,8 +146,7 @@ func WithEnforcer(enforcer *authz.Enforcer) NewOpt { } func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resourceType authz.ResourceType, resourceID uuid.UUID) error { - sub := usercontext.CurrentAuthzSubject(ctx) - if sub != string(authz.RoleOrgMember) { + if !rbacEnabled(ctx) { return nil } @@ -169,6 +168,10 @@ func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resou return errors.Forbidden("forbidden", "user not authorized") } +func rbacEnabled(ctx context.Context) bool { + return usercontext.CurrentAuthzSubject(ctx) == string(authz.RoleOrgMember) +} + // NOTE: some of these http errors get automatically translated to gRPC status codes // because they implement the gRPC status error interface // so it is safe to return either a gRPC status error or a kratos error diff --git a/app/controlplane/internal/usercontext/entities/user.go b/app/controlplane/internal/usercontext/entities/user.go index fb5f71087..8c8cb4df6 100644 --- a/app/controlplane/internal/usercontext/entities/user.go +++ b/app/controlplane/internal/usercontext/entities/user.go @@ -30,8 +30,7 @@ func WithCurrentUser(ctx context.Context, user *User) context.Context { return context.WithValue(ctx, currentUserCtxKey{}, user) } -// RequestID tries to retrieve requestID from the given context. -// If it doesn't exist, an empty string is returned. +// CurrentUser retrieves the user from the context func CurrentUser(ctx context.Context) *User { res := ctx.Value(currentUserCtxKey{}) if res == nil { diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index a712f9496..cf22eef9d 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -77,7 +77,7 @@ const ( // That last check will be done at the service level. RoleOrgMember Role = "role:org:member" - RoleProjectAdmin Role = "role:project:admin" + RoleProjectAdmin Role = "role:project:admin" RoleProjectViewer Role = "role:project:viewer" ) @@ -181,6 +181,7 @@ var rolesMap = map[Role][]*Policy{ PolicyProjectCreate, PolicyProjectUpdate, PolicyProjectDelete, + PolicyWorkflowContractCreate, }, RoleProjectAdmin: { PolicyWorkflowCreate, @@ -457,6 +458,11 @@ func (Role) Values() (roles []string) { RoleOwner, RoleAdmin, RoleViewer, + + // RBAC roles + RoleOrgMember, + RoleProjectAdmin, + RoleProjectViewer, } { roles = append(roles, string(s)) } diff --git a/app/controlplane/pkg/biz/membership.go b/app/controlplane/pkg/biz/membership.go index d5fd840e8..c60b9a444 100644 --- a/app/controlplane/pkg/biz/membership.go +++ b/app/controlplane/pkg/biz/membership.go @@ -55,6 +55,8 @@ type MembershipRepo interface { // RBAC methods ListAllByUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) + ListAllByResource(ctx context.Context, rt authz.ResourceType, id uuid.UUID) ([]*Membership, error) + AddResourceRole(ctx context.Context, resourceType authz.ResourceType, resID uuid.UUID, mType authz.MembershipType, memberID uuid.UUID, role authz.Role) error } type MembershipsRBAC interface { @@ -69,7 +71,7 @@ type MembershipUseCase struct { } func NewMembershipUseCase(repo MembershipRepo, orgUC *OrganizationUseCase, auditor *AuditorUseCase, logger log.Logger) *MembershipUseCase { - return &MembershipUseCase{repo, orgUC, log.NewHelper(logger), auditor} + return &MembershipUseCase{repo: repo, orgUseCase: orgUC, logger: log.NewHelper(logger), auditor: auditor} } // LeaveAndDeleteOrg deletes a membership (and the org i) from the database associated with the current user @@ -255,11 +257,6 @@ func (uc *MembershipUseCase) ByOrg(ctx context.Context, orgID string) ([]*Member return uc.repo.FindByOrg(ctx, orgUUID) } -// ListAllMembershipsForUser retrieves all membership records by resource type -func (uc *MembershipUseCase) ListAllMembershipsForUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) { - return uc.repo.ListAllByUser(ctx, userID) -} - // SetCurrent sets the current membership for the user // and unsets the previous one func (uc *MembershipUseCase) SetCurrent(ctx context.Context, userID, membershipID string) (*Membership, error) { @@ -319,3 +316,31 @@ func (uc *MembershipUseCase) FindByOrgNameAndUser(ctx context.Context, orgName, return m, nil } + +// RBAC methods + +// ListAllMembershipsForUser retrieves all membership records by resource type +func (uc *MembershipUseCase) ListAllMembershipsForUser(ctx context.Context, userID uuid.UUID) ([]*Membership, error) { + return uc.repo.ListAllByUser(ctx, userID) +} + +// SetProjectOwner sets the project owner (admin role). It skips the operation if an owner exists already +func (uc *MembershipUseCase) SetProjectOwner(ctx context.Context, projectID, userID uuid.UUID) error { + mm, err := uc.repo.ListAllByResource(ctx, authz.ResourceTypeProject, projectID) + if err != nil { + return fmt.Errorf("failed to find membership: %w", err) + } + + for _, m := range mm { + if m.Role == authz.RoleProjectAdmin { + // Found one already + return nil + } + } + + if err = uc.repo.AddResourceRole(ctx, authz.ResourceTypeProject, projectID, authz.MembershipTypeUser, userID, authz.RoleProjectAdmin); err != nil { + return fmt.Errorf("failed to set project owner: %w", err) + } + + return nil +} diff --git a/app/controlplane/pkg/biz/testhelpers/wire_gen.go b/app/controlplane/pkg/biz/testhelpers/wire_gen.go index 677d98cf3..e64eec56d 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire_gen.go +++ b/app/controlplane/pkg/biz/testhelpers/wire_gen.go @@ -80,7 +80,7 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r } workflowContractUseCase := biz.NewWorkflowContractUseCase(workflowContractRepo, registry, auditorUseCase, logger) projectsRepo := data.NewProjectsRepo(dataData, logger) - workflowUseCase := biz.NewWorkflowUsecase(workflowRepo, projectsRepo, workflowContractUseCase, auditorUseCase, logger) + workflowUseCase := biz.NewWorkflowUsecase(workflowRepo, projectsRepo, workflowContractUseCase, auditorUseCase, membershipUseCase, logger) workflowRunRepo := data.NewWorkflowRunRepo(dataData, logger) signingUseCase, err := biz.NewChainloopSigningUseCase(bootstrap, logger) if err != nil { diff --git a/app/controlplane/pkg/biz/workflow.go b/app/controlplane/pkg/biz/workflow.go index a15979f14..670612768 100644 --- a/app/controlplane/pkg/biz/workflow.go +++ b/app/controlplane/pkg/biz/workflow.go @@ -72,6 +72,9 @@ type WorkflowCreateOpts struct { // Public means that the associated workflow runs, attestations and materials // are reachable by other users, regardless of their organization Public bool + + // Owner identifies the user to be marked as owner of the project + Owner *uuid.UUID } type WorkflowUpdateOpts struct { @@ -102,15 +105,16 @@ type WorkflowListOpts struct { } type WorkflowUseCase struct { - wfRepo WorkflowRepo - projectRepo ProjectsRepo - contractUC *WorkflowContractUseCase - auditorUC *AuditorUseCase - logger *log.Helper + wfRepo WorkflowRepo + projectRepo ProjectsRepo + contractUC *WorkflowContractUseCase + auditorUC *AuditorUseCase + membershipUC *MembershipUseCase + logger *log.Helper } -func NewWorkflowUsecase(wfr WorkflowRepo, projectsRepo ProjectsRepo, schemaUC *WorkflowContractUseCase, auditorUC *AuditorUseCase, logger log.Logger) *WorkflowUseCase { - return &WorkflowUseCase{wfRepo: wfr, contractUC: schemaUC, projectRepo: projectsRepo, auditorUC: auditorUC, logger: log.NewHelper(logger)} +func NewWorkflowUsecase(wfr WorkflowRepo, projectsRepo ProjectsRepo, schemaUC *WorkflowContractUseCase, auditorUC *AuditorUseCase, membershipUC *MembershipUseCase, logger log.Logger) *WorkflowUseCase { + return &WorkflowUseCase{wfRepo: wfr, contractUC: schemaUC, projectRepo: projectsRepo, auditorUC: auditorUC, membershipUC: membershipUC, logger: log.NewHelper(logger)} } func (uc *WorkflowUseCase) Create(ctx context.Context, opts *WorkflowCreateOpts) (*Workflow, error) { @@ -151,6 +155,13 @@ func (uc *WorkflowUseCase) Create(ctx context.Context, opts *WorkflowCreateOpts) return nil, fmt.Errorf("failed to create workflow: %w", err) } + // Set project admin if a new project has been created + if opts.Owner != nil { + if err = uc.membershipUC.SetProjectOwner(ctx, wf.ProjectID, *opts.Owner); err != nil { + return nil, fmt.Errorf("failed to set project owner: %w", err) + } + } + orgUUID, err := uuid.Parse(opts.OrgID) if err != nil { uc.logger.Warn("failed to parse org id", "err", err) diff --git a/app/controlplane/pkg/data/ent/membership/membership.go b/app/controlplane/pkg/data/ent/membership/membership.go index 0c32936fb..80ba909a0 100644 --- a/app/controlplane/pkg/data/ent/membership/membership.go +++ b/app/controlplane/pkg/data/ent/membership/membership.go @@ -104,7 +104,7 @@ var ( // RoleValidator is a validator for the "role" field enum values. It is called by the builders before save. func RoleValidator(r authz.Role) error { switch r { - case "role:org:owner", "role:org:admin", "role:org:viewer": + case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer": return nil default: return fmt.Errorf("membership: invalid enum value for role field: %q", r) diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index c9d4f1b8d..6f84c97f4 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -222,7 +222,7 @@ var ( {Name: "current", Type: field.TypeBool, Default: false}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "updated_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, - {Name: "role", Type: field.TypeEnum, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer"}}, + {Name: "role", Type: field.TypeEnum, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer"}}, {Name: "membership_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"user", "group"}}, {Name: "member_id", Type: field.TypeUUID, Nullable: true}, {Name: "resource_type", Type: field.TypeEnum, Nullable: true, Enums: []string{"organization", "project"}}, @@ -269,7 +269,7 @@ var ( {Name: "status", Type: field.TypeEnum, Enums: []string{"accepted", "pending"}, Default: "pending"}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "deleted_at", Type: field.TypeTime, Nullable: true}, - {Name: "role", Type: field.TypeEnum, Nullable: true, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer"}}, + {Name: "role", Type: field.TypeEnum, Nullable: true, Enums: []string{"role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer"}}, {Name: "organization_id", Type: field.TypeUUID}, {Name: "sender_id", Type: field.TypeUUID}, } diff --git a/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go b/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go index 6406ac6e5..728230de1 100644 --- a/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go +++ b/app/controlplane/pkg/data/ent/orginvitation/orginvitation.go @@ -98,7 +98,7 @@ func StatusValidator(s biz.OrgInvitationStatus) error { // RoleValidator is a validator for the "role" field enum values. It is called by the builders before save. func RoleValidator(r authz.Role) error { switch r { - case "role:org:owner", "role:org:admin", "role:org:viewer": + case "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:project:admin", "role:project:viewer": return nil default: return fmt.Errorf("orginvitation: invalid enum value for role field: %q", r) diff --git a/app/controlplane/pkg/data/membership.go b/app/controlplane/pkg/data/membership.go index d1cb4cda7..944a7a1fc 100644 --- a/app/controlplane/pkg/data/membership.go +++ b/app/controlplane/pkg/data/membership.go @@ -145,19 +145,6 @@ func (r *MembershipRepo) FindByIDInUser(ctx context.Context, userID, membershipI return entMembershipToBiz(m), nil } -func (r *MembershipRepo) ListAllByUser(ctx context.Context, userID uuid.UUID) ([]*biz.Membership, error) { - mm, err := r.data.DB.Membership.Query().Where( - membership.MembershipTypeEQ(authz.MembershipTypeUser), - membership.MemberID(userID), - ).All(ctx) - - if err != nil { - return nil, fmt.Errorf("failed to query memberships: %v", err) - } - - return entMembershipsToBiz(mm), nil -} - func (r *MembershipRepo) FindByIDInOrg(ctx context.Context, orgID, membershipID uuid.UUID) (*biz.Membership, error) { m, err := r.data.DB.Membership.Query().Where( membership.MembershipTypeEQ(authz.MembershipTypeUser), @@ -225,6 +212,54 @@ func (r *MembershipRepo) Delete(ctx context.Context, id uuid.UUID) error { return r.data.DB.Membership.DeleteOneID(id).Exec(ctx) } +// RBAC methods + +func (r *MembershipRepo) ListAllByUser(ctx context.Context, userID uuid.UUID) ([]*biz.Membership, error) { + mm, err := r.data.DB.Membership.Query().Where( + membership.MembershipTypeEQ(authz.MembershipTypeUser), + membership.MemberID(userID), + ).All(ctx) + + if err != nil { + return nil, fmt.Errorf("failed to query memberships: %w", err) + } + + return entMembershipsToBiz(mm), nil +} + +func (r *MembershipRepo) ListAllByResource(ctx context.Context, rt authz.ResourceType, id uuid.UUID) ([]*biz.Membership, error) { + mm, err := r.data.DB.Membership.Query().Where( + membership.ResourceTypeEQ(rt), + membership.ResourceID(id), + ).All(ctx) + + if err != nil { + return nil, fmt.Errorf("failed to query memberships: %w", err) + } + + return entMembershipsToBiz(mm), nil +} + +func (r *MembershipRepo) AddResourceRole(ctx context.Context, resourceType authz.ResourceType, resID uuid.UUID, mType authz.MembershipType, memberID uuid.UUID, role authz.Role) error { + err := r.data.DB.Membership.Create(). + SetMembershipType(mType). + SetMemberID(memberID). + SetResourceType(resourceType). + SetResourceID(resID). + SetRole(role).Exec(ctx) + + if err != nil { + if !ent.IsConstraintError(err) { + return fmt.Errorf("failed to add resource role: %w", err) + } + + // combination already existed, ignore error + return nil + } + + return nil +} + func entMembershipsToBiz(memberships []*ent.Membership) []*biz.Membership { result := make([]*biz.Membership, 0, len(memberships)) for _, m := range memberships { From 0e3c04e4c2798e820ac806d1d55abd1e5fcc997d Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 14:10:56 +0200 Subject: [PATCH 09/48] att init complete Signed-off-by: Jose I. Paris --- app/cli/internal/action/attestation_init.go | 28 +++++++++++-------- .../internal/service/attestation.go | 20 ++++++------- app/controlplane/internal/service/service.go | 18 ++++++++++++ app/controlplane/internal/service/workflow.go | 6 ++++ app/controlplane/pkg/authz/authz.go | 10 +++++-- 5 files changed, 58 insertions(+), 24 deletions(-) diff --git a/app/cli/internal/action/attestation_init.go b/app/cli/internal/action/attestation_init.go index 9e4a9515c..5ed27d467 100644 --- a/app/cli/internal/action/attestation_init.go +++ b/app/cli/internal/action/attestation_init.go @@ -104,20 +104,24 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun // 0 - find or create the contract if we are creating the workflow (if any) contractRef := opts.NewWorkflowContractRef _, err := NewWorkflowDescribe(action.ActionsOpts).Run(ctx, opts.WorkflowName, opts.ProjectName) - if err != nil && status.Code(err) == codes.NotFound { - // Not found, let's see if we need to create the contract - if contractRef != "" { - // Try to find it by name - _, err := NewWorkflowContractDescribe(action.ActionsOpts).Run(contractRef, 0) - // An invalid argument might be raised if we use a file or URL in the "name" field, which must be DNS-1123 - // TODO: validate locally before doing the query - if err != nil && (status.Code(err) == codes.NotFound || status.Code(err) == codes.InvalidArgument) { - createResp, err := NewWorkflowContractCreate(action.ActionsOpts).Run(fmt.Sprintf("%s-%s", opts.ProjectName, opts.WorkflowName), nil, contractRef) - if err != nil { - return "", err + if err != nil { + if status.Code(err) == codes.NotFound { + // Not found, let's see if we need to create the contract + if contractRef != "" { + // Try to find it by name + _, err := NewWorkflowContractDescribe(action.ActionsOpts).Run(contractRef, 0) + // An invalid argument might be raised if we use a file or URL in the "name" field, which must be DNS-1123 + // TODO: validate locally before doing the query + if err != nil && (status.Code(err) == codes.NotFound || status.Code(err) == codes.InvalidArgument) { + createResp, err := NewWorkflowContractCreate(action.ActionsOpts).Run(fmt.Sprintf("%s-%s", opts.ProjectName, opts.WorkflowName), nil, contractRef) + if err != nil { + return "", err + } + contractRef = createResp.Name } - contractRef = createResp.Name } + } else { + return "", err } } diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 3cb25b724..898e764d0 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -154,6 +154,12 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer return nil, handleUseCaseErr(err, s.log) } + // Apply RBAC on the project + // try to load project and apply RBAC if needed + if err = s.userHasPermissionOnProject(ctx, s.projectUseCase, org.ID, req.ProjectName, authz.PolicyWorkflowRunCreate); err != nil { + return nil, err + } + // Find contract revision contractVersion, err := s.workflowContractUseCase.Describe(ctx, wf.OrgID.String(), wf.ContractID.String(), int(req.ContractRevision), biz.WithoutReferences()) if err != nil || contractVersion == nil { @@ -649,15 +655,8 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP } // try to load project and apply RBAC if needed - p, err := s.projectUseCase.FindProjectByReference(ctx, apiToken.OrgID, &biz.EntityRef{Name: req.ProjectName}) - if err != nil { - if !biz.IsNotFound(err) { - return nil, handleUseCaseErr(err, s.log) - } - } else if p != nil { - if err = s.authorizeResource(ctx, authz.PolicyWorkflowCreate, authz.ResourceTypeProject, p.ID); err != nil { - return nil, err - } + if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); err != nil { + return nil, err } if wf, err := s.workflowUseCase.FindByNameInOrg(ctx, apiToken.OrgID, req.GetProjectName(), req.GetWorkflowName()); err != nil { @@ -677,7 +676,8 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP } // set project owner if RBAC is enabled - if user := entities.CurrentUser(ctx); user != nil && rbacEnabled(ctx) { + if rbacEnabled(ctx) { + user := entities.CurrentUser(ctx) userID, err := uuid.Parse(user.ID) if err != nil { return nil, handleUseCaseErr(err, s.log) diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 0dff2f77b..9dc0d347c 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -168,6 +168,24 @@ func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resou return errors.Forbidden("forbidden", "user not authorized") } +func (s *service) userHasPermissionOnProject(ctx context.Context, pUC *biz.ProjectUseCase, orgID string, pName string, policy *authz.Policy) error { + if !rbacEnabled(ctx) { + return nil + } + p, err := pUC.FindProjectByReference(ctx, orgID, &biz.EntityRef{Name: pName}) + if err != nil { + if !biz.IsNotFound(err) { + return handleUseCaseErr(err, s.log) + } + } else if p != nil { + if err = s.authorizeResource(ctx, policy, authz.ResourceTypeProject, p.ID); err != nil { + return err + } + } + + return nil +} + func rbacEnabled(ctx context.Context) bool { return usercontext.CurrentAuthzSubject(ctx) == string(authz.RoleOrgMember) } diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index f9c56f8cd..d1c1a5db3 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -21,6 +21,7 @@ import ( "time" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/jsonfilter/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/pkg/jsonfilter" pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" @@ -235,6 +236,11 @@ func (s *WorkflowService) View(ctx context.Context, req *pb.WorkflowServiceViewR return nil, err } + // try to load project and apply RBAC if needed + if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.ProjectName, authz.PolicyWorkflowRead); err != nil { + return nil, err + } + var wf *biz.Workflow wf, err = s.useCase.FindByNameInOrg(ctx, currentOrg.ID, req.ProjectName, req.Name) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index cf22eef9d..c549747ff 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -116,8 +116,9 @@ var ( PolicyWorkflowContractUpdate = &Policy{ResourceWorkflowContract, ActionUpdate} PolicyWorkflowContractCreate = &Policy{ResourceWorkflowContract, ActionCreate} // WorkflowRun - PolicyWorkflowRunList = &Policy{ResourceWorkflowRun, ActionList} - PolicyWorkflowRunRead = &Policy{ResourceWorkflowRun, ActionRead} + PolicyWorkflowRunList = &Policy{ResourceWorkflowRun, ActionList} + PolicyWorkflowRunRead = &Policy{ResourceWorkflowRun, ActionRead} + PolicyWorkflowRunCreate = &Policy{ResourceWorkflowRun, ActionCreate} // Workflow PolicyWorkflowList = &Policy{ResourceWorkflow, ActionList} PolicyWorkflowRead = &Policy{ResourceWorkflow, ActionRead} @@ -176,15 +177,20 @@ var rolesMap = map[Role][]*Policy{ // + all the policies from the viewer role inherited automatically }, RoleOrgMember: { + // Allowed endpoints. RBAC will be applied where needed PolicyProjectList, PolicyProjectRead, PolicyProjectCreate, PolicyProjectUpdate, PolicyProjectDelete, + PolicyWorkflowRead, PolicyWorkflowContractCreate, }, RoleProjectAdmin: { + // RBAC will be applied in all these + PolicyWorkflowRead, PolicyWorkflowCreate, + PolicyWorkflowRunCreate, }, } From 7d49b6262dee21262cd65f4dbfb9e86ab41cf33a Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 14:18:53 +0200 Subject: [PATCH 10/48] remove unused roles Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index c549747ff..fb74bfef5 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -126,14 +126,6 @@ var ( // User Membership PolicyOrganizationRead = &Policy{Organization, ActionRead} - - // Projects - - PolicyProjectList = &Policy{ResourceProject, ActionList} - PolicyProjectRead = &Policy{ResourceProject, ActionRead} - PolicyProjectCreate = &Policy{ResourceProject, ActionCreate} - PolicyProjectUpdate = &Policy{ResourceProject, ActionUpdate} - PolicyProjectDelete = &Policy{ResourceProject, ActionDelete} ) // List of policies for each role @@ -178,11 +170,6 @@ var rolesMap = map[Role][]*Policy{ }, RoleOrgMember: { // Allowed endpoints. RBAC will be applied where needed - PolicyProjectList, - PolicyProjectRead, - PolicyProjectCreate, - PolicyProjectUpdate, - PolicyProjectDelete, PolicyWorkflowRead, PolicyWorkflowContractCreate, }, From b3d802e17aba39d037017151caf75462993b5c52 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 14:35:24 +0200 Subject: [PATCH 11/48] reset cache Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 5 +++++ .../internal/usercontext/currentorganization_middleware.go | 4 ++++ app/controlplane/pkg/authz/authz.go | 1 + 3 files changed, 10 insertions(+) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 898e764d0..bd6a669c5 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -690,5 +690,10 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP return nil, handleUseCaseErr(err, s.log) } + // reset cache, since we might have created a new project + if rbacEnabled(ctx) { + usercontext.ResetMembershipsCache() + } + return &cpAPI.FindOrCreateWorkflowResponse{Result: bizWorkflowToPb(wf)}, nil } diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware.go b/app/controlplane/internal/usercontext/currentorganization_middleware.go index e7a06dadc..6f3667201 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware.go @@ -114,6 +114,10 @@ func setCurrentMembershipsForUser(ctx context.Context, u *entities.User, members return entities.WithMembership(ctx, membership), nil } +func ResetMembershipsCache() { + membershipsCache.Purge() +} + func setCurrentOrganizationFromHeader(ctx context.Context, user *entities.User, orgName string, userUC biz.UserOrgFinder) (context.Context, error) { membership, err := userUC.MembershipInOrg(ctx, user.ID, orgName) if err != nil { diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index fb74bfef5..0bffb71de 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -171,6 +171,7 @@ var rolesMap = map[Role][]*Policy{ RoleOrgMember: { // Allowed endpoints. RBAC will be applied where needed PolicyWorkflowRead, + PolicyWorkflowContractRead, PolicyWorkflowContractCreate, }, RoleProjectAdmin: { From 23bd36bf8df20969bdebf8b96d1fb5d6eec67345 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 14:54:10 +0200 Subject: [PATCH 12/48] rbac on att push Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index bd6a669c5..6ad374c37 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -155,7 +155,6 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer } // Apply RBAC on the project - // try to load project and apply RBAC if needed if err = s.userHasPermissionOnProject(ctx, s.projectUseCase, org.ID, req.ProjectName, authz.PolicyWorkflowRunCreate); err != nil { return nil, err } @@ -233,6 +232,11 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe return nil, handleUseCaseErr(err, s.log) } + // Apply RBAC on the project + if err = s.userHasPermissionOnProject(ctx, s.projectUseCase, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { + return nil, err + } + wRun, err := s.wrUseCase.GetByIDInOrgOrPublic(ctx, robotAccount.OrgID, req.WorkflowRunId) if err != nil { return nil, handleUseCaseErr(err, s.log) From 9e1221dfa5b5e778346b936b2c09ffb313dfbf6a Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 17:03:04 +0200 Subject: [PATCH 13/48] add to att cancel Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 6ad374c37..3bbdcc99a 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -363,10 +363,16 @@ func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationS } // This will make sure the provided workflowRunID belongs to the org encoded in the robot account - if _, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", req.WorkflowRunId); err != nil { + wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", req.WorkflowRunId); + if err != nil { return nil, handleUseCaseErr(err, s.log) } + // Apply RBAC on the project + if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { + return nil, err + } + var status biz.WorkflowRunStatus switch req.Trigger { case cpAPI.AttestationServiceCancelRequest_TRIGGER_TYPE_FAILURE: From a4cb4e47e4e05de3315fb3f67275e4a8729b8039 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 17:08:04 +0200 Subject: [PATCH 14/48] update permission to cancel attestations Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 4 ++-- app/controlplane/pkg/authz/authz.go | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 3bbdcc99a..e1351bef7 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -363,13 +363,13 @@ func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationS } // This will make sure the provided workflowRunID belongs to the org encoded in the robot account - wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", req.WorkflowRunId); + wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", req.WorkflowRunId) if err != nil { return nil, handleUseCaseErr(err, s.log) } // Apply RBAC on the project - if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { + if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunUpdate); err != nil { return nil, err } diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 0bffb71de..154930da9 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -119,6 +119,7 @@ var ( PolicyWorkflowRunList = &Policy{ResourceWorkflowRun, ActionList} PolicyWorkflowRunRead = &Policy{ResourceWorkflowRun, ActionRead} PolicyWorkflowRunCreate = &Policy{ResourceWorkflowRun, ActionCreate} + PolicyWorkflowRunUpdate = &Policy{ResourceWorkflowRun, ActionUpdate} // Workflow PolicyWorkflowList = &Policy{ResourceWorkflow, ActionList} PolicyWorkflowRead = &Policy{ResourceWorkflow, ActionRead} @@ -179,6 +180,7 @@ var rolesMap = map[Role][]*Policy{ PolicyWorkflowRead, PolicyWorkflowCreate, PolicyWorkflowRunCreate, + PolicyWorkflowRunUpdate, // to reset attestations }, } From bbfe593928e121758fc3cda7a655b66b0811ea84 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 18:34:09 +0200 Subject: [PATCH 15/48] workflows done Signed-off-by: Jose I. Paris --- .../internal/service/attestation.go | 3 +- app/controlplane/internal/service/service.go | 34 +++++++++++++++---- app/controlplane/internal/service/workflow.go | 33 ++++++++++++++++++ app/controlplane/pkg/authz/authz.go | 19 +++++++++-- app/controlplane/pkg/biz/workflow.go | 2 ++ app/controlplane/pkg/data/project.go | 19 ++++++----- app/controlplane/pkg/data/workflow.go | 4 +++ 7 files changed, 97 insertions(+), 17 deletions(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index e1351bef7..d077dd298 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -665,7 +665,8 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP } // try to load project and apply RBAC if needed - if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); err != nil { + if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); !biz.IsNotFound(err) { + // Only return the error when the project exists. Otherwise, we will create the project return nil, err } diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 9dc0d347c..9839c0fc0 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -172,18 +172,40 @@ func (s *service) userHasPermissionOnProject(ctx context.Context, pUC *biz.Proje if !rbacEnabled(ctx) { return nil } + p, err := pUC.FindProjectByReference(ctx, orgID, &biz.EntityRef{Name: pName}) if err != nil { - if !biz.IsNotFound(err) { - return handleUseCaseErr(err, s.log) - } - } else if p != nil { - if err = s.authorizeResource(ctx, policy, authz.ResourceTypeProject, p.ID); err != nil { + // communicate not found error to the caller + if biz.IsNotFound(err) { return err } + return handleUseCaseErr(err, s.log) + } + + return s.authorizeResource(ctx, policy, authz.ResourceTypeProject, p.ID) +} + +func (s *service) visibleProjects(ctx context.Context, policy *authz.Policy) ([]uuid.UUID, error) { + if !rbacEnabled(ctx) { + return nil, nil + } + + projects := make([]uuid.UUID, 0) + + m := entities.CurrentMembership(ctx) + for _, rm := range m.Resources { + if rm.ResourceType == authz.ResourceTypeProject { + pass, err := s.enforcer.Enforce(string(rm.Role), policy) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + if pass { + projects = append(projects, rm.ResourceID) + } + } } - return nil + return projects, nil } func rbacEnabled(ctx context.Context) bool { diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index d1c1a5db3..258945c07 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -21,6 +21,7 @@ import ( "time" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/jsonfilter/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/pkg/jsonfilter" @@ -58,6 +59,11 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre return nil, err } + if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); !biz.IsNotFound(err) { + // Only return the error when the project exists. Otherwise, we will create the project + return nil, err + } + createOpts := &biz.WorkflowCreateOpts{ OrgID: currentOrg.ID, Name: req.GetName(), @@ -68,6 +74,16 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre Public: req.GetPublic(), } + // add current user as the owner of the project in case it needs to be created + if rbacEnabled(ctx) { + user := entities.CurrentUser(ctx) + userID, err := uuid.Parse(user.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + createOpts.Owner = &userID + } + p, err := s.useCase.Create(ctx, createOpts) if err != nil { return nil, handleUseCaseErr(err, s.log) @@ -82,6 +98,10 @@ func (s *WorkflowService) Update(ctx context.Context, req *pb.WorkflowServiceUpd return nil, err } + if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowUpdate); err != nil { + return nil, err + } + wf, err := s.useCase.FindByNameInOrg(ctx, currentOrg.ID, req.ProjectName, req.Name) if err != nil { return nil, handleUseCaseErr(err, s.log) @@ -196,6 +216,15 @@ func (s *WorkflowService) List(ctx context.Context, req *pb.WorkflowServiceListR filters.WorkflowActiveWindow = timeWindow } + if rbacEnabled(ctx) { + // filter by visible projects + projectIDs, err := s.visibleProjects(ctx, authz.PolicyWorkflowRead) + if err != nil { + return nil, err + } + filters.VisibleProjectsFromRBAC = projectIDs + } + workflows, count, err := s.useCase.List(ctx, currentOrg.ID, filters, paginationOpts) if err != nil { return nil, handleUseCaseErr(err, s.log) @@ -218,6 +247,10 @@ func (s *WorkflowService) Delete(ctx context.Context, req *pb.WorkflowServiceDel return nil, err } + if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowDelete); err != nil { + return nil, err + } + wf, err := s.useCase.FindByNameInOrg(ctx, currentOrg.ID, req.ProjectName, req.Name) if err != nil { return nil, handleUseCaseErr(err, s.log) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 154930da9..00b9c49f4 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -124,7 +124,8 @@ var ( PolicyWorkflowList = &Policy{ResourceWorkflow, ActionList} PolicyWorkflowRead = &Policy{ResourceWorkflow, ActionRead} PolicyWorkflowCreate = &Policy{ResourceWorkflow, ActionCreate} - + PolicyWorkflowUpdate = &Policy{ResourceWorkflow, ActionUpdate} + PolicyWorkflowDelete = &Policy{ResourceWorkflow, ActionDelete} // User Membership PolicyOrganizationRead = &Policy{Organization, ActionRead} ) @@ -174,13 +175,25 @@ var rolesMap = map[Role][]*Policy{ PolicyWorkflowRead, PolicyWorkflowContractRead, PolicyWorkflowContractCreate, + + PolicyWorkflowList, + PolicyWorkflowCreate, + PolicyWorkflowUpdate, + PolicyWorkflowDelete, }, + // RoleProjectAdmin: RBAC will be applied in all these RoleProjectAdmin: { - // RBAC will be applied in all these + // attestations + PolicyWorkflowRead, PolicyWorkflowCreate, PolicyWorkflowRunCreate, PolicyWorkflowRunUpdate, // to reset attestations + + // workflow operations + + PolicyWorkflowUpdate, + PolicyWorkflowDelete, }, } @@ -218,6 +231,8 @@ var ServerOperationsMap = map[string][]*Policy{ "/controlplane.v1.WorkflowService/List": {PolicyWorkflowList}, "/controlplane.v1.WorkflowService/View": {PolicyWorkflowRead}, "/controlplane.v1.WorkflowService/Create": {PolicyWorkflowCreate}, + "/controlplane.v1.WorkflowService/Update": {PolicyWorkflowUpdate}, + "/controlplane.v1.WorkflowService/Delete": {PolicyWorkflowDelete}, // WorkflowRun "/controlplane.v1.WorkflowRunService/List": {PolicyWorkflowRunList}, "/controlplane.v1.WorkflowRunService/View": {PolicyWorkflowRunRead}, diff --git a/app/controlplane/pkg/biz/workflow.go b/app/controlplane/pkg/biz/workflow.go index 670612768..60da3b605 100644 --- a/app/controlplane/pkg/biz/workflow.go +++ b/app/controlplane/pkg/biz/workflow.go @@ -102,6 +102,8 @@ type WorkflowListOpts struct { WorkflowRunLastStatus WorkflowRunStatus // JSONFilters is the filters to apply to the JSON fields JSONFilters []*jsonfilter.JSONFilter + // + VisibleProjectsFromRBAC []uuid.UUID } type WorkflowUseCase struct { diff --git a/app/controlplane/pkg/data/project.go b/app/controlplane/pkg/data/project.go index 89980f3ed..5e3fe759d 100644 --- a/app/controlplane/pkg/data/project.go +++ b/app/controlplane/pkg/data/project.go @@ -17,6 +17,7 @@ package data import ( "context" + "fmt" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent" @@ -42,10 +43,11 @@ func NewProjectsRepo(data *Data, logger log.Logger) biz.ProjectsRepo { // FindProjectByOrgIDAndName gets a project by organization ID and project name func (r *ProjectRepo) FindProjectByOrgIDAndName(ctx context.Context, orgID uuid.UUID, projectName string) (*biz.Project, error) { pro, err := r.data.DB.Organization.Query().Where(organization.ID(orgID)).QueryProjects().Where(project.Name(projectName)).Only(ctx) - if err != nil && !ent.IsNotFound(err) { - return nil, err - } else if pro == nil { - return nil, nil + if err != nil { + if ent.IsNotFound(err) { + return nil, biz.NewErrNotFound(fmt.Sprintf("project %s", projectName)) + } + return nil, fmt.Errorf("project query failed: %w", err) } return entProjectToBiz(pro), nil @@ -54,10 +56,11 @@ func (r *ProjectRepo) FindProjectByOrgIDAndName(ctx context.Context, orgID uuid. // FindProjectByOrgIDAndID gets a project by organization ID and project ID func (r *ProjectRepo) FindProjectByOrgIDAndID(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID) (*biz.Project, error) { pro, err := r.data.DB.Organization.Query().Where(organization.ID(orgID)).QueryProjects().Where(project.ID(projectID)).Only(ctx) - if err != nil && !ent.IsNotFound(err) { - return nil, err - } else if pro == nil { - return nil, nil + if err != nil { + if ent.IsNotFound(err) { + return nil, biz.NewErrNotFound(fmt.Sprintf("project %s", projectID.String())) + } + return nil, fmt.Errorf("project query failed: %w", err) } return entProjectToBiz(pro), nil diff --git a/app/controlplane/pkg/data/workflow.go b/app/controlplane/pkg/data/workflow.go index e6e16654c..25b7efe6f 100644 --- a/app/controlplane/pkg/data/workflow.go +++ b/app/controlplane/pkg/data/workflow.go @@ -212,6 +212,10 @@ func applyWorkflowFilters(wfQuery *ent.WorkflowQuery, opts *biz.WorkflowListOpts wfQuery = wfQuery.Where(workflow.Public(*opts.WorkflowPublic)) } + if opts.VisibleProjectsFromRBAC != nil { + wfQuery = wfQuery.Where(workflow.ProjectIDIn(opts.VisibleProjectsFromRBAC...)) + } + // Updated at on Workflows is only updated when a new workflow run is referenced meaning // a workflow run is started if opts.WorkflowActiveWindow != nil { From 5366022802363698eb0c93479efb1ffd89cfa688 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 18:34:34 +0200 Subject: [PATCH 16/48] lint Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 00b9c49f4..d698070e5 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -191,7 +191,7 @@ var rolesMap = map[Role][]*Policy{ PolicyWorkflowRunUpdate, // to reset attestations // workflow operations - + PolicyWorkflowUpdate, PolicyWorkflowDelete, }, From f6276876c9dc7d5e96a44ced302ec5143a1ff886 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 18:56:03 +0200 Subject: [PATCH 17/48] fix error management Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 2 +- app/controlplane/internal/service/service.go | 4 ---- app/controlplane/internal/service/workflow.go | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index d077dd298..daba1c80c 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -665,7 +665,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP } // try to load project and apply RBAC if needed - if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); !biz.IsNotFound(err) { + if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); !errors.IsNotFound(err) { // Only return the error when the project exists. Otherwise, we will create the project return nil, err } diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 9839c0fc0..1aff47aec 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -175,10 +175,6 @@ func (s *service) userHasPermissionOnProject(ctx context.Context, pUC *biz.Proje p, err := pUC.FindProjectByReference(ctx, orgID, &biz.EntityRef{Name: pName}) if err != nil { - // communicate not found error to the caller - if biz.IsNotFound(err) { - return err - } return handleUseCaseErr(err, s.log) } diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index 258945c07..604811aec 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -59,7 +59,7 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre return nil, err } - if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); !biz.IsNotFound(err) { + if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); !errors.IsNotFound(err) { // Only return the error when the project exists. Otherwise, we will create the project return nil, err } From d62dc449280a218f126fcdfd931580e82979eedc Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 18:58:19 +0200 Subject: [PATCH 18/48] simplify Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/workflow.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index 604811aec..0a96da2d0 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -216,14 +216,12 @@ func (s *WorkflowService) List(ctx context.Context, req *pb.WorkflowServiceListR filters.WorkflowActiveWindow = timeWindow } - if rbacEnabled(ctx) { - // filter by visible projects - projectIDs, err := s.visibleProjects(ctx, authz.PolicyWorkflowRead) - if err != nil { - return nil, err - } - filters.VisibleProjectsFromRBAC = projectIDs + // filter by visible projects if RBAC is enabled + projectIDs, err := s.visibleProjects(ctx, authz.PolicyWorkflowRead) + if err != nil { + return nil, err } + filters.VisibleProjectsFromRBAC = projectIDs workflows, count, err := s.useCase.List(ctx, currentOrg.ID, filters, paginationOpts) if err != nil { From 683b499a9a724820798278c97d91313103272dda Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 19:09:20 +0200 Subject: [PATCH 19/48] workflow run ls Signed-off-by: Jose I. Paris --- .../internal/service/workflowrun.go | 7 +++++++ app/controlplane/pkg/authz/authz.go | 5 +++++ app/controlplane/pkg/biz/workflowrun.go | 7 ++++--- app/controlplane/pkg/data/workflowrun.go | 18 +++++++++++------- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/controlplane/internal/service/workflowrun.go b/app/controlplane/internal/service/workflowrun.go index c9b6aee79..72dadd131 100644 --- a/app/controlplane/internal/service/workflowrun.go +++ b/app/controlplane/internal/service/workflowrun.go @@ -21,6 +21,7 @@ import ( pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" craftingpb "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/chainloop-dev/chainloop/pkg/credentials" @@ -66,6 +67,12 @@ func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServic // Configure filters filters := &biz.RunListFilters{} + projectIDs, err := s.visibleProjects(ctx, authz.PolicyWorkflowRunRead) + if err != nil { + return nil, err + } + filters.VisibleProjectsFromRBAC = projectIDs + // by workflow if req.GetWorkflowName() != "" && req.GetProjectName() != "" { wf, err := s.workflowUseCase.FindByNameInOrg(ctx, currentOrg.ID, req.GetProjectName(), req.GetWorkflowName()) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index d698070e5..1e0bb16e3 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -180,6 +180,8 @@ var rolesMap = map[Role][]*Policy{ PolicyWorkflowCreate, PolicyWorkflowUpdate, PolicyWorkflowDelete, + + PolicyWorkflowRunList, }, // RoleProjectAdmin: RBAC will be applied in all these RoleProjectAdmin: { @@ -194,6 +196,9 @@ var rolesMap = map[Role][]*Policy{ PolicyWorkflowUpdate, PolicyWorkflowDelete, + + // workflow runs + PolicyWorkflowRunRead, }, } diff --git a/app/controlplane/pkg/biz/workflowrun.go b/app/controlplane/pkg/biz/workflowrun.go index 43bd57612..90e077d98 100644 --- a/app/controlplane/pkg/biz/workflowrun.go +++ b/app/controlplane/pkg/biz/workflowrun.go @@ -327,9 +327,10 @@ func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, en } type RunListFilters struct { - WorkflowID *uuid.UUID - VersionID *uuid.UUID - Status WorkflowRunStatus + WorkflowID *uuid.UUID + VersionID *uuid.UUID + Status WorkflowRunStatus + VisibleProjectsFromRBAC []uuid.UUID } // List the workflowruns associated with an org and optionally filtered by a workflow diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index 7058693ab..089be0f75 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -25,6 +25,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/attestation" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowrun" @@ -233,11 +234,17 @@ func (r *WorkflowRunRepo) List(ctx context.Context, orgID uuid.UUID, filters *bi return nil, "", errors.New("pagination options is required") } - // query first for workflows to avoid joining the workflow_runs table - wfExist, err := r.data.DB.Workflow.Query().Where( + // workflow filters + wfPredicates := []predicate.Workflow{ workflow.DeletedAtIsNil(), workflow.OrganizationID(orgID), - ).Exist(ctx) + } + if filters.VisibleProjectsFromRBAC != nil { + wfPredicates = append(wfPredicates, workflow.ProjectIDIn(filters.VisibleProjectsFromRBAC...)) + } + + // query first for workflows to avoid joining the workflow_runs table + wfExist, err := r.data.DB.Workflow.Query().Where(wfPredicates...).Exist(ctx) if err != nil { return nil, "", fmt.Errorf("getting workflows: %w", err) } @@ -248,10 +255,7 @@ func (r *WorkflowRunRepo) List(ctx context.Context, orgID uuid.UUID, filters *bi // Query workflow runs by joining with workflows q := r.data.DB.WorkflowRun.Query().Where( - workflowrun.HasWorkflowWith( - workflow.OrganizationID(orgID), - workflow.DeletedAtIsNil(), - )). + workflowrun.HasWorkflowWith(wfPredicates...)). Order(ent.Desc(workflowrun.FieldCreatedAt)). WithWorkflowAndProject().WithVersion(). Limit(p.Limit + 1) From 908df4921ddda119618b85812dd155205153f632 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 19:15:22 +0200 Subject: [PATCH 20/48] wf run describe Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/workflowrun.go | 5 +++++ app/controlplane/pkg/authz/authz.go | 1 + 2 files changed, 6 insertions(+) diff --git a/app/controlplane/internal/service/workflowrun.go b/app/controlplane/internal/service/workflowrun.go index 72dadd131..91bfba3c3 100644 --- a/app/controlplane/internal/service/workflowrun.go +++ b/app/controlplane/internal/service/workflowrun.go @@ -151,6 +151,11 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic return nil, errors.BadRequest("invalid", "id or digest required") } + // Apply RBAC if needed + if err = s.authorizeResource(ctx, authz.PolicyWorkflowRunRead, authz.ResourceTypeProject, run.Workflow.ProjectID); err != nil { + return nil, err + } + var verificationResult *pb.WorkflowRunServiceViewResponse_VerificationResult if req.Verify { // it might be nil if it doesn't apply diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 1e0bb16e3..69e7e21e8 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -182,6 +182,7 @@ var rolesMap = map[Role][]*Policy{ PolicyWorkflowDelete, PolicyWorkflowRunList, + PolicyWorkflowRunRead, }, // RoleProjectAdmin: RBAC will be applied in all these RoleProjectAdmin: { From da278d51f85cb5517d75d113e24040b8e7c40b88 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 19:26:45 +0200 Subject: [PATCH 21/48] fix error check Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 2 +- app/controlplane/internal/service/workflow.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index daba1c80c..dec12c285 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -665,7 +665,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP } // try to load project and apply RBAC if needed - if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); !errors.IsNotFound(err) { + if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); err != nil && !errors.IsNotFound(err) { // Only return the error when the project exists. Otherwise, we will create the project return nil, err } diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index 0a96da2d0..3687687e3 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -59,7 +59,7 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre return nil, err } - if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); !errors.IsNotFound(err) { + if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); err != nil && !errors.IsNotFound(err) { // Only return the error when the project exists. Otherwise, we will create the project return nil, err } From f746d9d1a6c2ab9249653beb62e10e7a664b5360 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 20:16:03 +0200 Subject: [PATCH 22/48] remove change Signed-off-by: Jose I. Paris --- app/cli/internal/action/attestation_init.go | 28 +++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/app/cli/internal/action/attestation_init.go b/app/cli/internal/action/attestation_init.go index 5ed27d467..9e4a9515c 100644 --- a/app/cli/internal/action/attestation_init.go +++ b/app/cli/internal/action/attestation_init.go @@ -104,24 +104,20 @@ func (action *AttestationInit) Run(ctx context.Context, opts *AttestationInitRun // 0 - find or create the contract if we are creating the workflow (if any) contractRef := opts.NewWorkflowContractRef _, err := NewWorkflowDescribe(action.ActionsOpts).Run(ctx, opts.WorkflowName, opts.ProjectName) - if err != nil { - if status.Code(err) == codes.NotFound { - // Not found, let's see if we need to create the contract - if contractRef != "" { - // Try to find it by name - _, err := NewWorkflowContractDescribe(action.ActionsOpts).Run(contractRef, 0) - // An invalid argument might be raised if we use a file or URL in the "name" field, which must be DNS-1123 - // TODO: validate locally before doing the query - if err != nil && (status.Code(err) == codes.NotFound || status.Code(err) == codes.InvalidArgument) { - createResp, err := NewWorkflowContractCreate(action.ActionsOpts).Run(fmt.Sprintf("%s-%s", opts.ProjectName, opts.WorkflowName), nil, contractRef) - if err != nil { - return "", err - } - contractRef = createResp.Name + if err != nil && status.Code(err) == codes.NotFound { + // Not found, let's see if we need to create the contract + if contractRef != "" { + // Try to find it by name + _, err := NewWorkflowContractDescribe(action.ActionsOpts).Run(contractRef, 0) + // An invalid argument might be raised if we use a file or URL in the "name" field, which must be DNS-1123 + // TODO: validate locally before doing the query + if err != nil && (status.Code(err) == codes.NotFound || status.Code(err) == codes.InvalidArgument) { + createResp, err := NewWorkflowContractCreate(action.ActionsOpts).Run(fmt.Sprintf("%s-%s", opts.ProjectName, opts.WorkflowName), nil, contractRef) + if err != nil { + return "", err } + contractRef = createResp.Name } - } else { - return "", err } } From e12257d94963447257437a65a7a581cc2bf639db Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 20:31:58 +0200 Subject: [PATCH 23/48] apply suggestions Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 10 ++++------ app/controlplane/internal/service/service.go | 12 ++++-------- app/controlplane/internal/service/workflow.go | 8 ++++---- app/controlplane/internal/service/workflowrun.go | 4 ++-- app/controlplane/pkg/authz/membership.go | 5 +++-- app/controlplane/pkg/biz/workflow.go | 5 +++-- app/controlplane/pkg/biz/workflowrun.go | 8 ++++---- app/controlplane/pkg/data/workflow.go | 4 ++-- app/controlplane/pkg/data/workflowrun.go | 4 ++-- 9 files changed, 28 insertions(+), 32 deletions(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index dec12c285..2005a73b2 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -687,8 +687,8 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP } // set project owner if RBAC is enabled - if rbacEnabled(ctx) { - user := entities.CurrentUser(ctx) + user := entities.CurrentUser(ctx) + if user != nil { userID, err := uuid.Parse(user.ID) if err != nil { return nil, handleUseCaseErr(err, s.log) @@ -701,10 +701,8 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP return nil, handleUseCaseErr(err, s.log) } - // reset cache, since we might have created a new project - if rbacEnabled(ctx) { - usercontext.ResetMembershipsCache() - } + // reset RBAC cache, since we might have created a new project + usercontext.ResetMembershipsCache() return &cpAPI.FindOrCreateWorkflowResponse{Result: bizWorkflowToPb(wf)}, nil } diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 1aff47aec..3ba554fdf 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -181,8 +181,10 @@ func (s *service) userHasPermissionOnProject(ctx context.Context, pUC *biz.Proje return s.authorizeResource(ctx, policy, authz.ResourceTypeProject, p.ID) } -func (s *service) visibleProjects(ctx context.Context, policy *authz.Policy) ([]uuid.UUID, error) { +// visibleProjects returns projects where the user has any role (currently ProjectAdmin and ProjectViewer) +func (s *service) visibleProjects(ctx context.Context) ([]uuid.UUID, error) { if !rbacEnabled(ctx) { + // returning a NIL slice to denote that RBAC has not been applied, to differentiate from the empty slice case return nil, nil } @@ -191,13 +193,7 @@ func (s *service) visibleProjects(ctx context.Context, policy *authz.Policy) ([] m := entities.CurrentMembership(ctx) for _, rm := range m.Resources { if rm.ResourceType == authz.ResourceTypeProject { - pass, err := s.enforcer.Enforce(string(rm.Role), policy) - if err != nil { - return nil, handleUseCaseErr(err, s.log) - } - if pass { - projects = append(projects, rm.ResourceID) - } + projects = append(projects, rm.ResourceID) } } diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index 3687687e3..da5cc7362 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -75,8 +75,8 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre } // add current user as the owner of the project in case it needs to be created - if rbacEnabled(ctx) { - user := entities.CurrentUser(ctx) + user := entities.CurrentUser(ctx) + if user != nil { userID, err := uuid.Parse(user.ID) if err != nil { return nil, handleUseCaseErr(err, s.log) @@ -217,11 +217,11 @@ func (s *WorkflowService) List(ctx context.Context, req *pb.WorkflowServiceListR } // filter by visible projects if RBAC is enabled - projectIDs, err := s.visibleProjects(ctx, authz.PolicyWorkflowRead) + projectIDs, err := s.visibleProjects(ctx) if err != nil { return nil, err } - filters.VisibleProjectsFromRBAC = projectIDs + filters.ProjectIDs = projectIDs workflows, count, err := s.useCase.List(ctx, currentOrg.ID, filters, paginationOpts) if err != nil { diff --git a/app/controlplane/internal/service/workflowrun.go b/app/controlplane/internal/service/workflowrun.go index 91bfba3c3..c263dc6a0 100644 --- a/app/controlplane/internal/service/workflowrun.go +++ b/app/controlplane/internal/service/workflowrun.go @@ -67,11 +67,11 @@ func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServic // Configure filters filters := &biz.RunListFilters{} - projectIDs, err := s.visibleProjects(ctx, authz.PolicyWorkflowRunRead) + projectIDs, err := s.visibleProjects(ctx) if err != nil { return nil, err } - filters.VisibleProjectsFromRBAC = projectIDs + filters.ProjectIDs = projectIDs // by workflow if req.GetWorkflowName() != "" && req.GetProjectName() != "" { diff --git a/app/controlplane/pkg/authz/membership.go b/app/controlplane/pkg/authz/membership.go index 095df69a8..f59a102f9 100644 --- a/app/controlplane/pkg/authz/membership.go +++ b/app/controlplane/pkg/authz/membership.go @@ -15,9 +15,10 @@ package authz -// Polymorphic membership - +// MembershipType represents a polymorphic membership subject (user or group) type MembershipType string + +// ResourceType represent a membership resource (organizations, projects) type ResourceType string const ( diff --git a/app/controlplane/pkg/biz/workflow.go b/app/controlplane/pkg/biz/workflow.go index 60da3b605..ad400e793 100644 --- a/app/controlplane/pkg/biz/workflow.go +++ b/app/controlplane/pkg/biz/workflow.go @@ -102,8 +102,9 @@ type WorkflowListOpts struct { WorkflowRunLastStatus WorkflowRunStatus // JSONFilters is the filters to apply to the JSON fields JSONFilters []*jsonfilter.JSONFilter - // - VisibleProjectsFromRBAC []uuid.UUID + // ProjectIDs is used to filter the result by a project list + // Note that a `nil` value means "no filter", and an empty slice will cause an empty result + ProjectIDs []uuid.UUID } type WorkflowUseCase struct { diff --git a/app/controlplane/pkg/biz/workflowrun.go b/app/controlplane/pkg/biz/workflowrun.go index 90e077d98..440a05ba4 100644 --- a/app/controlplane/pkg/biz/workflowrun.go +++ b/app/controlplane/pkg/biz/workflowrun.go @@ -327,10 +327,10 @@ func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, en } type RunListFilters struct { - WorkflowID *uuid.UUID - VersionID *uuid.UUID - Status WorkflowRunStatus - VisibleProjectsFromRBAC []uuid.UUID + WorkflowID *uuid.UUID + VersionID *uuid.UUID + Status WorkflowRunStatus + ProjectIDs []uuid.UUID } // List the workflowruns associated with an org and optionally filtered by a workflow diff --git a/app/controlplane/pkg/data/workflow.go b/app/controlplane/pkg/data/workflow.go index 25b7efe6f..a8b8d6bbd 100644 --- a/app/controlplane/pkg/data/workflow.go +++ b/app/controlplane/pkg/data/workflow.go @@ -212,8 +212,8 @@ func applyWorkflowFilters(wfQuery *ent.WorkflowQuery, opts *biz.WorkflowListOpts wfQuery = wfQuery.Where(workflow.Public(*opts.WorkflowPublic)) } - if opts.VisibleProjectsFromRBAC != nil { - wfQuery = wfQuery.Where(workflow.ProjectIDIn(opts.VisibleProjectsFromRBAC...)) + if opts.ProjectIDs != nil { + wfQuery = wfQuery.Where(workflow.ProjectIDIn(opts.ProjectIDs...)) } // Updated at on Workflows is only updated when a new workflow run is referenced meaning diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index 089be0f75..8fd07a30c 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -239,8 +239,8 @@ func (r *WorkflowRunRepo) List(ctx context.Context, orgID uuid.UUID, filters *bi workflow.DeletedAtIsNil(), workflow.OrganizationID(orgID), } - if filters.VisibleProjectsFromRBAC != nil { - wfPredicates = append(wfPredicates, workflow.ProjectIDIn(filters.VisibleProjectsFromRBAC...)) + if filters.ProjectIDs != nil { + wfPredicates = append(wfPredicates, workflow.ProjectIDIn(filters.ProjectIDs...)) } // query first for workflows to avoid joining the workflow_runs table From ddf3908c8517222ad13eff20b748fd6b18476a7c Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 20:52:45 +0200 Subject: [PATCH 24/48] add workflow contract permissions for member role Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 69e7e21e8..fd7169c3b 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -127,7 +127,8 @@ var ( PolicyWorkflowUpdate = &Policy{ResourceWorkflow, ActionUpdate} PolicyWorkflowDelete = &Policy{ResourceWorkflow, ActionDelete} // User Membership - PolicyOrganizationRead = &Policy{Organization, ActionRead} + PolicyOrganizationRead = &Policy{Organization, ActionRead} + PolicyOrganizationListMemberships = &Policy{Organization, ActionRead} ) // List of policies for each role @@ -172,9 +173,13 @@ var rolesMap = map[Role][]*Policy{ }, RoleOrgMember: { // Allowed endpoints. RBAC will be applied where needed + PolicyOrganizationListMemberships, + PolicyWorkflowRead, + PolicyWorkflowContractList, PolicyWorkflowContractRead, PolicyWorkflowContractCreate, + PolicyWorkflowContractUpdate, PolicyWorkflowList, PolicyWorkflowCreate, @@ -260,6 +265,8 @@ var ServerOperationsMap = map[string][]*Policy{ // Leave the organization or delete your account "/controlplane.v1.UserService/DeleteMembership": {}, "/controlplane.v1.AuthService/DeleteAccount": {}, + + "/controlplane.v1.OrganizationService/ListMemberships": {PolicyOrganizationListMemberships}, } type SubjectAPIToken struct { From d3af96880fb25849009e295e70ec3b52b90f1c42 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 21:52:32 +0200 Subject: [PATCH 25/48] inject project use case in all services Signed-off-by: Jose I. Paris --- .../gen/frontend/google/protobuf/descriptor.ts | 16 ++++++++++------ app/controlplane/cmd/wire.go | 3 ++- app/controlplane/cmd/wire_gen.go | 6 +++--- app/controlplane/internal/service/service.go | 15 +++++++++++---- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts b/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts index d59b21da4..0d2d2fb32 100644 --- a/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts +++ b/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts @@ -30,7 +30,7 @@ export enum Edition { EDITION_2024 = 1001, /** * EDITION_1_TEST_ONLY - Placeholder editions for testing feature resolution. These should not be - * used or relied on outside of tests. + * used or relyed on outside of tests. */ EDITION_1_TEST_ONLY = 1, EDITION_2_TEST_ONLY = 2, @@ -875,13 +875,12 @@ export interface MessageOptions { export interface FieldOptions { /** - * NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead. * The ctype option instructs the C++ code generator to use a different * representation of the field than it normally would. See the specific * options below. This option is only implemented to support use of * [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of - * type "bytes" in the open source release. - * TODO: make ctype actually deprecated. + * type "bytes" in the open source release -- sorry, we'll try to include + * other types in a future version! */ ctype: FieldOptions_CType; /** @@ -1053,7 +1052,11 @@ export function fieldOptions_JSTypeToJSON(object: FieldOptions_JSType): string { } } -/** If set to RETENTION_SOURCE, the option will be omitted from the binary. */ +/** + * If set to RETENTION_SOURCE, the option will be omitted from the binary. + * Note: as of January 2023, support for this is in progress and does not yet + * have an effect (b/264593489). + */ export enum FieldOptions_OptionRetention { RETENTION_UNKNOWN = 0, RETENTION_RUNTIME = 1, @@ -1096,7 +1099,8 @@ export function fieldOptions_OptionRetentionToJSON(object: FieldOptions_OptionRe /** * This indicates the types of entities that the field may apply to when used * as an option. If it is unset, then the field may be freely used as an - * option on any kind of entity. + * option on any kind of entity. Note: as of January 2023, support for this is + * in progress and does not yet have an effect (b/264593489). */ export enum FieldOptions_OptionTargetType { TARGET_TYPE_UNKNOWN = 0, diff --git a/app/controlplane/cmd/wire.go b/app/controlplane/cmd/wire.go index b56b78105..8aa97c894 100644 --- a/app/controlplane/cmd/wire.go +++ b/app/controlplane/cmd/wire.go @@ -90,10 +90,11 @@ func newPolicyProviderConfig(in []*conf.PolicyProvider) []*policies.NewRegistryC return out } -func serviceOpts(l log.Logger, enforcer *authz.Enforcer) []service.NewOpt { +func serviceOpts(l log.Logger, enforcer *authz.Enforcer, pUC *biz.ProjectUseCase) []service.NewOpt { return []service.NewOpt{ service.WithLogger(l), service.WithEnforcer(enforcer), + service.WithProjectUseCase(pUC), } } diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 12a5b1611..57e041322 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -128,7 +128,7 @@ 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) + 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) @@ -328,8 +328,8 @@ func newPolicyProviderConfig(in []*conf.PolicyProvider) []*policies.NewRegistryC return out } -func serviceOpts(l log.Logger, enforcer *authz.Enforcer) []service.NewOpt { - return []service.NewOpt{service.WithLogger(l), service.WithEnforcer(enforcer)} +func serviceOpts(l log.Logger, enforcer *authz.Enforcer, pUC *biz.ProjectUseCase) []service.NewOpt { + return []service.NewOpt{service.WithLogger(l), service.WithEnforcer(enforcer), service.WithProjectUseCase(pUC)} } func newCASServerOptions(in *conf.Bootstrap_CASServer) *biz.CASServerDefaultOpts { diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 3ba554fdf..1f06c1004 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -127,8 +127,9 @@ func newService(opts ...NewOpt) *service { } type service struct { - log *log.Helper - enforcer *authz.Enforcer + log *log.Helper + enforcer *authz.Enforcer + projectUseCase *biz.ProjectUseCase } type NewOpt func(s *service) @@ -145,6 +146,12 @@ func WithEnforcer(enforcer *authz.Enforcer) NewOpt { } } +func WithProjectUseCase(projectUseCase *biz.ProjectUseCase) NewOpt { + return func(s *service) { + s.projectUseCase = projectUseCase + } +} + func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resourceType authz.ResourceType, resourceID uuid.UUID) error { if !rbacEnabled(ctx) { return nil @@ -168,12 +175,12 @@ func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resou return errors.Forbidden("forbidden", "user not authorized") } -func (s *service) userHasPermissionOnProject(ctx context.Context, pUC *biz.ProjectUseCase, orgID string, pName string, policy *authz.Policy) error { +func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, pName string, policy *authz.Policy) error { if !rbacEnabled(ctx) { return nil } - p, err := pUC.FindProjectByReference(ctx, orgID, &biz.EntityRef{Name: pName}) + p, err := s.projectUseCase.FindProjectByReference(ctx, orgID, &biz.EntityRef{Name: pName}) if err != nil { return handleUseCaseErr(err, s.log) } From 296a6bd4fdfb4f9bdc86e837a1874dfc32504c3a Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 21:53:58 +0200 Subject: [PATCH 26/48] fix calls Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 8 ++++---- app/controlplane/internal/service/workflow.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 2005a73b2..25660f3f3 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -155,7 +155,7 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer } // Apply RBAC on the project - if err = s.userHasPermissionOnProject(ctx, s.projectUseCase, org.ID, req.ProjectName, authz.PolicyWorkflowRunCreate); err != nil { + if err = s.userHasPermissionOnProject(ctx, org.ID, req.ProjectName, authz.PolicyWorkflowRunCreate); err != nil { return nil, err } @@ -233,7 +233,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe } // Apply RBAC on the project - if err = s.userHasPermissionOnProject(ctx, s.projectUseCase, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { + if err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { return nil, err } @@ -369,7 +369,7 @@ func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationS } // Apply RBAC on the project - if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunUpdate); err != nil { + if err := s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunUpdate); err != nil { return nil, err } @@ -665,7 +665,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP } // try to load project and apply RBAC if needed - if err := s.userHasPermissionOnProject(ctx, s.projectUseCase, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); err != nil && !errors.IsNotFound(err) { + if err := s.userHasPermissionOnProject(ctx, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); err != nil && !errors.IsNotFound(err) { // Only return the error when the project exists. Otherwise, we will create the project return nil, err } diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index da5cc7362..dc9ee9fc7 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -59,7 +59,7 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre return nil, err } - if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); err != nil && !errors.IsNotFound(err) { + if err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); err != nil && !errors.IsNotFound(err) { // Only return the error when the project exists. Otherwise, we will create the project return nil, err } @@ -98,7 +98,7 @@ func (s *WorkflowService) Update(ctx context.Context, req *pb.WorkflowServiceUpd return nil, err } - if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowUpdate); err != nil { + if err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowUpdate); err != nil { return nil, err } @@ -245,7 +245,7 @@ func (s *WorkflowService) Delete(ctx context.Context, req *pb.WorkflowServiceDel return nil, err } - if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowDelete); err != nil { + if err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowDelete); err != nil { return nil, err } @@ -268,7 +268,7 @@ func (s *WorkflowService) View(ctx context.Context, req *pb.WorkflowServiceViewR } // try to load project and apply RBAC if needed - if err = s.userHasPermissionOnProject(ctx, s.projectsUseCase, currentOrg.ID, req.ProjectName, authz.PolicyWorkflowRead); err != nil { + if err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.ProjectName, authz.PolicyWorkflowRead); err != nil { return nil, err } From 7f2ed585aa23682750109abe9d085b6de85bef7c Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 22:03:09 +0200 Subject: [PATCH 27/48] apply suggestions Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 10 ++++++---- app/controlplane/internal/service/workflow.go | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 25660f3f3..8cb738c4d 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -369,7 +369,7 @@ func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationS } // Apply RBAC on the project - if err := s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunUpdate); err != nil { + if err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunUpdate); err != nil { return nil, err } @@ -665,9 +665,11 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP } // try to load project and apply RBAC if needed - if err := s.userHasPermissionOnProject(ctx, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); err != nil && !errors.IsNotFound(err) { - // Only return the error when the project exists. Otherwise, we will create the project - return nil, err + if err := s.userHasPermissionOnProject(ctx, apiToken.OrgID, req.ProjectName, authz.PolicyWorkflowCreate); err != nil { + // if the project is not found, we ignore the error, since we'll create the project in this call + if !errors.IsNotFound(err) { + return nil, err + } } if wf, err := s.workflowUseCase.FindByNameInOrg(ctx, apiToken.OrgID, req.GetProjectName(), req.GetWorkflowName()); err != nil { diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index dc9ee9fc7..23d5af3c0 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -59,9 +59,11 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre return nil, err } - if err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); err != nil && !errors.IsNotFound(err) { - // Only return the error when the project exists. Otherwise, we will create the project - return nil, err + if err = s.userHasPermissionOnProject(ctx, currentOrg.ID, req.GetProjectName(), authz.PolicyWorkflowCreate); err != nil { + // if the project is not found, we ignore the error, since we'll create the project in this call + if !errors.IsNotFound(err) { + return nil, err + } } createOpts := &biz.WorkflowCreateOpts{ From 9296e27c27e05df5af4f3873a35a4c47b72c13ca Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 22:43:29 +0200 Subject: [PATCH 28/48] apply RBAC to remote attestations Signed-off-by: Jose I. Paris --- .../internal/service/attestationstate.go | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/controlplane/internal/service/attestationstate.go b/app/controlplane/internal/service/attestationstate.go index 28bd19ee9..7de437d48 100644 --- a/app/controlplane/internal/service/attestationstate.go +++ b/app/controlplane/internal/service/attestationstate.go @@ -25,6 +25,7 @@ import ( cpAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" errors "github.com/go-kratos/kratos/v2/errors" @@ -67,6 +68,11 @@ func (s *AttestationStateService) Initialized(ctx context.Context, req *cpAPI.At return nil, handleUseCaseErr(err, s.log) } + // Apply RBAC on the project + if err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { + return nil, err + } + initialized, err := s.attestationStateUseCase.Initialized(ctx, wf.ID.String(), req.WorkflowRunId) if err != nil { return nil, handleUseCaseErr(err, s.log) @@ -89,6 +95,11 @@ func (s *AttestationStateService) Save(ctx context.Context, req *cpAPI.Attestati return nil, handleUseCaseErr(err, s.log) } + // Apply RBAC on the project + if err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunCreate); err != nil { + return nil, err + } + encryptionPassphrase, err := encryptionPassphrase(ctx) if err != nil { return nil, errors.Forbidden("forbidden", "failed to authenticate request") @@ -117,6 +128,11 @@ func (s *AttestationStateService) Read(ctx context.Context, req *cpAPI.Attestati return nil, handleUseCaseErr(err, s.log) } + // Apply RBAC on the project + if err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunRead); err != nil { + return nil, err + } + encryptionPassphrase, err := encryptionPassphrase(ctx) if err != nil { return nil, errors.Forbidden("forbidden", "failed to authenticate request") @@ -146,6 +162,11 @@ func (s *AttestationStateService) Reset(ctx context.Context, req *cpAPI.Attestat return nil, handleUseCaseErr(err, s.log) } + // Apply RBAC on the project + if err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wf.Project, authz.PolicyWorkflowRunUpdate); err != nil { + return nil, err + } + if err := s.attestationStateUseCase.Reset(ctx, wf.ID.String(), req.WorkflowRunId); err != nil { return nil, handleUseCaseErr(err, s.log) } From 0528079f568acec6fe4e0447a9a4cd9c9ada535b Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Mon, 23 Jun 2025 23:12:36 +0200 Subject: [PATCH 29/48] casbackend ls Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index fd7169c3b..71bbc3202 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -188,6 +188,11 @@ var rolesMap = map[Role][]*Policy{ PolicyWorkflowRunList, PolicyWorkflowRunRead, + + PolicyArtifactDownload, + PolicyArtifactUpload, + + PolicyCASBackendList, }, // RoleProjectAdmin: RBAC will be applied in all these RoleProjectAdmin: { From 656a344f57d1f3ed070b502643fef55470a58b89 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 00:02:46 +0200 Subject: [PATCH 30/48] allow member invitations Signed-off-by: Jose I. Paris --- app/controlplane/pkg/biz/user.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controlplane/pkg/biz/user.go b/app/controlplane/pkg/biz/user.go index a7bc0db83..a3eb5cc93 100644 --- a/app/controlplane/pkg/biz/user.go +++ b/app/controlplane/pkg/biz/user.go @@ -254,6 +254,8 @@ func PbRoleToBiz(r pb.MembershipRole) authz.Role { return authz.RoleAdmin case pb.MembershipRole_MEMBERSHIP_ROLE_ORG_VIEWER: return authz.RoleViewer + case pb.MembershipRole_MEMBERSHIP_ROLE_ORG_MEMBER: + return authz.RoleOrgMember default: return "" } From 9b34c1b3d9b7d31d905faf5c4c4e4182a16d6715 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 00:05:06 +0200 Subject: [PATCH 31/48] unify error messages Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/service.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 1f06c1004..64ba51ec4 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -167,12 +167,12 @@ func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resou return handleUseCaseErr(err, s.log) } if !pass { - return errors.Forbidden("forbidden", "user not authorized") + return errors.Forbidden("forbidden", "operation not allowed") } return nil } } - return errors.Forbidden("forbidden", "user not authorized") + return errors.Forbidden("forbidden", "operation not allowed") } func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, pName string, policy *authz.Policy) error { From fdf606ceb64e66348c487a28171bbb558cffa660 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 00:17:28 +0200 Subject: [PATCH 32/48] getuploadcreds Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 8cb738c4d..195ae349b 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -418,6 +418,11 @@ func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.Atte return nil, errors.NotFound("not found", "workflow run not found") } + // Apply RBAC on the project + if err = s.userHasPermissionOnProject(ctx, robotAccount.OrgID, wRun.Workflow.Project, authz.PolicyWorkflowRunCreate); err != nil { + return nil, err + } + if len(wRun.CASBackends) == 0 { return nil, errors.NotFound("not found", "workflow run has no CAS backend") } From b324e72507cddede6d62ffa3533ee1ad46d8bfab Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 11:51:41 +0200 Subject: [PATCH 33/48] attachments Signed-off-by: Jose I. Paris --- .../internal/dispatcher/dispatcher.go | 8 ++++- .../internal/service/integration.go | 15 ++++++++-- app/controlplane/internal/service/service.go | 6 ++-- app/controlplane/internal/service/workflow.go | 6 +--- .../internal/service/workflowrun.go | 7 ++--- app/controlplane/pkg/authz/authz.go | 13 ++++++++ app/controlplane/pkg/biz/integration.go | 30 +++++++++++-------- app/controlplane/pkg/biz/integration_test.go | 4 +-- .../pkg/data/integrationattachment.go | 10 +++++-- 9 files changed, 65 insertions(+), 34 deletions(-) diff --git a/app/controlplane/internal/dispatcher/dispatcher.go b/app/controlplane/internal/dispatcher/dispatcher.go index b440730df..49305e134 100644 --- a/app/controlplane/internal/dispatcher/dispatcher.go +++ b/app/controlplane/internal/dispatcher/dispatcher.go @@ -24,6 +24,7 @@ import ( "time" crv1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/uuid" "github.com/cenkalti/backoff/v4" @@ -146,7 +147,12 @@ func (d *FanOutDispatcher) initDispatchQueue(ctx context.Context, orgID, workflo queue := dispatchQueue{} // List enabled integrations with this workflow - attachments, err := d.integrationUC.ListAttachments(ctx, orgID, workflowID) + wfUUID, err := uuid.Parse(workflowID) + if err != nil { + return nil, fmt.Errorf("parsing workflow ID: %w", err) + } + + attachments, err := d.integrationUC.ListAttachments(ctx, orgID, &biz.ListAttachmentsOpts{WorkflowID: &wfUUID}) if err != nil { return nil, fmt.Errorf("listing attachments: %w", err) } diff --git a/app/controlplane/internal/service/integration.go b/app/controlplane/internal/service/integration.go index c948efa9e..390ecc8f2 100644 --- a/app/controlplane/internal/service/integration.go +++ b/app/controlplane/internal/service/integration.go @@ -20,6 +20,7 @@ import ( "fmt" pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1" errors "github.com/go-kratos/kratos/v2/errors" @@ -129,6 +130,11 @@ func (s *IntegrationsService) Attach(ctx context.Context, req *pb.IntegrationsSe return nil, handleUseCaseErr(err, s.log) } + // Apply RBAC if needed + if err = s.authorizeResource(ctx, authz.PolicyAttachedIntegrationAttach, authz.ResourceTypeProject, wf.ProjectID); err != nil { + return nil, err + } + res, err := s.integrationUC.AttachToWorkflow(ctx, &biz.AttachOpts{ OrgID: org.ID, IntegrationID: integration.ID.String(), WorkflowID: wf.ID.String(), AttachmentConfig: req.Config, @@ -218,7 +224,7 @@ func (s *IntegrationsService) ListAttachments(ctx context.Context, req *pb.ListA } // Translate workflow name to ID - var workflowID string + opts := &biz.ListAttachmentsOpts{} if req.GetWorkflowName() != "" { wf, err := s.workflowUC.FindByNameInOrg(ctx, org.ID, req.GetProjectName(), req.GetWorkflowName()) if err != nil { @@ -227,10 +233,13 @@ func (s *IntegrationsService) ListAttachments(ctx context.Context, req *pb.ListA } return nil, handleUseCaseErr(err, s.log) } - workflowID = wf.ID.String() + opts.WorkflowID = &wf.ID } - integrations, err := s.integrationUC.ListAttachments(ctx, org.ID, workflowID) + // apply RBAC if needed + opts.ProjectIDs = s.visibleProjects(ctx) + + integrations, err := s.integrationUC.ListAttachments(ctx, org.ID, opts) if err != nil { return nil, handleUseCaseErr(err, s.log) } diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index 64ba51ec4..c0aff6b06 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -189,10 +189,10 @@ func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, } // visibleProjects returns projects where the user has any role (currently ProjectAdmin and ProjectViewer) -func (s *service) visibleProjects(ctx context.Context) ([]uuid.UUID, error) { +func (s *service) visibleProjects(ctx context.Context) []uuid.UUID { if !rbacEnabled(ctx) { // returning a NIL slice to denote that RBAC has not been applied, to differentiate from the empty slice case - return nil, nil + return nil } projects := make([]uuid.UUID, 0) @@ -204,7 +204,7 @@ func (s *service) visibleProjects(ctx context.Context) ([]uuid.UUID, error) { } } - return projects, nil + return projects } func rbacEnabled(ctx context.Context) bool { diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index 23d5af3c0..b5d2f0aa5 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -219,11 +219,7 @@ func (s *WorkflowService) List(ctx context.Context, req *pb.WorkflowServiceListR } // filter by visible projects if RBAC is enabled - projectIDs, err := s.visibleProjects(ctx) - if err != nil { - return nil, err - } - filters.ProjectIDs = projectIDs + filters.ProjectIDs = s.visibleProjects(ctx) workflows, count, err := s.useCase.List(ctx, currentOrg.ID, filters, paginationOpts) if err != nil { diff --git a/app/controlplane/internal/service/workflowrun.go b/app/controlplane/internal/service/workflowrun.go index c263dc6a0..ab681dee7 100644 --- a/app/controlplane/internal/service/workflowrun.go +++ b/app/controlplane/internal/service/workflowrun.go @@ -67,11 +67,8 @@ func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServic // Configure filters filters := &biz.RunListFilters{} - projectIDs, err := s.visibleProjects(ctx) - if err != nil { - return nil, err - } - filters.ProjectIDs = projectIDs + // Apply RBAC if needed + filters.ProjectIDs = s.visibleProjects(ctx) // by workflow if req.GetWorkflowName() != "" && req.GetProjectName() != "" { diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 71bbc3202..6138672ef 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -193,6 +193,16 @@ var rolesMap = map[Role][]*Policy{ PolicyArtifactUpload, PolicyCASBackendList, + + PolicyOrganizationRead, + + // integrations + PolicyAvailableIntegrationList, + PolicyAvailableIntegrationRead, + PolicyRegisteredIntegrationList, + // attachments (RBAC will be applied) + PolicyAttachedIntegrationList, + PolicyAttachedIntegrationAttach, }, // RoleProjectAdmin: RBAC will be applied in all these RoleProjectAdmin: { @@ -210,6 +220,9 @@ var rolesMap = map[Role][]*Policy{ // workflow runs PolicyWorkflowRunRead, + + // integrations + PolicyAttachedIntegrationAttach, }, } diff --git a/app/controlplane/pkg/biz/integration.go b/app/controlplane/pkg/biz/integration.go index c6d1f0714..9039f676d 100644 --- a/app/controlplane/pkg/biz/integration.go +++ b/app/controlplane/pkg/biz/integration.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io" + "slices" "time" "github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1" @@ -77,7 +78,7 @@ type IntegrationRepo interface { type IntegrationAttachmentRepo interface { Create(ctx context.Context, integrationID, workflowID uuid.UUID, config []byte) (*IntegrationAttachment, error) - List(ctx context.Context, orgID, workflowID uuid.UUID) ([]*IntegrationAndAttachment, error) + List(ctx context.Context, orgID uuid.UUID, opts *ListAttachmentsOpts) ([]*IntegrationAndAttachment, error) FindByIDInOrg(ctx context.Context, orgID, ID uuid.UUID) (*IntegrationAttachment, error) SoftDelete(ctx context.Context, ID uuid.UUID) error } @@ -313,32 +314,37 @@ func (uc *IntegrationUseCase) Delete(ctx context.Context, orgID, integrationID s return uc.integrationRepo.SoftDelete(ctx, integrationUUID) } +type ListAttachmentsOpts struct { + // limit search for a particular workflow + WorkflowID *uuid.UUID + // limit search in a list of projects. Note that `nil` is no filter + ProjectIDs []uuid.UUID +} + // List attachments returns the list of attachments for a given organization and optionally workflow -func (uc *IntegrationUseCase) ListAttachments(ctx context.Context, orgID, workflowID string) ([]*IntegrationAndAttachment, error) { +func (uc *IntegrationUseCase) ListAttachments(ctx context.Context, orgID string, opts *ListAttachmentsOpts) ([]*IntegrationAndAttachment, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, NewErrInvalidUUID(err) } - workflowUUID := uuid.Nil - if workflowID != "" { - var err error - workflowUUID, err = uuid.Parse(workflowID) - if err != nil { - return nil, NewErrInvalidUUID(err) - } - + if opts != nil && opts.WorkflowID != nil { // We check that the workflow belongs to the provided organization // This check is mostly informative to the user - wf, err := uc.workflowRepo.GetOrgScoped(ctx, orgUUID, workflowUUID) + wf, err := uc.workflowRepo.GetOrgScoped(ctx, orgUUID, *opts.WorkflowID) if err != nil { return nil, err } else if wf == nil { return nil, NewErrNotFound("workflow") } + if opts.ProjectIDs != nil { + if !slices.Contains(opts.ProjectIDs, wf.ProjectID) { + return nil, NewErrNotFound("project") + } + } } - return uc.integrationARepo.List(ctx, orgUUID, workflowUUID) + return uc.integrationARepo.List(ctx, orgUUID, opts) } // Detach integration from workflow diff --git a/app/controlplane/pkg/biz/integration_test.go b/app/controlplane/pkg/biz/integration_test.go index 43cf94124..4e1c6583c 100644 --- a/app/controlplane/pkg/biz/integration_test.go +++ b/app/controlplane/pkg/biz/integration_test.go @@ -199,7 +199,7 @@ func (s *testSuite) TestAttachWorkflow() { assert.Equal(s.workflow.ID, got.WorkflowID) // Make sure it has been stored - attachments, err := s.Integration.ListAttachments(ctx, s.org.ID, s.workflow.ID.String()) + attachments, err := s.Integration.ListAttachments(ctx, s.org.ID, &biz.ListAttachmentsOpts{WorkflowID: &s.workflow.ID}) assert.NoError(err) assert.Len(attachments, 1) }) @@ -241,7 +241,7 @@ func (s *testSuite) TestListAttachments() { assert.NoError(err) // List the attachments - attachments, err := s.Integration.ListAttachments(ctx, s.org.ID, s.workflow.ID.String()) + attachments, err := s.Integration.ListAttachments(ctx, s.org.ID, &biz.ListAttachmentsOpts{WorkflowID: &s.workflow.ID}) assert.NoError(err) assert.Len(attachments, 1) assert.NotNil(attachments[0].Integration) diff --git a/app/controlplane/pkg/data/integrationattachment.go b/app/controlplane/pkg/data/integrationattachment.go index 89a1c4957..a67257421 100644 --- a/app/controlplane/pkg/data/integrationattachment.go +++ b/app/controlplane/pkg/data/integrationattachment.go @@ -58,10 +58,14 @@ func (r *IntegrationAttachmentRepo) Create(ctx context.Context, integrationID, w return res.IntegrationAttachment, nil } -func (r *IntegrationAttachmentRepo) List(ctx context.Context, orgID, workflowID uuid.UUID) ([]*biz.IntegrationAndAttachment, error) { +func (r *IntegrationAttachmentRepo) List(ctx context.Context, orgID uuid.UUID, opts *biz.ListAttachmentsOpts) ([]*biz.IntegrationAndAttachment, error) { wfQuery := orgScopedQuery(r.data.DB, orgID).QueryWorkflows() - if workflowID != uuid.Nil { - wfQuery = wfQuery.Where(workflow.ID(workflowID)) + if opts != nil && opts.WorkflowID != nil { + wfQuery = wfQuery.Where(workflow.ID(*opts.WorkflowID)) + } + + if opts != nil && opts.ProjectIDs != nil { + wfQuery = wfQuery.Where(workflow.ProjectIDIn(opts.ProjectIDs...)) } res, err := wfQuery.QueryIntegrationAttachments().WithIntegration().WithWorkflow(). From 6ea96d9a9c71a6a491677b5e6bca736f798873f5 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 13:20:25 +0200 Subject: [PATCH 34/48] attachment detach Signed-off-by: Jose I. Paris --- .../internal/service/integration.go | 26 +++++++++++++++++++ app/controlplane/pkg/authz/authz.go | 3 +++ app/controlplane/pkg/biz/integration.go | 11 ++++++++ 3 files changed, 40 insertions(+) diff --git a/app/controlplane/internal/service/integration.go b/app/controlplane/internal/service/integration.go index 390ecc8f2..e19aa6035 100644 --- a/app/controlplane/internal/service/integration.go +++ b/app/controlplane/internal/service/integration.go @@ -24,6 +24,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1" errors "github.com/go-kratos/kratos/v2/errors" + "github.com/google/uuid" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -262,6 +263,31 @@ func (s *IntegrationsService) Detach(ctx context.Context, req *pb.IntegrationsSe return nil, err } + // find the project it belongs to + orgID, err := uuid.Parse(org.ID) + if err != nil { + return nil, errors.BadRequest("bad request", "invalid organization") + } + attID, err := uuid.Parse(req.Id) + if err != nil { + return nil, errors.BadRequest("bad request", "invalid integration attachment") + } + + att, err := s.integrationUC.GetAttachment(ctx, orgID, attID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + wf, err := s.workflowUC.FindByIDInOrg(ctx, org.ID, att.WorkflowID.String()) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + + // Apply RBAC + if err = s.authorizeResource(ctx, authz.PolicyAttachedIntegrationDetach, authz.ResourceTypeProject, wf.ProjectID); err != nil { + return nil, handleUseCaseErr(err, s.log) + } + if err := s.integrationUC.Detach(ctx, org.ID, req.Id); err != nil { if biz.IsNotFound(err) { return nil, errors.NotFound("not found", err.Error()) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 6138672ef..fd610140d 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -105,6 +105,7 @@ var ( // Attached integrations PolicyAttachedIntegrationList = &Policy{ResourceAttachedIntegration, ActionList} PolicyAttachedIntegrationAttach = &Policy{ResourceAttachedIntegration, ActionCreate} + PolicyAttachedIntegrationDetach = &Policy{ResourceAttachedIntegration, ActionDelete} // Org Metrics PolicyOrgMetricsRead = &Policy{ResourceOrgMetric, ActionList} // Robot Account @@ -203,6 +204,7 @@ var rolesMap = map[Role][]*Policy{ // attachments (RBAC will be applied) PolicyAttachedIntegrationList, PolicyAttachedIntegrationAttach, + PolicyAttachedIntegrationDetach, }, // RoleProjectAdmin: RBAC will be applied in all these RoleProjectAdmin: { @@ -223,6 +225,7 @@ var rolesMap = map[Role][]*Policy{ // integrations PolicyAttachedIntegrationAttach, + PolicyAttachedIntegrationDetach, }, } diff --git a/app/controlplane/pkg/biz/integration.go b/app/controlplane/pkg/biz/integration.go index 9039f676d..7b192841c 100644 --- a/app/controlplane/pkg/biz/integration.go +++ b/app/controlplane/pkg/biz/integration.go @@ -368,3 +368,14 @@ func (uc *IntegrationUseCase) Detach(ctx context.Context, orgID, attachmentID st return uc.integrationARepo.SoftDelete(ctx, attachmentUUID) } + +func (uc *IntegrationUseCase) GetAttachment(ctx context.Context, orgID, attID uuid.UUID) (*IntegrationAttachment, error) { + attachment, err := uc.integrationARepo.FindByIDInOrg(ctx, orgID, attID) + if err != nil { + return nil, err + } else if attachment == nil { + return nil, NewErrNotFound("attachment") + } + + return attachment, nil +} From a6eb273f91b9a84897464600a53ab0620a2b3b76 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 13:26:04 +0200 Subject: [PATCH 35/48] remove membership list Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index fd610140d..5a62430b5 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -174,8 +174,6 @@ var rolesMap = map[Role][]*Policy{ }, RoleOrgMember: { // Allowed endpoints. RBAC will be applied where needed - PolicyOrganizationListMemberships, - PolicyWorkflowRead, PolicyWorkflowContractList, PolicyWorkflowContractRead, From 96cd0183b84e4759ca1b480eff18dfb261cad80e Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 14:27:04 +0200 Subject: [PATCH 36/48] metrics totals Signed-off-by: Jose I. Paris --- .../internal/service/orgmetric.go | 9 +++-- app/controlplane/pkg/authz/authz.go | 2 + app/controlplane/pkg/biz/orgmetrics.go | 18 ++++----- app/controlplane/pkg/data/orgmetrics.go | 37 ++++++++++++++----- 4 files changed, 44 insertions(+), 22 deletions(-) diff --git a/app/controlplane/internal/service/orgmetric.go b/app/controlplane/internal/service/orgmetric.go index 2009daedc..e4b2f977d 100644 --- a/app/controlplane/internal/service/orgmetric.go +++ b/app/controlplane/internal/service/orgmetric.go @@ -45,19 +45,22 @@ func (s *OrgMetricsService) Totals(ctx context.Context, req *pb.OrgMetricsServic timeWindow := calculateTimeWindow(&req.TimeWindow) + // get user visible projects + projectIDs := s.visibleProjects(ctx) + // totals // TODO: Merge it to a single request - totals, err := s.uc.RunsTotal(ctx, currentOrg.ID, timeWindow) + totals, err := s.uc.RunsTotal(ctx, currentOrg.ID, timeWindow, projectIDs) if err != nil { return nil, handleUseCaseErr(err, s.log) } - totalsByStatus, err := s.uc.RunsTotalByStatus(ctx, currentOrg.ID, timeWindow) + totalsByStatus, err := s.uc.RunsTotalByStatus(ctx, currentOrg.ID, timeWindow, projectIDs) if err != nil { return nil, handleUseCaseErr(err, s.log) } - totalsByRunnerType, err := s.uc.RunsTotalByRunnerType(ctx, currentOrg.ID, timeWindow) + totalsByRunnerType, err := s.uc.RunsTotalByRunnerType(ctx, currentOrg.ID, timeWindow, projectIDs) if err != nil { return nil, handleUseCaseErr(err, s.log) } diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 5a62430b5..f31bfc0bf 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -224,6 +224,8 @@ var rolesMap = map[Role][]*Policy{ // integrations PolicyAttachedIntegrationAttach, PolicyAttachedIntegrationDetach, + + PolicyOrgMetricsRead, }, } diff --git a/app/controlplane/pkg/biz/orgmetrics.go b/app/controlplane/pkg/biz/orgmetrics.go index 5ee440f4d..31b613c52 100644 --- a/app/controlplane/pkg/biz/orgmetrics.go +++ b/app/controlplane/pkg/biz/orgmetrics.go @@ -38,10 +38,10 @@ type OrgMetricsUseCase struct { type OrgMetricsRepo interface { // Total number of runs within the provided time window (from now) - RunsTotal(ctx context.Context, orgID uuid.UUID, timeWindow *TimeWindow) (int32, error) + RunsTotal(ctx context.Context, orgID uuid.UUID, timeWindow *TimeWindow, projectIDs []uuid.UUID) (int32, error) // Total number by run status - RunsByStatusTotal(ctx context.Context, orgID uuid.UUID, timeWindow *TimeWindow) (map[string]int32, error) - RunsByRunnerTypeTotal(ctx context.Context, orgID uuid.UUID, timeWindow *TimeWindow) (map[string]int32, error) + RunsByStatusTotal(ctx context.Context, orgID uuid.UUID, timeWindow *TimeWindow, projectIDs []uuid.UUID) (map[string]int32, error) + RunsByRunnerTypeTotal(ctx context.Context, orgID uuid.UUID, timeWindow *TimeWindow, projectIDs []uuid.UUID) (map[string]int32, error) TopWorkflowsByRunsCount(ctx context.Context, orgID uuid.UUID, numWorkflows int, timeWindow *TimeWindow) ([]*TopWorkflowsByRunsCountItem, error) DailyRunsCount(ctx context.Context, orgID, workflowID uuid.UUID, timeWindow *TimeWindow) ([]*DayRunsCount, error) } @@ -80,7 +80,7 @@ func NewOrgMetricsUseCase(r OrgMetricsRepo, orgRepo OrganizationRepo, wfUseCase }, nil } -func (uc *OrgMetricsUseCase) RunsTotal(ctx context.Context, orgID string, timeWindow *TimeWindow) (int32, error) { +func (uc *OrgMetricsUseCase) RunsTotal(ctx context.Context, orgID string, timeWindow *TimeWindow, projectIDs []uuid.UUID) (int32, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return 0, err @@ -90,10 +90,10 @@ func (uc *OrgMetricsUseCase) RunsTotal(ctx context.Context, orgID string, timeWi return 0, err } - return uc.repo.RunsTotal(ctx, orgUUID, timeWindow) + return uc.repo.RunsTotal(ctx, orgUUID, timeWindow, projectIDs) } -func (uc *OrgMetricsUseCase) RunsTotalByStatus(ctx context.Context, orgID string, timeWindow *TimeWindow) (map[string]int32, error) { +func (uc *OrgMetricsUseCase) RunsTotalByStatus(ctx context.Context, orgID string, timeWindow *TimeWindow, projectIDs []uuid.UUID) (map[string]int32, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, err @@ -103,10 +103,10 @@ func (uc *OrgMetricsUseCase) RunsTotalByStatus(ctx context.Context, orgID string return nil, err } - return uc.repo.RunsByStatusTotal(ctx, orgUUID, timeWindow) + return uc.repo.RunsByStatusTotal(ctx, orgUUID, timeWindow, projectIDs) } -func (uc *OrgMetricsUseCase) RunsTotalByRunnerType(ctx context.Context, orgID string, timeWindow *TimeWindow) (map[string]int32, error) { +func (uc *OrgMetricsUseCase) RunsTotalByRunnerType(ctx context.Context, orgID string, timeWindow *TimeWindow, projectIDs []uuid.UUID) (map[string]int32, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, err @@ -116,7 +116,7 @@ func (uc *OrgMetricsUseCase) RunsTotalByRunnerType(ctx context.Context, orgID st return nil, err } - return uc.repo.RunsByRunnerTypeTotal(ctx, orgUUID, timeWindow) + return uc.repo.RunsByRunnerTypeTotal(ctx, orgUUID, timeWindow, projectIDs) } // DailyRunsCount returns the number of runs per day within the provided time window (from now) diff --git a/app/controlplane/pkg/data/orgmetrics.go b/app/controlplane/pkg/data/orgmetrics.go index 657784c57..70b6420ff 100644 --- a/app/controlplane/pkg/data/orgmetrics.go +++ b/app/controlplane/pkg/data/orgmetrics.go @@ -42,9 +42,15 @@ func NewOrgMetricsRepo(data *Data, l log.Logger) biz.OrgMetricsRepo { } } -func (repo *OrgMetricsRepo) RunsTotal(ctx context.Context, orgID uuid.UUID, tw *biz.TimeWindow) (int32, error) { - total, err := orgScopedQuery(repo.data.DB, orgID). - QueryWorkflows().WithProject(). +func (repo *OrgMetricsRepo) RunsTotal(ctx context.Context, orgID uuid.UUID, tw *biz.TimeWindow, projectIDs []uuid.UUID) (int32, error) { + wfQuery := orgScopedQuery(repo.data.DB, orgID). + QueryWorkflows() + + if projectIDs != nil { + wfQuery = wfQuery.Where(workflow.ProjectIDIn(projectIDs...)) + } + + total, err := wfQuery.WithProject(). QueryWorkflowruns(). Where( workflowrun.CreatedAtGTE(tw.From), @@ -59,14 +65,20 @@ func (repo *OrgMetricsRepo) RunsTotal(ctx context.Context, orgID uuid.UUID, tw * return int32(total), nil } -func (repo *OrgMetricsRepo) RunsByStatusTotal(ctx context.Context, orgID uuid.UUID, tw *biz.TimeWindow) (map[string]int32, error) { +func (repo *OrgMetricsRepo) RunsByStatusTotal(ctx context.Context, orgID uuid.UUID, tw *biz.TimeWindow, projectIDs []uuid.UUID) (map[string]int32, error) { var runs []struct { State string Count int32 } - if err := orgScopedQuery(repo.data.DB, orgID). - QueryWorkflows().WithProject(). + wfQuery := orgScopedQuery(repo.data.DB, orgID). + QueryWorkflows() + + if projectIDs != nil { + wfQuery = wfQuery.Where(workflow.ProjectIDIn(projectIDs...)) + } + + if err := wfQuery.WithProject(). QueryWorkflowruns(). Where( workflowrun.CreatedAtGTE(tw.From), @@ -86,15 +98,20 @@ func (repo *OrgMetricsRepo) RunsByStatusTotal(ctx context.Context, orgID uuid.UU return result, nil } -func (repo *OrgMetricsRepo) RunsByRunnerTypeTotal(ctx context.Context, orgID uuid.UUID, tw *biz.TimeWindow) (map[string]int32, error) { +func (repo *OrgMetricsRepo) RunsByRunnerTypeTotal(ctx context.Context, orgID uuid.UUID, tw *biz.TimeWindow, projectIDs []uuid.UUID) (map[string]int32, error) { var runs []struct { RunnerType string `json:"runner_type"` Count int32 } - if err := orgScopedQuery(repo.data.DB, orgID). - QueryWorkflows(). - QueryWorkflowruns(). + wfQuery := orgScopedQuery(repo.data.DB, orgID). + QueryWorkflows() + + if projectIDs != nil { + wfQuery = wfQuery.Where(workflow.ProjectIDIn(projectIDs...)) + } + + if err := wfQuery.QueryWorkflowruns(). Where( workflowrun.CreatedAtGTE(tw.From), workflowrun.CreatedAtLTE(tw.To), From 453c66ec02b144845f96f2ffe9058f513234e807 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 14:55:01 +0200 Subject: [PATCH 37/48] add role to org member Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index f31bfc0bf..d151f0c9a 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -203,6 +203,8 @@ var rolesMap = map[Role][]*Policy{ PolicyAttachedIntegrationList, PolicyAttachedIntegrationAttach, PolicyAttachedIntegrationDetach, + + PolicyOrgMetricsRead, }, // RoleProjectAdmin: RBAC will be applied in all these RoleProjectAdmin: { From 0faf6b2fce39d3b313a2bf450559f8e2852d099e Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 15:01:31 +0200 Subject: [PATCH 38/48] org metrics are project-aware Signed-off-by: Jose I. Paris --- .../internal/service/orgmetric.go | 10 +++++++-- app/controlplane/pkg/biz/orgmetrics.go | 12 +++++------ app/controlplane/pkg/data/orgmetrics.go | 21 ++++++++++++++----- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/app/controlplane/internal/service/orgmetric.go b/app/controlplane/internal/service/orgmetric.go index e4b2f977d..10b8c288d 100644 --- a/app/controlplane/internal/service/orgmetric.go +++ b/app/controlplane/internal/service/orgmetric.go @@ -80,7 +80,10 @@ func (s *OrgMetricsService) TopWorkflowsByRunsCount(ctx context.Context, req *pb timeWindow := calculateTimeWindow(&req.TimeWindow) - res, err := s.uc.TopWorkflowsByRunsCount(ctx, currentOrg.ID, int(req.GetNumWorkflows()), timeWindow) + // get user visible projects + projectIDs := s.visibleProjects(ctx) + + res, err := s.uc.TopWorkflowsByRunsCount(ctx, currentOrg.ID, int(req.GetNumWorkflows()), timeWindow, projectIDs) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -104,7 +107,10 @@ func (s *OrgMetricsService) DailyRunsCount(ctx context.Context, req *pb.DailyRun timeWindow := calculateTimeWindow(&req.TimeWindow) - metricsByDay, err := s.uc.DailyRunsCount(ctx, org.ID, req.WorkflowId, timeWindow) + // get user visible projects + projectIDs := s.visibleProjects(ctx) + + metricsByDay, err := s.uc.DailyRunsCount(ctx, org.ID, req.WorkflowId, timeWindow, projectIDs) if err != nil { return nil, handleUseCaseErr(err, s.log) } diff --git a/app/controlplane/pkg/biz/orgmetrics.go b/app/controlplane/pkg/biz/orgmetrics.go index 31b613c52..c59f7262a 100644 --- a/app/controlplane/pkg/biz/orgmetrics.go +++ b/app/controlplane/pkg/biz/orgmetrics.go @@ -42,8 +42,8 @@ type OrgMetricsRepo interface { // Total number by run status RunsByStatusTotal(ctx context.Context, orgID uuid.UUID, timeWindow *TimeWindow, projectIDs []uuid.UUID) (map[string]int32, error) RunsByRunnerTypeTotal(ctx context.Context, orgID uuid.UUID, timeWindow *TimeWindow, projectIDs []uuid.UUID) (map[string]int32, error) - TopWorkflowsByRunsCount(ctx context.Context, orgID uuid.UUID, numWorkflows int, timeWindow *TimeWindow) ([]*TopWorkflowsByRunsCountItem, error) - DailyRunsCount(ctx context.Context, orgID, workflowID uuid.UUID, timeWindow *TimeWindow) ([]*DayRunsCount, error) + TopWorkflowsByRunsCount(ctx context.Context, orgID uuid.UUID, numWorkflows int, timeWindow *TimeWindow, projectIDs []uuid.UUID) ([]*TopWorkflowsByRunsCountItem, error) + DailyRunsCount(ctx context.Context, orgID, workflowID uuid.UUID, timeWindow *TimeWindow, projectIDs []uuid.UUID) ([]*DayRunsCount, error) } type DayRunsCount struct { @@ -121,7 +121,7 @@ func (uc *OrgMetricsUseCase) RunsTotalByRunnerType(ctx context.Context, orgID st // DailyRunsCount returns the number of runs per day within the provided time window (from now) // Optionally filtered by workflowID -func (uc *OrgMetricsUseCase) DailyRunsCount(ctx context.Context, orgID string, workflowID *string, timeWindow *TimeWindow) ([]*DayRunsCount, error) { +func (uc *OrgMetricsUseCase) DailyRunsCount(ctx context.Context, orgID string, workflowID *string, timeWindow *TimeWindow, projectIDs []uuid.UUID) ([]*DayRunsCount, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, NewErrInvalidUUID(err) @@ -139,7 +139,7 @@ func (uc *OrgMetricsUseCase) DailyRunsCount(ctx context.Context, orgID string, w } } - return uc.repo.DailyRunsCount(ctx, orgUUID, workflowUUID, timeWindow) + return uc.repo.DailyRunsCount(ctx, orgUUID, workflowUUID, timeWindow, projectIDs) } type TopWorkflowsByRunsCountItem struct { @@ -148,7 +148,7 @@ type TopWorkflowsByRunsCountItem struct { Total int32 } -func (uc *OrgMetricsUseCase) TopWorkflowsByRunsCount(ctx context.Context, orgID string, numWorkflows int, timeWindow *TimeWindow) ([]*TopWorkflowsByRunsCountItem, error) { +func (uc *OrgMetricsUseCase) TopWorkflowsByRunsCount(ctx context.Context, orgID string, numWorkflows int, timeWindow *TimeWindow, projectIDs []uuid.UUID) ([]*TopWorkflowsByRunsCountItem, error) { orgUUID, err := uuid.Parse(orgID) if err != nil { return nil, err @@ -158,7 +158,7 @@ func (uc *OrgMetricsUseCase) TopWorkflowsByRunsCount(ctx context.Context, orgID return nil, err } - return uc.repo.TopWorkflowsByRunsCount(ctx, orgUUID, numWorkflows, timeWindow) + return uc.repo.TopWorkflowsByRunsCount(ctx, orgUUID, numWorkflows, timeWindow, projectIDs) } // GetLastWorkflowStatusByRun returns the last status of each workflow by its last run diff --git a/app/controlplane/pkg/data/orgmetrics.go b/app/controlplane/pkg/data/orgmetrics.go index 70b6420ff..280891f3c 100644 --- a/app/controlplane/pkg/data/orgmetrics.go +++ b/app/controlplane/pkg/data/orgmetrics.go @@ -130,7 +130,7 @@ func (repo *OrgMetricsRepo) RunsByRunnerTypeTotal(ctx context.Context, orgID uui return result, nil } -func (repo *OrgMetricsRepo) TopWorkflowsByRunsCount(ctx context.Context, orgID uuid.UUID, numWorkflows int, tw *biz.TimeWindow) ([]*biz.TopWorkflowsByRunsCountItem, error) { +func (repo *OrgMetricsRepo) TopWorkflowsByRunsCount(ctx context.Context, orgID uuid.UUID, numWorkflows int, tw *biz.TimeWindow, projectIDs []uuid.UUID) ([]*biz.TopWorkflowsByRunsCountItem, error) { var runs []struct { WorkflowID string `json:"workflow_id"` State string @@ -138,9 +138,14 @@ func (repo *OrgMetricsRepo) TopWorkflowsByRunsCount(ctx context.Context, orgID u } // Get workflow runs grouped by state and workflowRunID - if err := orgScopedQuery(repo.data.DB, orgID). - QueryWorkflows(). - QueryWorkflowruns(). + wfQuery := orgScopedQuery(repo.data.DB, orgID). + QueryWorkflows() + + if projectIDs != nil { + wfQuery = wfQuery.Where(workflow.ProjectIDIn(projectIDs...)) + } + + if err := wfQuery.QueryWorkflowruns(). Where( workflowrun.CreatedAtGTE(tw.From), workflowrun.CreatedAtLTE(tw.To), @@ -201,7 +206,7 @@ func (repo *OrgMetricsRepo) TopWorkflowsByRunsCount(ctx context.Context, orgID u return result[0:numWorkflows], nil } -func (repo *OrgMetricsRepo) DailyRunsCount(ctx context.Context, orgID, workflowID uuid.UUID, tw *biz.TimeWindow) ([]*biz.DayRunsCount, error) { +func (repo *OrgMetricsRepo) DailyRunsCount(ctx context.Context, orgID, workflowID uuid.UUID, tw *biz.TimeWindow, projectIDs []uuid.UUID) ([]*biz.DayRunsCount, error) { var runsByStateAndDay []struct { State string Count int32 @@ -210,6 +215,12 @@ func (repo *OrgMetricsRepo) DailyRunsCount(ctx context.Context, orgID, workflowI // Get workflow runs grouped by state and day q := orgScopedQuery(repo.data.DB, orgID).QueryWorkflows() + + // filter by visible projects + if projectIDs != nil { + q = q.Where(workflow.ProjectIDIn(projectIDs...)) + } + // optionally filter by workflowID if workflowID != uuid.Nil { q = q.Where(workflow.ID(workflowID)) From 2f7a6076813ce2d1f86636c75ee8ce30f9d3aaec Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 15:13:47 +0200 Subject: [PATCH 39/48] undo change Signed-off-by: Jose I. Paris --- .../gen/frontend/google/protobuf/descriptor.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts b/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts index 0d2d2fb32..d59b21da4 100644 --- a/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts +++ b/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts @@ -30,7 +30,7 @@ export enum Edition { EDITION_2024 = 1001, /** * EDITION_1_TEST_ONLY - Placeholder editions for testing feature resolution. These should not be - * used or relyed on outside of tests. + * used or relied on outside of tests. */ EDITION_1_TEST_ONLY = 1, EDITION_2_TEST_ONLY = 2, @@ -875,12 +875,13 @@ export interface MessageOptions { export interface FieldOptions { /** + * NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead. * The ctype option instructs the C++ code generator to use a different * representation of the field than it normally would. See the specific * options below. This option is only implemented to support use of * [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of - * type "bytes" in the open source release -- sorry, we'll try to include - * other types in a future version! + * type "bytes" in the open source release. + * TODO: make ctype actually deprecated. */ ctype: FieldOptions_CType; /** @@ -1052,11 +1053,7 @@ export function fieldOptions_JSTypeToJSON(object: FieldOptions_JSType): string { } } -/** - * If set to RETENTION_SOURCE, the option will be omitted from the binary. - * Note: as of January 2023, support for this is in progress and does not yet - * have an effect (b/264593489). - */ +/** If set to RETENTION_SOURCE, the option will be omitted from the binary. */ export enum FieldOptions_OptionRetention { RETENTION_UNKNOWN = 0, RETENTION_RUNTIME = 1, @@ -1099,8 +1096,7 @@ export function fieldOptions_OptionRetentionToJSON(object: FieldOptions_OptionRe /** * This indicates the types of entities that the field may apply to when used * as an option. If it is unset, then the field may be freely used as an - * option on any kind of entity. Note: as of January 2023, support for this is - * in progress and does not yet have an effect (b/264593489). + * option on any kind of entity. */ export enum FieldOptions_OptionTargetType { TARGET_TYPE_UNKNOWN = 0, From f29a69477a2c3060eaa861fc5b43baf1078767e3 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 16:47:28 +0200 Subject: [PATCH 40/48] referrer Signed-off-by: Jose I. Paris --- .../frontend/google/protobuf/descriptor.ts | 16 ++++-- app/controlplane/cmd/wire_gen.go | 4 +- app/controlplane/internal/service/referrer.go | 2 +- app/controlplane/pkg/authz/authz.go | 1 + app/controlplane/pkg/biz/project.go | 1 + app/controlplane/pkg/biz/referrer.go | 56 +++++++++++++++++-- .../pkg/biz/testhelpers/wire_gen.go | 2 +- app/controlplane/pkg/data/project.go | 16 ++++++ app/controlplane/pkg/data/referrer.go | 5 ++ 9 files changed, 88 insertions(+), 15 deletions(-) diff --git a/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts b/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts index d59b21da4..0d2d2fb32 100644 --- a/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts +++ b/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts @@ -30,7 +30,7 @@ export enum Edition { EDITION_2024 = 1001, /** * EDITION_1_TEST_ONLY - Placeholder editions for testing feature resolution. These should not be - * used or relied on outside of tests. + * used or relyed on outside of tests. */ EDITION_1_TEST_ONLY = 1, EDITION_2_TEST_ONLY = 2, @@ -875,13 +875,12 @@ export interface MessageOptions { export interface FieldOptions { /** - * NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead. * The ctype option instructs the C++ code generator to use a different * representation of the field than it normally would. See the specific * options below. This option is only implemented to support use of * [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of - * type "bytes" in the open source release. - * TODO: make ctype actually deprecated. + * type "bytes" in the open source release -- sorry, we'll try to include + * other types in a future version! */ ctype: FieldOptions_CType; /** @@ -1053,7 +1052,11 @@ export function fieldOptions_JSTypeToJSON(object: FieldOptions_JSType): string { } } -/** If set to RETENTION_SOURCE, the option will be omitted from the binary. */ +/** + * If set to RETENTION_SOURCE, the option will be omitted from the binary. + * Note: as of January 2023, support for this is in progress and does not yet + * have an effect (b/264593489). + */ export enum FieldOptions_OptionRetention { RETENTION_UNKNOWN = 0, RETENTION_RUNTIME = 1, @@ -1096,7 +1099,8 @@ export function fieldOptions_OptionRetentionToJSON(object: FieldOptions_OptionRe /** * This indicates the types of entities that the field may apply to when used * as an option. If it is unset, then the field may be freely used as an - * option on any kind of entity. + * option on any kind of entity. Note: as of January 2023, support for this is + * in progress and does not yet have an effect (b/264593489). */ export enum FieldOptions_OptionTargetType { TARGET_TYPE_UNKNOWN = 0, diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 57e041322..b6b77b3d2 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -98,8 +98,9 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l v2 := _wireValue casClientUseCase := biz.NewCASClientUseCase(casCredentialsUseCase, bootstrap_CASServer, logger, v2...) referrerRepo := data.NewReferrerRepo(dataData, workflowRepo, logger) + projectsRepo := data.NewProjectsRepo(dataData, logger) referrerSharedIndex := bootstrap.ReferrerSharedIndex - referrerUseCase, err := biz.NewReferrerUseCase(referrerRepo, workflowRepo, membershipRepo, referrerSharedIndex, logger) + referrerUseCase, err := biz.NewReferrerUseCase(referrerRepo, workflowRepo, membershipRepo, projectsRepo, referrerSharedIndex, logger) if err != nil { cleanup() return nil, nil, err @@ -116,7 +117,6 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l cleanup() return nil, nil, err } - projectsRepo := data.NewProjectsRepo(dataData, logger) workflowContractRepo := data.NewWorkflowContractRepo(dataData, logger) v3 := bootstrap.PolicyProviders v4 := newPolicyProviderConfig(v3) diff --git a/app/controlplane/internal/service/referrer.go b/app/controlplane/internal/service/referrer.go index 62fabe461..3a657711e 100644 --- a/app/controlplane/internal/service/referrer.go +++ b/app/controlplane/internal/service/referrer.go @@ -62,7 +62,7 @@ func (s *ReferrerService) DiscoverPrivate(ctx context.Context, req *pb.ReferrerS return nil, fmt.Errorf("invalid org UUID: %w", err) } - referrer, err = s.referrerUC.GetFromRoot(ctx, req.GetDigest(), req.GetKind(), []uuid.UUID{orgUUID}) + referrer, err = s.referrerUC.GetFromRoot(ctx, req.GetDigest(), req.GetKind(), []uuid.UUID{orgUUID}, nil) } if err != nil { return nil, handleUseCaseErr(err, s.log) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index d151f0c9a..9de36a7d0 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -205,6 +205,7 @@ var rolesMap = map[Role][]*Policy{ PolicyAttachedIntegrationDetach, PolicyOrgMetricsRead, + PolicyReferrerRead, }, // RoleProjectAdmin: RBAC will be applied in all these RoleProjectAdmin: { diff --git a/app/controlplane/pkg/biz/project.go b/app/controlplane/pkg/biz/project.go index 5df25f6ae..ee72bb33f 100644 --- a/app/controlplane/pkg/biz/project.go +++ b/app/controlplane/pkg/biz/project.go @@ -30,6 +30,7 @@ type ProjectsRepo interface { FindProjectByOrgIDAndName(ctx context.Context, orgID uuid.UUID, projectName string) (*Project, error) FindProjectByOrgIDAndID(ctx context.Context, orgID uuid.UUID, projectID uuid.UUID) (*Project, error) Create(ctx context.Context, orgID uuid.UUID, name string) (*Project, error) + ListProjectsByOrgID(ctx context.Context, orgID uuid.UUID) ([]*Project, error) } // ProjectUseCase is a use case for projects diff --git a/app/controlplane/pkg/biz/referrer.go b/app/controlplane/pkg/biz/referrer.go index 6574eaa39..80aa220e6 100644 --- a/app/controlplane/pkg/biz/referrer.go +++ b/app/controlplane/pkg/biz/referrer.go @@ -20,10 +20,12 @@ import ( "errors" "fmt" "io" + "slices" "sort" "time" conf "github.com/chainloop-dev/chainloop/app/controlplane/internal/conf/controlplane/config/v1" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" v2 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop" "github.com/chainloop-dev/chainloop/pkg/servicelogger" @@ -38,11 +40,12 @@ type ReferrerUseCase struct { repo ReferrerRepo membershipRepo MembershipRepo workflowRepo WorkflowRepo + projectsRepo ProjectsRepo logger *log.Helper indexConfig *conf.ReferrerSharedIndex } -func NewReferrerUseCase(repo ReferrerRepo, wfRepo WorkflowRepo, mRepo MembershipRepo, indexCfg *conf.ReferrerSharedIndex, l log.Logger) (*ReferrerUseCase, error) { +func NewReferrerUseCase(repo ReferrerRepo, wfRepo WorkflowRepo, mRepo MembershipRepo, projectsRepo ProjectsRepo, indexCfg *conf.ReferrerSharedIndex, l log.Logger) (*ReferrerUseCase, error) { if l == nil { l = log.NewStdLogger(io.Discard) } @@ -63,6 +66,7 @@ func NewReferrerUseCase(repo ReferrerRepo, wfRepo WorkflowRepo, mRepo Membership membershipRepo: mRepo, indexConfig: indexCfg, workflowRepo: wfRepo, + projectsRepo: projectsRepo, logger: logger, }, nil } @@ -105,6 +109,8 @@ type GetFromRootFilters struct { RootKind *string // Wether to filter by visibility or not Public *bool + // If not nil, it will be used to filter the result by project + ProjectIDs []uuid.UUID } type GetFromRootFilter func(*GetFromRootFilters) @@ -115,6 +121,12 @@ func WithKind(kind string) func(*GetFromRootFilters) { } } +func WithProjectIDs(projectIDs []uuid.UUID) func(*GetFromRootFilters) { + return func(o *GetFromRootFilters) { + o.ProjectIDs = projectIDs + } +} + func WithPublicVisibility(public bool) func(*GetFromRootFilters) { return func(o *GetFromRootFilters) { o.Public = &public @@ -160,24 +172,58 @@ func (s *ReferrerUseCase) GetFromRootUser(ctx context.Context, digest, rootKind, // We pass the list of organizationsIDs from where to look for the referrer // For now we just pass the list of organizations the user is member of // in the future we will expand this to publicly available orgs and so on. - memberships, err := s.membershipRepo.FindByUser(ctx, userUUID) + memberships, err := s.membershipRepo.ListAllByUser(ctx, userUUID) if err != nil { return nil, fmt.Errorf("finding memberships: %w", err) } orgIDs := make([]uuid.UUID, 0, len(memberships)) + var projectIDs []uuid.UUID for _, m := range memberships { - orgIDs = append(orgIDs, m.OrganizationID) + if m.ResourceType == authz.ResourceTypeOrganization { + orgIDs = append(orgIDs, m.ResourceID) + // If the role in the org is member, we must enable RBAC for projects. + if m.Role == authz.RoleOrgMember { + // get list of projects in org, and match it with the memberships to build a filter + orgProjects, err := s.getProjectsWithMembership(ctx, m.ResourceID, memberships) + if err != nil { + return nil, err + } + // note that appending an empty slice to a nil slice doesn't change it (it's still nil) + projectIDs = append(projectIDs, orgProjects...) + } + } + } + + return s.GetFromRoot(ctx, digest, rootKind, orgIDs, projectIDs) +} + +// getProjectsWithMembership returns the list of project IDs in the org for which the user has a membership +func (s *ReferrerUseCase) getProjectsWithMembership(ctx context.Context, orgID uuid.UUID, memberships []*Membership) ([]uuid.UUID, error) { + ids := make([]uuid.UUID, 0) + projects, err := s.projectsRepo.ListProjectsByOrgID(ctx, orgID) + if err != nil { + return nil, fmt.Errorf("listing projects: %w", err) + } + for _, p := range projects { + if slices.ContainsFunc(memberships, func(m *Membership) bool { + return m.ResourceType == authz.ResourceTypeProject && m.ResourceID == p.ID + }) { + ids = append(ids, p.ID) + } } - return s.GetFromRoot(ctx, digest, rootKind, orgIDs) + return ids, nil } -func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind string, orgIDs []uuid.UUID) (*StoredReferrer, error) { +func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind string, orgIDs []uuid.UUID, projectIDs []uuid.UUID) (*StoredReferrer, error) { filters := make([]GetFromRootFilter, 0) if rootKind != "" { filters = append(filters, WithKind(rootKind)) } + if projectIDs != nil { + filters = append(filters, WithProjectIDs(projectIDs)) + } ref, err := s.repo.GetFromRoot(ctx, digest, orgIDs, filters...) if err != nil { diff --git a/app/controlplane/pkg/biz/testhelpers/wire_gen.go b/app/controlplane/pkg/biz/testhelpers/wire_gen.go index e64eec56d..a8f286a6e 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire_gen.go +++ b/app/controlplane/pkg/biz/testhelpers/wire_gen.go @@ -125,7 +125,7 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r } referrerRepo := data.NewReferrerRepo(dataData, workflowRepo, logger) referrerSharedIndex := _wireReferrerSharedIndexValue - referrerUseCase, err := biz.NewReferrerUseCase(referrerRepo, workflowRepo, membershipRepo, referrerSharedIndex, logger) + referrerUseCase, err := biz.NewReferrerUseCase(referrerRepo, workflowRepo, membershipRepo, projectsRepo, referrerSharedIndex, logger) if err != nil { cleanup() return nil, nil, err diff --git a/app/controlplane/pkg/data/project.go b/app/controlplane/pkg/data/project.go index 5e3fe759d..e5b40770b 100644 --- a/app/controlplane/pkg/data/project.go +++ b/app/controlplane/pkg/data/project.go @@ -66,6 +66,22 @@ func (r *ProjectRepo) FindProjectByOrgIDAndID(ctx context.Context, orgID uuid.UU return entProjectToBiz(pro), nil } +func (r *ProjectRepo) ListProjectsByOrgID(ctx context.Context, orgID uuid.UUID) ([]*biz.Project, error) { + prs, err := r.data.DB.Project.Query().Where( + project.OrganizationID(orgID), + project.DeletedAtIsNil()).All(ctx) + if err != nil { + return nil, fmt.Errorf("list projects failed: %w", err) + } + + res := make([]*biz.Project, 0, len(prs)) + for _, p := range prs { + res = append(res, entProjectToBiz(p)) + } + + return res, nil +} + func (r *ProjectRepo) Create(ctx context.Context, orgID uuid.UUID, name string) (*biz.Project, error) { pro, err := r.data.DB.Project.Create().SetOrganizationID(orgID).SetName(name).Save(ctx) if err != nil && !ent.IsNotFound(err) { diff --git a/app/controlplane/pkg/data/referrer.go b/app/controlplane/pkg/data/referrer.go index bd238fb21..b40695e5b 100644 --- a/app/controlplane/pkg/data/referrer.go +++ b/app/controlplane/pkg/data/referrer.go @@ -152,6 +152,11 @@ func (r *ReferrerRepo) GetFromRoot(ctx context.Context, digest string, orgIDs [] workflow.DeletedAtIsNil(), workflow.HasOrganizationWith(organization.IDIn(orgIDs...)), } + // Filter by allowed projects + if opts.ProjectIDs != nil { + predicateWF = append(predicateWF, workflow.ProjectIDIn(opts.ProjectIDs...)) + } + // optionally attaching its visibility if opts.Public != nil { predicateWF = append(predicateWF, workflow.Public(*opts.Public)) From ae94dddb9b2032bd213771852d096802bd42abaf Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 17:05:12 +0200 Subject: [PATCH 41/48] undo change Signed-off-by: Jose I. Paris --- .../gen/frontend/google/protobuf/descriptor.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts b/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts index 0d2d2fb32..d59b21da4 100644 --- a/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts +++ b/app/controlplane/api/gen/frontend/google/protobuf/descriptor.ts @@ -30,7 +30,7 @@ export enum Edition { EDITION_2024 = 1001, /** * EDITION_1_TEST_ONLY - Placeholder editions for testing feature resolution. These should not be - * used or relyed on outside of tests. + * used or relied on outside of tests. */ EDITION_1_TEST_ONLY = 1, EDITION_2_TEST_ONLY = 2, @@ -875,12 +875,13 @@ export interface MessageOptions { export interface FieldOptions { /** + * NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead. * The ctype option instructs the C++ code generator to use a different * representation of the field than it normally would. See the specific * options below. This option is only implemented to support use of * [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of - * type "bytes" in the open source release -- sorry, we'll try to include - * other types in a future version! + * type "bytes" in the open source release. + * TODO: make ctype actually deprecated. */ ctype: FieldOptions_CType; /** @@ -1052,11 +1053,7 @@ export function fieldOptions_JSTypeToJSON(object: FieldOptions_JSType): string { } } -/** - * If set to RETENTION_SOURCE, the option will be omitted from the binary. - * Note: as of January 2023, support for this is in progress and does not yet - * have an effect (b/264593489). - */ +/** If set to RETENTION_SOURCE, the option will be omitted from the binary. */ export enum FieldOptions_OptionRetention { RETENTION_UNKNOWN = 0, RETENTION_RUNTIME = 1, @@ -1099,8 +1096,7 @@ export function fieldOptions_OptionRetentionToJSON(object: FieldOptions_OptionRe /** * This indicates the types of entities that the field may apply to when used * as an option. If it is unset, then the field may be freely used as an - * option on any kind of entity. Note: as of January 2023, support for this is - * in progress and does not yet have an effect (b/264593489). + * option on any kind of entity. */ export enum FieldOptions_OptionTargetType { TARGET_TYPE_UNKNOWN = 0, From 164e0d2c5ad894aad4b0c73549945d585646456f Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 17:39:04 +0200 Subject: [PATCH 42/48] fix test Signed-off-by: Jose I. Paris --- app/controlplane/pkg/biz/referrer_integration_test.go | 4 ++-- app/controlplane/pkg/biz/referrer_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controlplane/pkg/biz/referrer_integration_test.go b/app/controlplane/pkg/biz/referrer_integration_test.go index a15e4b942..a88f152cc 100644 --- a/app/controlplane/pkg/biz/referrer_integration_test.go +++ b/app/controlplane/pkg/biz/referrer_integration_test.go @@ -87,7 +87,7 @@ func (s *referrerIntegrationTestSuite) TestGetFromRootInPublicSharedIndex() { }) s.T().Run("it should appear if we whitelist org2", func(t *testing.T) { - uc, err := biz.NewReferrerUseCase(s.Repos.Referrer, s.Repos.Workflow, s.Repos.Membership, + uc, err := biz.NewReferrerUseCase(s.Repos.Referrer, s.Repos.Workflow, s.Repos.Membership, nil, &conf.ReferrerSharedIndex{ Enabled: true, AllowedOrgs: []string{s.org2.ID}, @@ -463,7 +463,7 @@ func (s *referrerIntegrationTestSuite) SetupTest() { _, err = s.Membership.Create(ctx, s.org2.ID, s.user2.ID, biz.WithCurrentMembership()) require.NoError(s.T(), err) - s.sharedEnabledUC, err = biz.NewReferrerUseCase(s.Repos.Referrer, s.Repos.Workflow, s.Repos.Membership, + s.sharedEnabledUC, err = biz.NewReferrerUseCase(s.Repos.Referrer, s.Repos.Workflow, s.Repos.Membership, nil, &conf.ReferrerSharedIndex{ Enabled: true, AllowedOrgs: []string{s.org1.ID}, diff --git a/app/controlplane/pkg/biz/referrer_test.go b/app/controlplane/pkg/biz/referrer_test.go index b13131eea..7cff735be 100644 --- a/app/controlplane/pkg/biz/referrer_test.go +++ b/app/controlplane/pkg/biz/referrer_test.go @@ -70,7 +70,7 @@ func (s *referrerTestSuite) TestInitialization() { for _, tc := range testCases { s.T().Run(tc.name, func(t *testing.T) { - _, err := NewReferrerUseCase(nil, nil, nil, tc.conf, nil) + _, err := NewReferrerUseCase(nil, nil, nil, nil, tc.conf, nil) if tc.wantErrMsg != "" { assert.EqualError(t, err, tc.wantErrMsg) } else { From 6d6c91ff5bdf0d346173024b346aad879af48395 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 18:06:09 +0200 Subject: [PATCH 43/48] document roles Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 9de36a7d0..ae98b7fe6 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -136,6 +136,7 @@ var ( // NOTE: roles are hierarchical, this means that the Admin Role can inherit all the policies from the Viewer Role // so we do not need to add them as well. var rolesMap = map[Role][]*Policy{ + // RoleViewer is an org-scoped role that provides read-only access to all resources RoleViewer: { // Referrer PolicyReferrerRead, @@ -166,12 +167,15 @@ var rolesMap = map[Role][]*Policy{ // Organization PolicyOrganizationRead, }, + // RoleAdmin is an org-scoped role that provides super admin privileges (it's the higher role) RoleAdmin: { // We do a manual check in the artifact upload endpoint // so we need the actual policy in place skipping it is not enough PolicyArtifactUpload, // + all the policies from the viewer role inherited automatically }, + // RoleOrgMember is an org-scoped role that enables RBAC in the underlying resources. Users with this role at + // the organization level will need specific project roles to access their contents RoleOrgMember: { // Allowed endpoints. RBAC will be applied where needed PolicyWorkflowRead, @@ -207,7 +211,8 @@ var rolesMap = map[Role][]*Policy{ PolicyOrgMetricsRead, PolicyReferrerRead, }, - // RoleProjectAdmin: RBAC will be applied in all these + // RoleProjectAdmin: represents a project administrator. It's the higher role in project resources, + // and it's only considered when the org-level role is `RoleOrgMember` RoleProjectAdmin: { // attestations @@ -228,6 +233,7 @@ var rolesMap = map[Role][]*Policy{ PolicyAttachedIntegrationAttach, PolicyAttachedIntegrationDetach, + // metrics PolicyOrgMetricsRead, }, } From 7a886d1585b7b1905bea05fe70f77e3cd931583a Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 18:34:28 +0200 Subject: [PATCH 44/48] remove member from documentation Signed-off-by: Jose I. Paris --- app/cli/cmd/organization_invitation_create.go | 2 +- app/cli/documentation/cli-reference.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/cli/cmd/organization_invitation_create.go b/app/cli/cmd/organization_invitation_create.go index dc7982c6e..71db0a734 100644 --- a/app/cli/cmd/organization_invitation_create.go +++ b/app/cli/cmd/organization_invitation_create.go @@ -54,7 +54,7 @@ func newOrganizationInvitationCreateCmd() *cobra.Command { cmd.Flags().StringVar(&receiverEmail, "receiver", "", "Email of the user to invite") err := cmd.MarkFlagRequired("receiver") - cmd.Flags().StringVar(&role, "role", string(action.RoleViewer), fmt.Sprintf("Role of the user in the organization, available %s", action.AvailableRoles)) + cmd.Flags().StringVar(&role, "role", string(action.RoleViewer), fmt.Sprintf("Role of the user in the organization, available %s", action.AvailableRoles[:3])) cobra.CheckErr(err) return cmd diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 01987dfa2..2ba2d315e 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2282,7 +2282,7 @@ Options ``` -h, --help help for create --receiver string Email of the user to invite ---role string Role of the user in the organization, available admin, owner, viewer, member (default "viewer") +--role string Role of the user in the organization, available admin, owner, viewer (default "viewer") ``` Options inherited from parent commands From c276566bb0034053a5dc197ff06d30f0160581cb Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 18:37:53 +0200 Subject: [PATCH 45/48] undocument member Signed-off-by: Jose I. Paris --- app/cli/cmd/organization_member_update.go | 2 +- app/cli/documentation/cli-reference.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/cli/cmd/organization_member_update.go b/app/cli/cmd/organization_member_update.go index cec3e035c..5d24c51d2 100644 --- a/app/cli/cmd/organization_member_update.go +++ b/app/cli/cmd/organization_member_update.go @@ -54,7 +54,7 @@ func newOrganizationMemberUpdateCmd() *cobra.Command { err := cmd.MarkFlagRequired("id") cobra.CheckErr(err) - cmd.Flags().StringVar(&role, "role", string(action.RoleViewer), fmt.Sprintf("Role of the user in the organization, available %s", action.AvailableRoles)) + cmd.Flags().StringVar(&role, "role", string(action.RoleViewer), fmt.Sprintf("Role of the user in the organization, available %s", action.AvailableRoles[:3])) cobra.CheckErr(err) return cmd diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index 2ba2d315e..20ac04df0 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2440,7 +2440,7 @@ Options ``` -h, --help help for update --id string Membership ID ---role string Role of the user in the organization, available admin, owner, viewer, member (default "viewer") +--role string Role of the user in the organization, available admin, owner, viewer (default "viewer") ``` Options inherited from parent commands From 13df2b21102984e4f2d0b89e021e3fb01e61eba4 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 18:47:58 +0200 Subject: [PATCH 46/48] lower expiration to 1 second Signed-off-by: Jose I. Paris --- .../internal/usercontext/currentorganization_middleware.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware.go b/app/controlplane/internal/usercontext/currentorganization_middleware.go index 6f3667201..394790395 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware.go @@ -31,7 +31,8 @@ import ( "github.com/hashicorp/golang-lru/v2/expirable" ) -var membershipsCache = expirable.NewLRU[string, *entities.Membership](0, nil, time.Second*10) +// membershipsCache caches user memberships to save some database queries during intensive sessions +var membershipsCache = expirable.NewLRU[string, *entities.Membership](0, nil, time.Second*1) func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, membershipUC biz.MembershipsRBAC, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { From 44bdfc88fdf152cd83208dfc38d8689cb5d22250 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 20:05:25 +0200 Subject: [PATCH 47/48] fix integration detach Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index ae98b7fe6..0c378fbd4 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -203,6 +203,7 @@ var rolesMap = map[Role][]*Policy{ PolicyAvailableIntegrationList, PolicyAvailableIntegrationRead, PolicyRegisteredIntegrationList, + PolicyRegisteredIntegrationRead, // attachments (RBAC will be applied) PolicyAttachedIntegrationList, PolicyAttachedIntegrationAttach, @@ -263,6 +264,7 @@ var ServerOperationsMap = map[string][]*Policy{ // Attached integrations "/controlplane.v1.IntegrationsService/ListAttachments": {PolicyAttachedIntegrationList}, "/controlplane.v1.IntegrationsService/Attach": {PolicyAttachedIntegrationAttach}, + "/controlplane.v1.IntegrationsService/Detach": {PolicyAttachedIntegrationDetach}, // Metrics "/controlplane.v1.OrgMetricsService/.*": {PolicyOrgMetricsRead}, // Robot Account From 42249651088c5228d996cdb8c40307051ba23223 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Tue, 24 Jun 2025 20:20:38 +0200 Subject: [PATCH 48/48] add commments Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/service.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index c0aff6b06..88ce7d7a9 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -152,6 +152,10 @@ func WithProjectUseCase(projectUseCase *biz.ProjectUseCase) NewOpt { } } +// authorizeResource is a helper that checks if the user has a particular `op` permission policy on a particular resource +// For example: `s.authorizeResource(ctx, authz.PolicyAttachedIntegrationDetach, authz.ResourceTypeProject, projectUUID);` +// checks if the user has a role in the project that allows to detach integrations on it. +// This method is available to every service that embeds `service` func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resourceType authz.ResourceType, resourceID uuid.UUID) error { if !rbacEnabled(ctx) { return nil @@ -175,6 +179,9 @@ func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resou return errors.Forbidden("forbidden", "operation not allowed") } +// userHasPermissionOnProject is a helper method that checks if a policy can be applied to a project. It looks for a project +// by name and ensures that the user has a role that allows that specific operation in the project. +// check authorizeResource method func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, pName string, policy *authz.Policy) error { if !rbacEnabled(ctx) { return nil @@ -207,6 +214,7 @@ func (s *service) visibleProjects(ctx context.Context) []uuid.UUID { return projects } +// RBAC feature is enabled if the user has the `Org Member` role. func rbacEnabled(ctx context.Context) bool { return usercontext.CurrentAuthzSubject(ctx) == string(authz.RoleOrgMember) }