Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 2 additions & 23 deletions app/controlplane/pkg/biz/casmapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"slices"
"time"

"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
"github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
"github.com/chainloop-dev/chainloop/pkg/servicelogger"
"github.com/go-kratos/kratos/v2/log"
Expand Down Expand Up @@ -100,29 +99,9 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByUser(ctx context.Context
return nil, NewErrInvalidUUID(err)
}

// Load ALL memberships for the given user
memberships, err := uc.membershipRepo.ListAllByUser(ctx, userUUID)
userOrgs, projectIDs, err := getOrgsAndRBACInfoForUser(ctx, userUUID, uc.membershipRepo, uc.projectsRepo)
if err != nil {
return nil, fmt.Errorf("failed to list memberships: %w", err)
}

userOrgs := make([]uuid.UUID, 0)
// This map holds the list of project IDs by org with RBAC active (user is org "member")
projectIDs := make(map[uuid.UUID][]uuid.UUID)
for _, m := range memberships {
if m.ResourceType == authz.ResourceTypeOrganization {
userOrgs = append(userOrgs, 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 := getProjectsWithMembership(ctx, uc.projectsRepo, 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[m.ResourceID] = orgProjects
}
}
return nil, err
}

mapping, err := uc.FindCASMappingForDownloadByOrg(ctx, digest, userOrgs, projectIDs)
Expand Down
29 changes: 29 additions & 0 deletions app/controlplane/pkg/biz/membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,32 @@ func (uc *MembershipUseCase) SetProjectOwner(ctx context.Context, projectID, use

return nil
}

func getOrgsAndRBACInfoForUser(ctx context.Context, userID uuid.UUID, mRepo MembershipRepo, pRepo ProjectsRepo) ([]uuid.UUID, map[uuid.UUID][]uuid.UUID, error) {
// Load ALL memberships for the given user
memberships, err := mRepo.ListAllByUser(ctx, userID)
if err != nil {
return nil, nil, fmt.Errorf("failed to list memberships: %w", err)
}

userOrgs := make([]uuid.UUID, 0)
// This map holds the list of project IDs by org with RBAC active (user is org "member")
projectIDs := make(map[uuid.UUID][]uuid.UUID)
for _, m := range memberships {
if m.ResourceType == authz.ResourceTypeOrganization {
userOrgs = append(userOrgs, 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 := getProjectsWithMembership(ctx, pRepo, m.ResourceID, memberships)
if err != nil {
return nil, nil, err
}
// note that appending an empty slice to a nil slice doesn't change it (it's still nil)
projectIDs[m.ResourceID] = orgProjects
}
}
}

return userOrgs, projectIDs, nil
}
54 changes: 18 additions & 36 deletions app/controlplane/pkg/biz/referrer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"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"
Expand Down Expand Up @@ -99,17 +98,21 @@ type StoredReferrer struct {
ID uuid.UUID
CreatedAt *time.Time
// Fully expanded list of 1-level off references
References []*StoredReferrer
OrgIDs, WorkflowIDs []uuid.UUID
References []*StoredReferrer
OrgIDs, WorkflowIDs, ProjectIDs []uuid.UUID
}

type OrgID = uuid.UUID
type ProjectID = uuid.UUID

type GetFromRootFilters struct {
// RootKind is the kind of the root referrer, i.e ATTESTATION
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
// ProjectIDs stores visible projects by org for the requesting user.
// If an org entry doesn't exist, it means that RBAC is not applied, hence all projects in that org are visible
ProjectIDs map[OrgID][]ProjectID
}

type GetFromRootFilter func(*GetFromRootFilters)
Expand All @@ -120,7 +123,8 @@ func WithKind(kind string) func(*GetFromRootFilters) {
}
}

func WithProjectIDs(projectIDs []uuid.UUID) func(*GetFromRootFilters) {
// WithVisibleProjectIDs sets visible projects by org for organizations with RBAC enabled for the user (role is OrgMember)
func WithVisibleProjectIDs(projectIDs map[OrgID][]ProjectID) func(*GetFromRootFilters) {
return func(o *GetFromRootFilters) {
o.ProjectIDs = projectIDs
}
Expand Down Expand Up @@ -168,46 +172,24 @@ func (s *ReferrerUseCase) GetFromRootUser(ctx context.Context, digest, rootKind,
return nil, NewErrInvalidUUID(err)
}

// 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.ListAllByUser(ctx, userUUID)
userOrgs, projectIDs, err := getOrgsAndRBACInfoForUser(ctx, userUUID, s.membershipRepo, s.projectsRepo)
if err != nil {
return nil, fmt.Errorf("finding memberships: %w", err)
return nil, err
}

orgIDs := make([]uuid.UUID, 0, len(memberships))
var projectIDs []uuid.UUID
for _, m := range memberships {
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 {
// ensure list is initialized
if projectIDs == nil {
projectIDs = make([]uuid.UUID, 0)
}
// get list of projects in org, and match it with the memberships to build a filter
orgProjects, err := getProjectsWithMembership(ctx, s.projectsRepo, 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)
// 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.
return s.GetFromRoot(ctx, digest, rootKind, userOrgs, projectIDs)
}

func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind string, orgIDs []uuid.UUID, projectIDs []uuid.UUID) (*StoredReferrer, error) {
func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind string, orgIDs []uuid.UUID, projectIDs map[OrgID][]ProjectID) (*StoredReferrer, error) {
filters := make([]GetFromRootFilter, 0)
if rootKind != "" {
filters = append(filters, WithKind(rootKind))
}
if projectIDs != nil {
filters = append(filters, WithProjectIDs(projectIDs))
filters = append(filters, WithVisibleProjectIDs(projectIDs))
}

ref, err := s.repo.GetFromRoot(ctx, digest, orgIDs, filters...)
Expand Down
55 changes: 44 additions & 11 deletions app/controlplane/pkg/data/referrer.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package data
import (
"context"
"fmt"
"slices"

"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent"
Expand All @@ -27,6 +28,7 @@ import (
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow"
"github.com/go-kratos/kratos/v2/log"
"github.com/google/uuid"
"golang.org/x/exp/maps"
)

type ReferrerRepo struct {
Expand Down Expand Up @@ -152,11 +154,6 @@ 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))
Expand All @@ -183,8 +180,8 @@ func (r *ReferrerRepo) GetFromRoot(ctx context.Context, digest string, orgIDs []
}

// Find the referrer recursively starting from the root
res, err := r.doGet(ctx, refs[0], orgIDs, opts.Public, 0)
if err != nil {
res, err := r.doGet(ctx, refs[0], orgIDs, opts.ProjectIDs, opts.Public, 0)
if err != nil && !biz.IsErrUnauthorized(err) {
return nil, fmt.Errorf("failed to get referrer: %w", err)
}

Expand All @@ -196,7 +193,7 @@ func (r *ReferrerRepo) GetFromRoot(ctx context.Context, digest string, orgIDs []
// we also need to limit this because there might be cycles
const maxTraverseLevels = 1

func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrgs []uuid.UUID, public *bool, level int) (*biz.StoredReferrer, error) {
func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrgs []uuid.UUID, visibleProjectsMap map[uuid.UUID][]uuid.UUID, public *bool, level int) (*biz.StoredReferrer, error) {
// Assemble the referrer to return
res := &biz.StoredReferrer{
ID: root.ID,
Expand All @@ -213,6 +210,11 @@ func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrg
// add additional information related to the workflows
hydrateWorkflowsInfo(root, res)

// check that, if RBAC is required, the user has visibility on the artifact in at least 1 org/project
if visible := isReferrerVisible(res, allowedOrgs, visibleProjectsMap); !visible {
return nil, biz.NewErrUnauthorizedStr("referrer not allowed")
}

// Next: We'll find the references recursively up to a max of maxTraverseLevels levels
if level >= maxTraverseLevels {
return res, nil
Expand Down Expand Up @@ -245,24 +247,51 @@ func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrg
for _, reference := range refs {
// Call recursively the function
// we return all the references
ref, err := r.doGet(ctx, reference, allowedOrgs, public, level+1)
if err != nil {
ref, err := r.doGet(ctx, reference, allowedOrgs, visibleProjectsMap, public, level+1)
if err != nil && !biz.IsErrUnauthorized(err) {
return nil, fmt.Errorf("failed to get referrer: %w", err)
}

res.References = append(res.References, ref)
if ref != nil {
res.References = append(res.References, ref)
}
}

return res, nil
}

func isReferrerVisible(ref *biz.StoredReferrer, allowedOrgs []uuid.UUID, visibleProjectsMap map[uuid.UUID][]uuid.UUID) bool {
for _, oid := range ref.OrgIDs {
if !slices.Contains(allowedOrgs, oid) {
// skip check in organizations where the user doesn't have access
continue
}
if visibleProjects, ok := visibleProjectsMap[oid]; ok {
// if entry is present, it means we need to apply RBAC
// check if visible projects and referrer projects match
// by checking if any project is visible by the user
for _, pid := range ref.ProjectIDs {
if slices.Contains(visibleProjects, pid) {
return true
}
}
} else {
// if entry is not found in the map, it means that RBAC is not needed for this org, we have finished
return true
}
}

return false
}

// hydrate the referrer with the following information:
// - isPublic: if it has a public workflow associated
// - workflowIDs: the list of associated workflows
// - orgIDs: the list of associated organizations
func hydrateWorkflowsInfo(root *ent.Referrer, out *biz.StoredReferrer) {
isPublic := false
workflowIDs := make([]uuid.UUID, 0, len(root.Edges.Workflows))
projectIDs := make(map[uuid.UUID]bool, 0)
orgIDs := make([]uuid.UUID, 0)
orgsMap := make(map[uuid.UUID]struct{}, 0)
for _, wf := range root.Edges.Workflows {
Expand All @@ -273,9 +302,13 @@ func hydrateWorkflowsInfo(root *ent.Referrer, out *biz.StoredReferrer) {
if _, ok := orgsMap[wf.OrganizationID]; !ok {
orgIDs = append(orgIDs, wf.OrganizationID)
}
if _, ok := projectIDs[wf.ProjectID]; !ok {
projectIDs[wf.ProjectID] = true
}
orgsMap[wf.OrganizationID] = struct{}{}
}

out.ProjectIDs = maps.Keys(projectIDs)
out.InPublicWorkflow = isPublic
out.WorkflowIDs = workflowIDs
out.OrgIDs = orgIDs
Expand Down
Loading