From 6b73ab87ddb9e982cd9b42e6d867e0b78aa16335 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 25 Jun 2025 16:23:40 +0200 Subject: [PATCH 01/13] remove artifact uploads from org members Signed-off-by: Jose I. Paris --- app/controlplane/pkg/authz/authz.go | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index b4cc56c3c..2dece59d8 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -193,7 +193,6 @@ var rolesMap = map[Role][]*Policy{ PolicyWorkflowRunRead, PolicyArtifactDownload, - PolicyArtifactUpload, PolicyCASBackendList, From 12b5791149bdce6e657bcc16dd04a88b63a8b162 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 25 Jun 2025 16:29:49 +0200 Subject: [PATCH 02/13] add project_id field Signed-off-by: Jose I. Paris --- app/controlplane/pkg/data/ent/casmapping.go | 44 +++++- .../pkg/data/ent/casmapping/casmapping.go | 32 ++++ .../pkg/data/ent/casmapping/where.go | 78 ++++++++++ .../pkg/data/ent/casmapping_create.go | 61 ++++++++ .../pkg/data/ent/casmapping_query.go | 79 +++++++++- .../pkg/data/ent/casmapping_update.go | 6 + app/controlplane/pkg/data/ent/client.go | 16 ++ .../ent/migrate/migrations/20250625142831.sql | 2 + .../pkg/data/ent/migrate/migrations/atlas.sum | 3 +- .../pkg/data/ent/migrate/schema.go | 15 +- app/controlplane/pkg/data/ent/mutation.go | 140 +++++++++++++++++- app/controlplane/pkg/data/ent/schema-viz.html | 2 +- .../pkg/data/ent/schema/casmapping.go | 2 + 13 files changed, 467 insertions(+), 13 deletions(-) create mode 100644 app/controlplane/pkg/data/ent/migrate/migrations/20250625142831.sql diff --git a/app/controlplane/pkg/data/ent/casmapping.go b/app/controlplane/pkg/data/ent/casmapping.go index ba19b1a70..41abb37ba 100644 --- a/app/controlplane/pkg/data/ent/casmapping.go +++ b/app/controlplane/pkg/data/ent/casmapping.go @@ -12,6 +12,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/casbackend" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/casmapping" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project" "github.com/google/uuid" ) @@ -28,10 +29,13 @@ type CASMapping struct { WorkflowRunID uuid.UUID `json:"workflow_run_id,omitempty"` // OrganizationID holds the value of the "organization_id" field. OrganizationID uuid.UUID `json:"organization_id,omitempty"` + // ProjectID holds the value of the "project_id" field. + ProjectID uuid.UUID `json:"project_id,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the CASMappingQuery when eager-loading is set. Edges CASMappingEdges `json:"edges"` cas_mapping_cas_backend *uuid.UUID + cas_mapping_project *uuid.UUID selectValues sql.SelectValues } @@ -41,9 +45,11 @@ type CASMappingEdges struct { CasBackend *CASBackend `json:"cas_backend,omitempty"` // Organization holds the value of the organization edge. Organization *Organization `json:"organization,omitempty"` + // Project holds the value of the project edge. + Project *Project `json:"project,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [2]bool + loadedTypes [3]bool } // CasBackendOrErr returns the CasBackend value or an error if the edge @@ -68,6 +74,17 @@ func (e CASMappingEdges) OrganizationOrErr() (*Organization, error) { return nil, &NotLoadedError{edge: "organization"} } +// ProjectOrErr returns the Project value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e CASMappingEdges) ProjectOrErr() (*Project, error) { + if e.Project != nil { + return e.Project, nil + } else if e.loadedTypes[2] { + return nil, &NotFoundError{label: project.Label} + } + return nil, &NotLoadedError{edge: "project"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*CASMapping) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) @@ -77,10 +94,12 @@ func (*CASMapping) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullString) case casmapping.FieldCreatedAt: values[i] = new(sql.NullTime) - case casmapping.FieldID, casmapping.FieldWorkflowRunID, casmapping.FieldOrganizationID: + case casmapping.FieldID, casmapping.FieldWorkflowRunID, casmapping.FieldOrganizationID, casmapping.FieldProjectID: values[i] = new(uuid.UUID) case casmapping.ForeignKeys[0]: // cas_mapping_cas_backend values[i] = &sql.NullScanner{S: new(uuid.UUID)} + case casmapping.ForeignKeys[1]: // cas_mapping_project + values[i] = &sql.NullScanner{S: new(uuid.UUID)} default: values[i] = new(sql.UnknownType) } @@ -126,6 +145,12 @@ func (cm *CASMapping) assignValues(columns []string, values []any) error { } else if value != nil { cm.OrganizationID = *value } + case casmapping.FieldProjectID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field project_id", values[i]) + } else if value != nil { + cm.ProjectID = *value + } case casmapping.ForeignKeys[0]: if value, ok := values[i].(*sql.NullScanner); !ok { return fmt.Errorf("unexpected type %T for field cas_mapping_cas_backend", values[i]) @@ -133,6 +158,13 @@ func (cm *CASMapping) assignValues(columns []string, values []any) error { cm.cas_mapping_cas_backend = new(uuid.UUID) *cm.cas_mapping_cas_backend = *value.S.(*uuid.UUID) } + case casmapping.ForeignKeys[1]: + if value, ok := values[i].(*sql.NullScanner); !ok { + return fmt.Errorf("unexpected type %T for field cas_mapping_project", values[i]) + } else if value.Valid { + cm.cas_mapping_project = new(uuid.UUID) + *cm.cas_mapping_project = *value.S.(*uuid.UUID) + } default: cm.selectValues.Set(columns[i], values[i]) } @@ -156,6 +188,11 @@ func (cm *CASMapping) QueryOrganization() *OrganizationQuery { return NewCASMappingClient(cm.config).QueryOrganization(cm) } +// QueryProject queries the "project" edge of the CASMapping entity. +func (cm *CASMapping) QueryProject() *ProjectQuery { + return NewCASMappingClient(cm.config).QueryProject(cm) +} + // Update returns a builder for updating this CASMapping. // Note that you need to call CASMapping.Unwrap() before calling this method if this CASMapping // was returned from a transaction, and the transaction was committed or rolled back. @@ -190,6 +227,9 @@ func (cm *CASMapping) String() string { builder.WriteString(", ") builder.WriteString("organization_id=") builder.WriteString(fmt.Sprintf("%v", cm.OrganizationID)) + builder.WriteString(", ") + builder.WriteString("project_id=") + builder.WriteString(fmt.Sprintf("%v", cm.ProjectID)) builder.WriteByte(')') return builder.String() } diff --git a/app/controlplane/pkg/data/ent/casmapping/casmapping.go b/app/controlplane/pkg/data/ent/casmapping/casmapping.go index a2c36edb7..9724211da 100644 --- a/app/controlplane/pkg/data/ent/casmapping/casmapping.go +++ b/app/controlplane/pkg/data/ent/casmapping/casmapping.go @@ -23,10 +23,14 @@ const ( FieldWorkflowRunID = "workflow_run_id" // FieldOrganizationID holds the string denoting the organization_id field in the database. FieldOrganizationID = "organization_id" + // FieldProjectID holds the string denoting the project_id field in the database. + FieldProjectID = "project_id" // EdgeCasBackend holds the string denoting the cas_backend edge name in mutations. EdgeCasBackend = "cas_backend" // EdgeOrganization holds the string denoting the organization edge name in mutations. EdgeOrganization = "organization" + // EdgeProject holds the string denoting the project edge name in mutations. + EdgeProject = "project" // Table holds the table name of the casmapping in the database. Table = "cas_mappings" // CasBackendTable is the table that holds the cas_backend relation/edge. @@ -43,6 +47,13 @@ const ( OrganizationInverseTable = "organizations" // OrganizationColumn is the table column denoting the organization relation/edge. OrganizationColumn = "organization_id" + // ProjectTable is the table that holds the project relation/edge. + ProjectTable = "cas_mappings" + // ProjectInverseTable is the table name for the Project entity. + // It exists in this package in order to avoid circular dependency with the "project" package. + ProjectInverseTable = "projects" + // ProjectColumn is the table column denoting the project relation/edge. + ProjectColumn = "cas_mapping_project" ) // Columns holds all SQL columns for casmapping fields. @@ -52,12 +63,14 @@ var Columns = []string{ FieldCreatedAt, FieldWorkflowRunID, FieldOrganizationID, + FieldProjectID, } // ForeignKeys holds the SQL foreign-keys that are owned by the "cas_mappings" // table and are not defined as standalone fields in the schema. var ForeignKeys = []string{ "cas_mapping_cas_backend", + "cas_mapping_project", } // ValidColumn reports if the column name is valid (part of the table columns). @@ -110,6 +123,11 @@ func ByOrganizationID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldOrganizationID, opts...).ToFunc() } +// ByProjectID orders the results by the project_id field. +func ByProjectID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProjectID, opts...).ToFunc() +} + // ByCasBackendField orders the results by cas_backend field. func ByCasBackendField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { @@ -123,6 +141,13 @@ func ByOrganizationField(field string, opts ...sql.OrderTermOption) OrderOption sqlgraph.OrderByNeighborTerms(s, newOrganizationStep(), sql.OrderByField(field, opts...)) } } + +// ByProjectField orders the results by project field. +func ByProjectField(field string, opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newProjectStep(), sql.OrderByField(field, opts...)) + } +} func newCasBackendStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), @@ -137,3 +162,10 @@ func newOrganizationStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.M2O, false, OrganizationTable, OrganizationColumn), ) } +func newProjectStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(ProjectInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, ProjectTable, ProjectColumn), + ) +} diff --git a/app/controlplane/pkg/data/ent/casmapping/where.go b/app/controlplane/pkg/data/ent/casmapping/where.go index 8e3a109e1..55ee05076 100644 --- a/app/controlplane/pkg/data/ent/casmapping/where.go +++ b/app/controlplane/pkg/data/ent/casmapping/where.go @@ -76,6 +76,11 @@ func OrganizationID(v uuid.UUID) predicate.CASMapping { return predicate.CASMapping(sql.FieldEQ(FieldOrganizationID, v)) } +// ProjectID applies equality check predicate on the "project_id" field. It's identical to ProjectIDEQ. +func ProjectID(v uuid.UUID) predicate.CASMapping { + return predicate.CASMapping(sql.FieldEQ(FieldProjectID, v)) +} + // DigestEQ applies the EQ predicate on the "digest" field. func DigestEQ(v string) predicate.CASMapping { return predicate.CASMapping(sql.FieldEQ(FieldDigest, v)) @@ -251,6 +256,56 @@ func OrganizationIDNotIn(vs ...uuid.UUID) predicate.CASMapping { return predicate.CASMapping(sql.FieldNotIn(FieldOrganizationID, vs...)) } +// ProjectIDEQ applies the EQ predicate on the "project_id" field. +func ProjectIDEQ(v uuid.UUID) predicate.CASMapping { + return predicate.CASMapping(sql.FieldEQ(FieldProjectID, v)) +} + +// ProjectIDNEQ applies the NEQ predicate on the "project_id" field. +func ProjectIDNEQ(v uuid.UUID) predicate.CASMapping { + return predicate.CASMapping(sql.FieldNEQ(FieldProjectID, v)) +} + +// ProjectIDIn applies the In predicate on the "project_id" field. +func ProjectIDIn(vs ...uuid.UUID) predicate.CASMapping { + return predicate.CASMapping(sql.FieldIn(FieldProjectID, vs...)) +} + +// ProjectIDNotIn applies the NotIn predicate on the "project_id" field. +func ProjectIDNotIn(vs ...uuid.UUID) predicate.CASMapping { + return predicate.CASMapping(sql.FieldNotIn(FieldProjectID, vs...)) +} + +// ProjectIDGT applies the GT predicate on the "project_id" field. +func ProjectIDGT(v uuid.UUID) predicate.CASMapping { + return predicate.CASMapping(sql.FieldGT(FieldProjectID, v)) +} + +// ProjectIDGTE applies the GTE predicate on the "project_id" field. +func ProjectIDGTE(v uuid.UUID) predicate.CASMapping { + return predicate.CASMapping(sql.FieldGTE(FieldProjectID, v)) +} + +// ProjectIDLT applies the LT predicate on the "project_id" field. +func ProjectIDLT(v uuid.UUID) predicate.CASMapping { + return predicate.CASMapping(sql.FieldLT(FieldProjectID, v)) +} + +// ProjectIDLTE applies the LTE predicate on the "project_id" field. +func ProjectIDLTE(v uuid.UUID) predicate.CASMapping { + return predicate.CASMapping(sql.FieldLTE(FieldProjectID, v)) +} + +// ProjectIDIsNil applies the IsNil predicate on the "project_id" field. +func ProjectIDIsNil() predicate.CASMapping { + return predicate.CASMapping(sql.FieldIsNull(FieldProjectID)) +} + +// ProjectIDNotNil applies the NotNil predicate on the "project_id" field. +func ProjectIDNotNil() predicate.CASMapping { + return predicate.CASMapping(sql.FieldNotNull(FieldProjectID)) +} + // HasCasBackend applies the HasEdge predicate on the "cas_backend" edge. func HasCasBackend() predicate.CASMapping { return predicate.CASMapping(func(s *sql.Selector) { @@ -297,6 +352,29 @@ func HasOrganizationWith(preds ...predicate.Organization) predicate.CASMapping { }) } +// HasProject applies the HasEdge predicate on the "project" edge. +func HasProject() predicate.CASMapping { + return predicate.CASMapping(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, ProjectTable, ProjectColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasProjectWith applies the HasEdge predicate on the "project" edge with a given conditions (other predicates). +func HasProjectWith(preds ...predicate.Project) predicate.CASMapping { + return predicate.CASMapping(func(s *sql.Selector) { + step := newProjectStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.CASMapping) predicate.CASMapping { return predicate.CASMapping(sql.AndPredicates(predicates...)) diff --git a/app/controlplane/pkg/data/ent/casmapping_create.go b/app/controlplane/pkg/data/ent/casmapping_create.go index a60db6610..203f0ef97 100644 --- a/app/controlplane/pkg/data/ent/casmapping_create.go +++ b/app/controlplane/pkg/data/ent/casmapping_create.go @@ -15,6 +15,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/casbackend" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/casmapping" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project" "github.com/google/uuid" ) @@ -66,6 +67,20 @@ func (cmc *CASMappingCreate) SetOrganizationID(u uuid.UUID) *CASMappingCreate { return cmc } +// SetProjectID sets the "project_id" field. +func (cmc *CASMappingCreate) SetProjectID(u uuid.UUID) *CASMappingCreate { + cmc.mutation.SetProjectID(u) + return cmc +} + +// SetNillableProjectID sets the "project_id" field if the given value is not nil. +func (cmc *CASMappingCreate) SetNillableProjectID(u *uuid.UUID) *CASMappingCreate { + if u != nil { + cmc.SetProjectID(*u) + } + return cmc +} + // SetID sets the "id" field. func (cmc *CASMappingCreate) SetID(u uuid.UUID) *CASMappingCreate { cmc.mutation.SetID(u) @@ -96,6 +111,25 @@ func (cmc *CASMappingCreate) SetOrganization(o *Organization) *CASMappingCreate return cmc.SetOrganizationID(o.ID) } +// SetProjectID sets the "project" edge to the Project entity by ID. +func (cmc *CASMappingCreate) SetProjectID(id uuid.UUID) *CASMappingCreate { + cmc.mutation.SetProjectID(id) + return cmc +} + +// SetNillableProjectID sets the "project" edge to the Project entity by ID if the given value is not nil. +func (cmc *CASMappingCreate) SetNillableProjectID(id *uuid.UUID) *CASMappingCreate { + if id != nil { + cmc = cmc.SetProjectID(*id) + } + return cmc +} + +// SetProject sets the "project" edge to the Project entity. +func (cmc *CASMappingCreate) SetProject(p *Project) *CASMappingCreate { + return cmc.SetProjectID(p.ID) +} + // Mutation returns the CASMappingMutation object of the builder. func (cmc *CASMappingCreate) Mutation() *CASMappingMutation { return cmc.mutation @@ -206,6 +240,10 @@ func (cmc *CASMappingCreate) createSpec() (*CASMapping, *sqlgraph.CreateSpec) { _spec.SetField(casmapping.FieldWorkflowRunID, field.TypeUUID, value) _node.WorkflowRunID = value } + if value, ok := cmc.mutation.ProjectID(); ok { + _spec.SetField(casmapping.FieldProjectID, field.TypeUUID, value) + _node.ProjectID = value + } if nodes := cmc.mutation.CasBackendIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -240,6 +278,23 @@ func (cmc *CASMappingCreate) createSpec() (*CASMapping, *sqlgraph.CreateSpec) { _node.OrganizationID = nodes[0] _spec.Edges = append(_spec.Edges, edge) } + if nodes := cmc.mutation.ProjectIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: false, + Table: casmapping.ProjectTable, + Columns: []string{casmapping.ProjectColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(project.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.cas_mapping_project = &nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } @@ -321,6 +376,9 @@ func (u *CASMappingUpsertOne) UpdateNewValues() *CASMappingUpsertOne { if _, exists := u.create.mutation.OrganizationID(); exists { s.SetIgnore(casmapping.FieldOrganizationID) } + if _, exists := u.create.mutation.ProjectID(); exists { + s.SetIgnore(casmapping.FieldProjectID) + } })) return u } @@ -547,6 +605,9 @@ func (u *CASMappingUpsertBulk) UpdateNewValues() *CASMappingUpsertBulk { if _, exists := b.mutation.OrganizationID(); exists { s.SetIgnore(casmapping.FieldOrganizationID) } + if _, exists := b.mutation.ProjectID(); exists { + s.SetIgnore(casmapping.FieldProjectID) + } } })) return u diff --git a/app/controlplane/pkg/data/ent/casmapping_query.go b/app/controlplane/pkg/data/ent/casmapping_query.go index 8292dcc28..10d929971 100644 --- a/app/controlplane/pkg/data/ent/casmapping_query.go +++ b/app/controlplane/pkg/data/ent/casmapping_query.go @@ -16,6 +16,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/casmapping" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project" "github.com/google/uuid" ) @@ -28,6 +29,7 @@ type CASMappingQuery struct { predicates []predicate.CASMapping withCasBackend *CASBackendQuery withOrganization *OrganizationQuery + withProject *ProjectQuery withFKs bool modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). @@ -110,6 +112,28 @@ func (cmq *CASMappingQuery) QueryOrganization() *OrganizationQuery { return query } +// QueryProject chains the current query on the "project" edge. +func (cmq *CASMappingQuery) QueryProject() *ProjectQuery { + query := (&ProjectClient{config: cmq.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := cmq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := cmq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(casmapping.Table, casmapping.FieldID, selector), + sqlgraph.To(project.Table, project.FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, casmapping.ProjectTable, casmapping.ProjectColumn), + ) + fromU = sqlgraph.SetNeighbors(cmq.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first CASMapping entity from the query. // Returns a *NotFoundError when no CASMapping was found. func (cmq *CASMappingQuery) First(ctx context.Context) (*CASMapping, error) { @@ -304,6 +328,7 @@ func (cmq *CASMappingQuery) Clone() *CASMappingQuery { predicates: append([]predicate.CASMapping{}, cmq.predicates...), withCasBackend: cmq.withCasBackend.Clone(), withOrganization: cmq.withOrganization.Clone(), + withProject: cmq.withProject.Clone(), // clone intermediate query. sql: cmq.sql.Clone(), path: cmq.path, @@ -333,6 +358,17 @@ func (cmq *CASMappingQuery) WithOrganization(opts ...func(*OrganizationQuery)) * return cmq } +// WithProject tells the query-builder to eager-load the nodes that are connected to +// the "project" edge. The optional arguments are used to configure the query builder of the edge. +func (cmq *CASMappingQuery) WithProject(opts ...func(*ProjectQuery)) *CASMappingQuery { + query := (&ProjectClient{config: cmq.config}).Query() + for _, opt := range opts { + opt(query) + } + cmq.withProject = query + return cmq +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -412,12 +448,13 @@ func (cmq *CASMappingQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]* nodes = []*CASMapping{} withFKs = cmq.withFKs _spec = cmq.querySpec() - loadedTypes = [2]bool{ + loadedTypes = [3]bool{ cmq.withCasBackend != nil, cmq.withOrganization != nil, + cmq.withProject != nil, } ) - if cmq.withCasBackend != nil { + if cmq.withCasBackend != nil || cmq.withProject != nil { withFKs = true } if withFKs { @@ -456,6 +493,12 @@ func (cmq *CASMappingQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]* return nil, err } } + if query := cmq.withProject; query != nil { + if err := cmq.loadProject(ctx, query, nodes, nil, + func(n *CASMapping, e *Project) { n.Edges.Project = e }); err != nil { + return nil, err + } + } return nodes, nil } @@ -520,6 +563,38 @@ func (cmq *CASMappingQuery) loadOrganization(ctx context.Context, query *Organiz } return nil } +func (cmq *CASMappingQuery) loadProject(ctx context.Context, query *ProjectQuery, nodes []*CASMapping, init func(*CASMapping), assign func(*CASMapping, *Project)) error { + ids := make([]uuid.UUID, 0, len(nodes)) + nodeids := make(map[uuid.UUID][]*CASMapping) + for i := range nodes { + if nodes[i].cas_mapping_project == nil { + continue + } + fk := *nodes[i].cas_mapping_project + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + if len(ids) == 0 { + return nil + } + query.Where(project.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return fmt.Errorf(`unexpected foreign-key "cas_mapping_project" returned %v`, n.ID) + } + for i := range nodes { + assign(nodes[i], n) + } + } + return nil +} func (cmq *CASMappingQuery) sqlCount(ctx context.Context) (int, error) { _spec := cmq.querySpec() diff --git a/app/controlplane/pkg/data/ent/casmapping_update.go b/app/controlplane/pkg/data/ent/casmapping_update.go index 74ca792c3..429aa4eb1 100644 --- a/app/controlplane/pkg/data/ent/casmapping_update.go +++ b/app/controlplane/pkg/data/ent/casmapping_update.go @@ -92,6 +92,9 @@ func (cmu *CASMappingUpdate) sqlSave(ctx context.Context) (n int, err error) { if cmu.mutation.WorkflowRunIDCleared() { _spec.ClearField(casmapping.FieldWorkflowRunID, field.TypeUUID) } + if cmu.mutation.ProjectIDCleared() { + _spec.ClearField(casmapping.FieldProjectID, field.TypeUUID) + } _spec.AddModifiers(cmu.modifiers...) if n, err = sqlgraph.UpdateNodes(ctx, cmu.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { @@ -208,6 +211,9 @@ func (cmuo *CASMappingUpdateOne) sqlSave(ctx context.Context) (_node *CASMapping if cmuo.mutation.WorkflowRunIDCleared() { _spec.ClearField(casmapping.FieldWorkflowRunID, field.TypeUUID) } + if cmuo.mutation.ProjectIDCleared() { + _spec.ClearField(casmapping.FieldProjectID, field.TypeUUID) + } _spec.AddModifiers(cmuo.modifiers...) _node = &CASMapping{config: cmuo.config} _spec.Assign = _node.assignValues diff --git a/app/controlplane/pkg/data/ent/client.go b/app/controlplane/pkg/data/ent/client.go index 8467e7c73..1c21fdf83 100644 --- a/app/controlplane/pkg/data/ent/client.go +++ b/app/controlplane/pkg/data/ent/client.go @@ -951,6 +951,22 @@ func (c *CASMappingClient) QueryOrganization(cm *CASMapping) *OrganizationQuery return query } +// QueryProject queries the project edge of a CASMapping. +func (c *CASMappingClient) QueryProject(cm *CASMapping) *ProjectQuery { + query := (&ProjectClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := cm.ID + step := sqlgraph.NewStep( + sqlgraph.From(casmapping.Table, casmapping.FieldID, id), + sqlgraph.To(project.Table, project.FieldID), + sqlgraph.Edge(sqlgraph.M2O, false, casmapping.ProjectTable, casmapping.ProjectColumn), + ) + fromV = sqlgraph.Neighbors(cm.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *CASMappingClient) Hooks() []Hook { return c.hooks.CASMapping diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250625142831.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250625142831.sql new file mode 100644 index 000000000..16239f599 --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250625142831.sql @@ -0,0 +1,2 @@ +-- Modify "cas_mappings" table +ALTER TABLE "cas_mappings" ADD COLUMN "project_id" uuid NULL, ADD COLUMN "cas_mapping_project" uuid NULL; diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index 3045a7375..337aed272 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:h/gZpSCHr10MO/31l3/0hc+a9Gq7JOAN1HBdLSYujZ4= +h1:vVIMVv6bnO6jLmPHs8im+5A1QPJINH8fDB20pM5lAIQ= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -88,3 +88,4 @@ h1:h/gZpSCHr10MO/31l3/0hc+a9Gq7JOAN1HBdLSYujZ4= 20250616182009.sql h1:xmLOXknF6GTJP6UgYj6nO6NY0qQ3xfpQ4Awx/KqkSTA= 20250616182058.sql h1:fg5r2AZPj/n9y+FeCRaieUrj0UdFzBooLg7xJsNz8P0= 20250617182716.sql h1:APJGiHfWf95qNV622cc367xde2kEn217BUH+za58vxc= +20250625142831.sql h1:VTAjSauxA2Bdp1adyDlt7PWX68rbg5SHfdtWiCVNTUE= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index 6f84c97f4..bf329a053 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -111,8 +111,10 @@ var ( {Name: "digest", Type: field.TypeString}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "workflow_run_id", Type: field.TypeUUID, Nullable: true}, + {Name: "project_id", Type: field.TypeUUID, Nullable: true}, {Name: "cas_mapping_cas_backend", Type: field.TypeUUID}, {Name: "organization_id", Type: field.TypeUUID}, + {Name: "cas_mapping_project", Type: field.TypeUUID, Nullable: true}, } // CasMappingsTable holds the schema information for the "cas_mappings" table. CasMappingsTable = &schema.Table{ @@ -122,16 +124,22 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "cas_mappings_cas_backends_cas_backend", - Columns: []*schema.Column{CasMappingsColumns[4]}, + Columns: []*schema.Column{CasMappingsColumns[5]}, RefColumns: []*schema.Column{CasBackendsColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "cas_mappings_organizations_organization", - Columns: []*schema.Column{CasMappingsColumns[5]}, + Columns: []*schema.Column{CasMappingsColumns[6]}, RefColumns: []*schema.Column{OrganizationsColumns[0]}, OnDelete: schema.Cascade, }, + { + Symbol: "cas_mappings_projects_project", + Columns: []*schema.Column{CasMappingsColumns[7]}, + RefColumns: []*schema.Column{ProjectsColumns[0]}, + OnDelete: schema.Cascade, + }, }, Indexes: []*schema.Index{ { @@ -147,7 +155,7 @@ var ( { Name: "casmapping_organization_id", Unique: false, - Columns: []*schema.Column{CasMappingsColumns[5]}, + Columns: []*schema.Column{CasMappingsColumns[6]}, }, }, } @@ -812,6 +820,7 @@ func init() { CasBackendsTable.ForeignKeys[0].RefTable = OrganizationsTable CasMappingsTable.ForeignKeys[0].RefTable = CasBackendsTable CasMappingsTable.ForeignKeys[1].RefTable = OrganizationsTable + CasMappingsTable.ForeignKeys[2].RefTable = ProjectsTable IntegrationsTable.ForeignKeys[0].RefTable = OrganizationsTable IntegrationAttachmentsTable.ForeignKeys[0].RefTable = IntegrationsTable IntegrationAttachmentsTable.ForeignKeys[1].RefTable = WorkflowsTable diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index b2d88d479..59518c9be 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -2440,11 +2440,14 @@ type CASMappingMutation struct { digest *string created_at *time.Time workflow_run_id *uuid.UUID + project_id *uuid.UUID clearedFields map[string]struct{} cas_backend *uuid.UUID clearedcas_backend bool organization *uuid.UUID clearedorganization bool + project *uuid.UUID + clearedproject bool done bool oldValue func(context.Context) (*CASMapping, error) predicates []predicate.CASMapping @@ -2711,6 +2714,55 @@ func (m *CASMappingMutation) ResetOrganizationID() { m.organization = nil } +// SetProjectID sets the "project_id" field. +func (m *CASMappingMutation) SetProjectID(u uuid.UUID) { + m.project_id = &u +} + +// ProjectID returns the value of the "project_id" field in the mutation. +func (m *CASMappingMutation) ProjectID() (r uuid.UUID, exists bool) { + v := m.project_id + if v == nil { + return + } + return *v, true +} + +// OldProjectID returns the old "project_id" field's value of the CASMapping entity. +// If the CASMapping 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 *CASMappingMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProjectID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProjectID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProjectID: %w", err) + } + return oldValue.ProjectID, nil +} + +// ClearProjectID clears the value of the "project_id" field. +func (m *CASMappingMutation) ClearProjectID() { + m.project_id = nil + m.clearedFields[casmapping.FieldProjectID] = struct{}{} +} + +// ProjectIDCleared returns if the "project_id" field was cleared in this mutation. +func (m *CASMappingMutation) ProjectIDCleared() bool { + _, ok := m.clearedFields[casmapping.FieldProjectID] + return ok +} + +// ResetProjectID resets all changes to the "project_id" field. +func (m *CASMappingMutation) ResetProjectID() { + m.project_id = nil + delete(m.clearedFields, casmapping.FieldProjectID) +} + // SetCasBackendID sets the "cas_backend" edge to the CASBackend entity by id. func (m *CASMappingMutation) SetCasBackendID(id uuid.UUID) { m.cas_backend = &id @@ -2777,6 +2829,45 @@ func (m *CASMappingMutation) ResetOrganization() { m.clearedorganization = false } +// SetProjectID sets the "project" edge to the Project entity by id. +func (m *CASMappingMutation) SetProjectID(id uuid.UUID) { + m.project = &id +} + +// ClearProject clears the "project" edge to the Project entity. +func (m *CASMappingMutation) ClearProject() { + m.clearedproject = true +} + +// ProjectCleared reports if the "project" edge to the Project entity was cleared. +func (m *CASMappingMutation) ProjectCleared() bool { + return m.clearedproject +} + +// ProjectID returns the "project" edge ID in the mutation. +func (m *CASMappingMutation) ProjectID() (id uuid.UUID, exists bool) { + if m.project != nil { + return *m.project, true + } + return +} + +// ProjectIDs returns the "project" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// ProjectID instead. It exists only for internal usage by the builders. +func (m *CASMappingMutation) ProjectIDs() (ids []uuid.UUID) { + if id := m.project; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetProject resets all changes to the "project" edge. +func (m *CASMappingMutation) ResetProject() { + m.project = nil + m.clearedproject = false +} + // Where appends a list predicates to the CASMappingMutation builder. func (m *CASMappingMutation) Where(ps ...predicate.CASMapping) { m.predicates = append(m.predicates, ps...) @@ -2811,7 +2902,7 @@ func (m *CASMappingMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *CASMappingMutation) Fields() []string { - fields := make([]string, 0, 4) + fields := make([]string, 0, 5) if m.digest != nil { fields = append(fields, casmapping.FieldDigest) } @@ -2824,6 +2915,9 @@ func (m *CASMappingMutation) Fields() []string { if m.organization != nil { fields = append(fields, casmapping.FieldOrganizationID) } + if m.project_id != nil { + fields = append(fields, casmapping.FieldProjectID) + } return fields } @@ -2840,6 +2934,8 @@ func (m *CASMappingMutation) Field(name string) (ent.Value, bool) { return m.WorkflowRunID() case casmapping.FieldOrganizationID: return m.OrganizationID() + case casmapping.FieldProjectID: + return m.ProjectID() } return nil, false } @@ -2857,6 +2953,8 @@ func (m *CASMappingMutation) OldField(ctx context.Context, name string) (ent.Val return m.OldWorkflowRunID(ctx) case casmapping.FieldOrganizationID: return m.OldOrganizationID(ctx) + case casmapping.FieldProjectID: + return m.OldProjectID(ctx) } return nil, fmt.Errorf("unknown CASMapping field %s", name) } @@ -2894,6 +2992,13 @@ func (m *CASMappingMutation) SetField(name string, value ent.Value) error { } m.SetOrganizationID(v) return nil + case casmapping.FieldProjectID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProjectID(v) + return nil } return fmt.Errorf("unknown CASMapping field %s", name) } @@ -2927,6 +3032,9 @@ func (m *CASMappingMutation) ClearedFields() []string { if m.FieldCleared(casmapping.FieldWorkflowRunID) { fields = append(fields, casmapping.FieldWorkflowRunID) } + if m.FieldCleared(casmapping.FieldProjectID) { + fields = append(fields, casmapping.FieldProjectID) + } return fields } @@ -2944,6 +3052,9 @@ func (m *CASMappingMutation) ClearField(name string) error { case casmapping.FieldWorkflowRunID: m.ClearWorkflowRunID() return nil + case casmapping.FieldProjectID: + m.ClearProjectID() + return nil } return fmt.Errorf("unknown CASMapping nullable field %s", name) } @@ -2964,19 +3075,25 @@ func (m *CASMappingMutation) ResetField(name string) error { case casmapping.FieldOrganizationID: m.ResetOrganizationID() return nil + case casmapping.FieldProjectID: + m.ResetProjectID() + return nil } return fmt.Errorf("unknown CASMapping field %s", name) } // AddedEdges returns all edge names that were set/added in this mutation. func (m *CASMappingMutation) AddedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 3) if m.cas_backend != nil { edges = append(edges, casmapping.EdgeCasBackend) } if m.organization != nil { edges = append(edges, casmapping.EdgeOrganization) } + if m.project != nil { + edges = append(edges, casmapping.EdgeProject) + } return edges } @@ -2992,13 +3109,17 @@ func (m *CASMappingMutation) AddedIDs(name string) []ent.Value { if id := m.organization; id != nil { return []ent.Value{*id} } + case casmapping.EdgeProject: + if id := m.project; id != nil { + return []ent.Value{*id} + } } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *CASMappingMutation) RemovedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 3) return edges } @@ -3010,13 +3131,16 @@ func (m *CASMappingMutation) RemovedIDs(name string) []ent.Value { // ClearedEdges returns all edge names that were cleared in this mutation. func (m *CASMappingMutation) ClearedEdges() []string { - edges := make([]string, 0, 2) + edges := make([]string, 0, 3) if m.clearedcas_backend { edges = append(edges, casmapping.EdgeCasBackend) } if m.clearedorganization { edges = append(edges, casmapping.EdgeOrganization) } + if m.clearedproject { + edges = append(edges, casmapping.EdgeProject) + } return edges } @@ -3028,6 +3152,8 @@ func (m *CASMappingMutation) EdgeCleared(name string) bool { return m.clearedcas_backend case casmapping.EdgeOrganization: return m.clearedorganization + case casmapping.EdgeProject: + return m.clearedproject } return false } @@ -3042,6 +3168,9 @@ func (m *CASMappingMutation) ClearEdge(name string) error { case casmapping.EdgeOrganization: m.ClearOrganization() return nil + case casmapping.EdgeProject: + m.ClearProject() + return nil } return fmt.Errorf("unknown CASMapping unique edge %s", name) } @@ -3056,6 +3185,9 @@ func (m *CASMappingMutation) ResetEdge(name string) error { case casmapping.EdgeOrganization: m.ResetOrganization() return nil + case casmapping.EdgeProject: + m.ResetProject() + return nil } return fmt.Errorf("unknown CASMapping edge %s", name) } diff --git a/app/controlplane/pkg/data/ent/schema-viz.html b/app/controlplane/pkg/data/ent/schema-viz.html index 02ef11249..7ac9c4c4a 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\":\"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 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\"},{\"name\":\"project_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\":\"CASMapping\",\"to\":\"Project\",\"label\":\"project\"},{\"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/casmapping.go b/app/controlplane/pkg/data/ent/schema/casmapping.go index dfabfe315..ade5235df 100644 --- a/app/controlplane/pkg/data/ent/schema/casmapping.go +++ b/app/controlplane/pkg/data/ent/schema/casmapping.go @@ -41,6 +41,7 @@ func (CASMapping) Fields() []ent.Field { Annotations(&entsql.Annotation{Default: "CURRENT_TIMESTAMP"}), field.UUID("workflow_run_id", uuid.UUID{}).Immutable().Optional(), field.UUID("organization_id", uuid.UUID{}).Immutable(), + field.UUID("project_id", uuid.UUID{}).Immutable().Optional(), } } @@ -48,6 +49,7 @@ func (CASMapping) Edges() []ent.Edge { return []ent.Edge{ edge.To("cas_backend", CASBackend.Type).Unique().Required().Immutable().Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), edge.To("organization", Organization.Type).Field("organization_id").Unique().Required().Immutable().Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), + edge.To("project", Project.Type).Unique().Immutable().Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), } } From 36529a34587196bea77ccd39e0872bc9f76124f5 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 25 Jun 2025 17:08:26 +0200 Subject: [PATCH 03/13] include project_id in attestations Signed-off-by: Jose I. Paris --- .../internal/service/attestation.go | 5 +++- app/controlplane/pkg/biz/casmapping.go | 20 ++++++-------- app/controlplane/pkg/data/casmapping.go | 14 ++++++---- app/controlplane/pkg/data/ent/casmapping.go | 10 ------- .../pkg/data/ent/casmapping/casmapping.go | 3 +-- .../pkg/data/ent/casmapping/where.go | 20 -------------- .../pkg/data/ent/casmapping_create.go | 20 +------------- .../pkg/data/ent/casmapping_query.go | 12 ++++----- .../pkg/data/ent/casmapping_update.go | 6 ----- .../ent/migrate/migrations/20250625142831.sql | 2 -- .../ent/migrate/migrations/20250625150654.sql | 2 ++ .../pkg/data/ent/migrate/migrations/atlas.sum | 4 +-- .../pkg/data/ent/migrate/schema.go | 11 ++++---- app/controlplane/pkg/data/ent/mutation.go | 27 +++++-------------- .../pkg/data/ent/schema/casmapping.go | 2 +- 15 files changed, 46 insertions(+), 112 deletions(-) delete mode 100644 app/controlplane/pkg/data/ent/migrate/migrations/20250625142831.sql create mode 100644 app/controlplane/pkg/data/ent/migrate/migrations/20250625150654.sql diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 195ae349b..1e674ed60 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -318,7 +318,10 @@ func (s *AttestationService) storeAttestation(ctx context.Context, envelope []by for _, ref := range references { s.log.Infow("msg", "creating CAS mapping", "name", ref.Name, "digest", ref.Digest, "workflowRun", workflowRunID, "casBackend", casBackend.ID.String()) - if _, err := s.casMappingUseCase.Create(ctx, ref.Digest, casBackend.ID.String(), workflowRunID); err != nil { + if _, err := s.casMappingUseCase.Create(ctx, ref.Digest, casBackend.ID.String(), &biz.CASMappingCreateOpts{ + WorkflowRunID: &wfRun.ID, + ProjectID: &wf.ProjectID, + }); err != nil { return nil, handleUseCaseErr(err, s.log) } } diff --git a/app/controlplane/pkg/biz/casmapping.go b/app/controlplane/pkg/biz/casmapping.go index 7160cf2a7..8882cd8e6 100644 --- a/app/controlplane/pkg/biz/casmapping.go +++ b/app/controlplane/pkg/biz/casmapping.go @@ -39,7 +39,7 @@ type CASMapping struct { type CASMappingRepo interface { // Create a mapping with an optional workflow run id - Create(ctx context.Context, digest string, casBackendID uuid.UUID, workflowRunID *uuid.UUID) (*CASMapping, error) + Create(ctx context.Context, digest string, casBackendID uuid.UUID, opts *CASMappingCreateOpts) (*CASMapping, error) // List all the CAS mappings for the given digest FindByDigest(ctx context.Context, digest string) ([]*CASMapping, error) } @@ -54,28 +54,24 @@ func NewCASMappingUseCase(repo CASMappingRepo, mRepo MembershipRepo, logger log. return &CASMappingUseCase{repo, mRepo, servicelogger.ScopedHelper(logger, "cas-mapping-usecase")} } +type CASMappingCreateOpts struct { + WorkflowRunID *uuid.UUID + ProjectID *uuid.UUID +} + // Create a mapping with an optional workflow run id -func (uc *CASMappingUseCase) Create(ctx context.Context, digest string, casBackendID string, workflowRunID string) (*CASMapping, error) { +func (uc *CASMappingUseCase) Create(ctx context.Context, digest string, casBackendID string, opts *CASMappingCreateOpts) (*CASMapping, error) { casBackendUUID, err := uuid.Parse(casBackendID) if err != nil { return nil, NewErrInvalidUUID(err) } - var workflowRunUUID *uuid.UUID - if workflowRunID != "" { - runUUID, err := uuid.Parse(workflowRunID) - if err != nil { - return nil, NewErrInvalidUUID(err) - } - workflowRunUUID = &runUUID - } - // parse the digest to make sure is a valid sha256 sum if _, err = cr_v1.NewHash(digest); err != nil { return nil, NewErrValidation(fmt.Errorf("invalid digest format: %w", err)) } - return uc.repo.Create(ctx, digest, casBackendUUID, workflowRunUUID) + return uc.repo.Create(ctx, digest, casBackendUUID, opts) } func (uc *CASMappingUseCase) FindByDigest(ctx context.Context, digest string) ([]*CASMapping, error) { diff --git a/app/controlplane/pkg/data/casmapping.go b/app/controlplane/pkg/data/casmapping.go index 942af40fe..dce6704b4 100644 --- a/app/controlplane/pkg/data/casmapping.go +++ b/app/controlplane/pkg/data/casmapping.go @@ -42,7 +42,7 @@ func NewCASMappingRepo(data *Data, cbRepo biz.CASBackendRepo, logger log.Logger) } } -func (r *CASMappingRepo) Create(ctx context.Context, digest string, casBackendID uuid.UUID, workflowRunID *uuid.UUID) (*biz.CASMapping, error) { +func (r *CASMappingRepo) Create(ctx context.Context, digest string, casBackendID uuid.UUID, opts *biz.CASMappingCreateOpts) (*biz.CASMapping, error) { casBackend, err := r.casBackendrepo.FindByID(ctx, casBackendID) if err != nil { return nil, fmt.Errorf("failed to find cas backend: %w", err) @@ -50,12 +50,16 @@ func (r *CASMappingRepo) Create(ctx context.Context, digest string, casBackendID return nil, fmt.Errorf("cas backend not found") } - mapping, err := r.data.DB.CASMapping.Create(). + query := r.data.DB.CASMapping.Create(). SetDigest(digest). SetCasBackendID(casBackendID). - SetNillableWorkflowRunID(workflowRunID). - SetOrganizationID(casBackend.OrganizationID). - Save(ctx) + SetOrganizationID(casBackend.OrganizationID) + + if opts != nil { + query.SetNillableProjectID(opts.ProjectID).SetNillableWorkflowRunID(opts.WorkflowRunID) + } + + mapping, err := query.Save(ctx) if err != nil { return nil, fmt.Errorf("failed to create casMapping: %w", err) } diff --git a/app/controlplane/pkg/data/ent/casmapping.go b/app/controlplane/pkg/data/ent/casmapping.go index 41abb37ba..34beed33b 100644 --- a/app/controlplane/pkg/data/ent/casmapping.go +++ b/app/controlplane/pkg/data/ent/casmapping.go @@ -35,7 +35,6 @@ type CASMapping struct { // The values are being populated by the CASMappingQuery when eager-loading is set. Edges CASMappingEdges `json:"edges"` cas_mapping_cas_backend *uuid.UUID - cas_mapping_project *uuid.UUID selectValues sql.SelectValues } @@ -98,8 +97,6 @@ func (*CASMapping) scanValues(columns []string) ([]any, error) { values[i] = new(uuid.UUID) case casmapping.ForeignKeys[0]: // cas_mapping_cas_backend values[i] = &sql.NullScanner{S: new(uuid.UUID)} - case casmapping.ForeignKeys[1]: // cas_mapping_project - values[i] = &sql.NullScanner{S: new(uuid.UUID)} default: values[i] = new(sql.UnknownType) } @@ -158,13 +155,6 @@ func (cm *CASMapping) assignValues(columns []string, values []any) error { cm.cas_mapping_cas_backend = new(uuid.UUID) *cm.cas_mapping_cas_backend = *value.S.(*uuid.UUID) } - case casmapping.ForeignKeys[1]: - if value, ok := values[i].(*sql.NullScanner); !ok { - return fmt.Errorf("unexpected type %T for field cas_mapping_project", values[i]) - } else if value.Valid { - cm.cas_mapping_project = new(uuid.UUID) - *cm.cas_mapping_project = *value.S.(*uuid.UUID) - } default: cm.selectValues.Set(columns[i], values[i]) } diff --git a/app/controlplane/pkg/data/ent/casmapping/casmapping.go b/app/controlplane/pkg/data/ent/casmapping/casmapping.go index 9724211da..52131d81e 100644 --- a/app/controlplane/pkg/data/ent/casmapping/casmapping.go +++ b/app/controlplane/pkg/data/ent/casmapping/casmapping.go @@ -53,7 +53,7 @@ const ( // It exists in this package in order to avoid circular dependency with the "project" package. ProjectInverseTable = "projects" // ProjectColumn is the table column denoting the project relation/edge. - ProjectColumn = "cas_mapping_project" + ProjectColumn = "project_id" ) // Columns holds all SQL columns for casmapping fields. @@ -70,7 +70,6 @@ var Columns = []string{ // table and are not defined as standalone fields in the schema. var ForeignKeys = []string{ "cas_mapping_cas_backend", - "cas_mapping_project", } // ValidColumn reports if the column name is valid (part of the table columns). diff --git a/app/controlplane/pkg/data/ent/casmapping/where.go b/app/controlplane/pkg/data/ent/casmapping/where.go index 55ee05076..ca6c96a58 100644 --- a/app/controlplane/pkg/data/ent/casmapping/where.go +++ b/app/controlplane/pkg/data/ent/casmapping/where.go @@ -276,26 +276,6 @@ func ProjectIDNotIn(vs ...uuid.UUID) predicate.CASMapping { return predicate.CASMapping(sql.FieldNotIn(FieldProjectID, vs...)) } -// ProjectIDGT applies the GT predicate on the "project_id" field. -func ProjectIDGT(v uuid.UUID) predicate.CASMapping { - return predicate.CASMapping(sql.FieldGT(FieldProjectID, v)) -} - -// ProjectIDGTE applies the GTE predicate on the "project_id" field. -func ProjectIDGTE(v uuid.UUID) predicate.CASMapping { - return predicate.CASMapping(sql.FieldGTE(FieldProjectID, v)) -} - -// ProjectIDLT applies the LT predicate on the "project_id" field. -func ProjectIDLT(v uuid.UUID) predicate.CASMapping { - return predicate.CASMapping(sql.FieldLT(FieldProjectID, v)) -} - -// ProjectIDLTE applies the LTE predicate on the "project_id" field. -func ProjectIDLTE(v uuid.UUID) predicate.CASMapping { - return predicate.CASMapping(sql.FieldLTE(FieldProjectID, v)) -} - // ProjectIDIsNil applies the IsNil predicate on the "project_id" field. func ProjectIDIsNil() predicate.CASMapping { return predicate.CASMapping(sql.FieldIsNull(FieldProjectID)) diff --git a/app/controlplane/pkg/data/ent/casmapping_create.go b/app/controlplane/pkg/data/ent/casmapping_create.go index 203f0ef97..87a35815b 100644 --- a/app/controlplane/pkg/data/ent/casmapping_create.go +++ b/app/controlplane/pkg/data/ent/casmapping_create.go @@ -111,20 +111,6 @@ func (cmc *CASMappingCreate) SetOrganization(o *Organization) *CASMappingCreate return cmc.SetOrganizationID(o.ID) } -// SetProjectID sets the "project" edge to the Project entity by ID. -func (cmc *CASMappingCreate) SetProjectID(id uuid.UUID) *CASMappingCreate { - cmc.mutation.SetProjectID(id) - return cmc -} - -// SetNillableProjectID sets the "project" edge to the Project entity by ID if the given value is not nil. -func (cmc *CASMappingCreate) SetNillableProjectID(id *uuid.UUID) *CASMappingCreate { - if id != nil { - cmc = cmc.SetProjectID(*id) - } - return cmc -} - // SetProject sets the "project" edge to the Project entity. func (cmc *CASMappingCreate) SetProject(p *Project) *CASMappingCreate { return cmc.SetProjectID(p.ID) @@ -240,10 +226,6 @@ func (cmc *CASMappingCreate) createSpec() (*CASMapping, *sqlgraph.CreateSpec) { _spec.SetField(casmapping.FieldWorkflowRunID, field.TypeUUID, value) _node.WorkflowRunID = value } - if value, ok := cmc.mutation.ProjectID(); ok { - _spec.SetField(casmapping.FieldProjectID, field.TypeUUID, value) - _node.ProjectID = value - } if nodes := cmc.mutation.CasBackendIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -292,7 +274,7 @@ func (cmc *CASMappingCreate) createSpec() (*CASMapping, *sqlgraph.CreateSpec) { for _, k := range nodes { edge.Target.Nodes = append(edge.Target.Nodes, k) } - _node.cas_mapping_project = &nodes[0] + _node.ProjectID = nodes[0] _spec.Edges = append(_spec.Edges, edge) } return _node, _spec diff --git a/app/controlplane/pkg/data/ent/casmapping_query.go b/app/controlplane/pkg/data/ent/casmapping_query.go index 10d929971..8660d4bcb 100644 --- a/app/controlplane/pkg/data/ent/casmapping_query.go +++ b/app/controlplane/pkg/data/ent/casmapping_query.go @@ -454,7 +454,7 @@ func (cmq *CASMappingQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]* cmq.withProject != nil, } ) - if cmq.withCasBackend != nil || cmq.withProject != nil { + if cmq.withCasBackend != nil { withFKs = true } if withFKs { @@ -567,10 +567,7 @@ func (cmq *CASMappingQuery) loadProject(ctx context.Context, query *ProjectQuery ids := make([]uuid.UUID, 0, len(nodes)) nodeids := make(map[uuid.UUID][]*CASMapping) for i := range nodes { - if nodes[i].cas_mapping_project == nil { - continue - } - fk := *nodes[i].cas_mapping_project + fk := nodes[i].ProjectID if _, ok := nodeids[fk]; !ok { ids = append(ids, fk) } @@ -587,7 +584,7 @@ func (cmq *CASMappingQuery) loadProject(ctx context.Context, query *ProjectQuery for _, n := range neighbors { nodes, ok := nodeids[n.ID] if !ok { - return fmt.Errorf(`unexpected foreign-key "cas_mapping_project" returned %v`, n.ID) + return fmt.Errorf(`unexpected foreign-key "project_id" returned %v`, n.ID) } for i := range nodes { assign(nodes[i], n) @@ -627,6 +624,9 @@ func (cmq *CASMappingQuery) querySpec() *sqlgraph.QuerySpec { if cmq.withOrganization != nil { _spec.Node.AddColumnOnce(casmapping.FieldOrganizationID) } + if cmq.withProject != nil { + _spec.Node.AddColumnOnce(casmapping.FieldProjectID) + } } if ps := cmq.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { diff --git a/app/controlplane/pkg/data/ent/casmapping_update.go b/app/controlplane/pkg/data/ent/casmapping_update.go index 429aa4eb1..74ca792c3 100644 --- a/app/controlplane/pkg/data/ent/casmapping_update.go +++ b/app/controlplane/pkg/data/ent/casmapping_update.go @@ -92,9 +92,6 @@ func (cmu *CASMappingUpdate) sqlSave(ctx context.Context) (n int, err error) { if cmu.mutation.WorkflowRunIDCleared() { _spec.ClearField(casmapping.FieldWorkflowRunID, field.TypeUUID) } - if cmu.mutation.ProjectIDCleared() { - _spec.ClearField(casmapping.FieldProjectID, field.TypeUUID) - } _spec.AddModifiers(cmu.modifiers...) if n, err = sqlgraph.UpdateNodes(ctx, cmu.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { @@ -211,9 +208,6 @@ func (cmuo *CASMappingUpdateOne) sqlSave(ctx context.Context) (_node *CASMapping if cmuo.mutation.WorkflowRunIDCleared() { _spec.ClearField(casmapping.FieldWorkflowRunID, field.TypeUUID) } - if cmuo.mutation.ProjectIDCleared() { - _spec.ClearField(casmapping.FieldProjectID, field.TypeUUID) - } _spec.AddModifiers(cmuo.modifiers...) _node = &CASMapping{config: cmuo.config} _spec.Assign = _node.assignValues diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250625142831.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250625142831.sql deleted file mode 100644 index 16239f599..000000000 --- a/app/controlplane/pkg/data/ent/migrate/migrations/20250625142831.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Modify "cas_mappings" table -ALTER TABLE "cas_mappings" ADD COLUMN "project_id" uuid NULL, ADD COLUMN "cas_mapping_project" uuid NULL; diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20250625150654.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20250625150654.sql new file mode 100644 index 000000000..b82edbd3d --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20250625150654.sql @@ -0,0 +1,2 @@ +-- Modify "cas_mappings" table +ALTER TABLE "cas_mappings" ADD COLUMN "project_id" uuid NULL; diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index 337aed272..0af55421c 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:vVIMVv6bnO6jLmPHs8im+5A1QPJINH8fDB20pM5lAIQ= +h1:Y5ovJ9AdK4OOnYX5P7cqSIlXwJS+IYlXynwtkNjYuP8= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -88,4 +88,4 @@ h1:vVIMVv6bnO6jLmPHs8im+5A1QPJINH8fDB20pM5lAIQ= 20250616182009.sql h1:xmLOXknF6GTJP6UgYj6nO6NY0qQ3xfpQ4Awx/KqkSTA= 20250616182058.sql h1:fg5r2AZPj/n9y+FeCRaieUrj0UdFzBooLg7xJsNz8P0= 20250617182716.sql h1:APJGiHfWf95qNV622cc367xde2kEn217BUH+za58vxc= -20250625142831.sql h1:VTAjSauxA2Bdp1adyDlt7PWX68rbg5SHfdtWiCVNTUE= +20250625150654.sql h1:bFDraxRNN2xnv3MwjGi0jiBbRNdqkrGSXFhyBFruEIw= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index bf329a053..f9ca031c2 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -111,10 +111,9 @@ var ( {Name: "digest", Type: field.TypeString}, {Name: "created_at", Type: field.TypeTime, Default: "CURRENT_TIMESTAMP"}, {Name: "workflow_run_id", Type: field.TypeUUID, Nullable: true}, - {Name: "project_id", Type: field.TypeUUID, Nullable: true}, {Name: "cas_mapping_cas_backend", Type: field.TypeUUID}, {Name: "organization_id", Type: field.TypeUUID}, - {Name: "cas_mapping_project", Type: field.TypeUUID, Nullable: true}, + {Name: "project_id", Type: field.TypeUUID, Nullable: true}, } // CasMappingsTable holds the schema information for the "cas_mappings" table. CasMappingsTable = &schema.Table{ @@ -124,19 +123,19 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "cas_mappings_cas_backends_cas_backend", - Columns: []*schema.Column{CasMappingsColumns[5]}, + Columns: []*schema.Column{CasMappingsColumns[4]}, RefColumns: []*schema.Column{CasBackendsColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "cas_mappings_organizations_organization", - Columns: []*schema.Column{CasMappingsColumns[6]}, + Columns: []*schema.Column{CasMappingsColumns[5]}, RefColumns: []*schema.Column{OrganizationsColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "cas_mappings_projects_project", - Columns: []*schema.Column{CasMappingsColumns[7]}, + Columns: []*schema.Column{CasMappingsColumns[6]}, RefColumns: []*schema.Column{ProjectsColumns[0]}, OnDelete: schema.Cascade, }, @@ -155,7 +154,7 @@ var ( { Name: "casmapping_organization_id", Unique: false, - Columns: []*schema.Column{CasMappingsColumns[6]}, + Columns: []*schema.Column{CasMappingsColumns[5]}, }, }, } diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index 59518c9be..bc48c46a0 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -2440,7 +2440,6 @@ type CASMappingMutation struct { digest *string created_at *time.Time workflow_run_id *uuid.UUID - project_id *uuid.UUID clearedFields map[string]struct{} cas_backend *uuid.UUID clearedcas_backend bool @@ -2716,12 +2715,12 @@ func (m *CASMappingMutation) ResetOrganizationID() { // SetProjectID sets the "project_id" field. func (m *CASMappingMutation) SetProjectID(u uuid.UUID) { - m.project_id = &u + m.project = &u } // ProjectID returns the value of the "project_id" field in the mutation. func (m *CASMappingMutation) ProjectID() (r uuid.UUID, exists bool) { - v := m.project_id + v := m.project if v == nil { return } @@ -2747,7 +2746,7 @@ func (m *CASMappingMutation) OldProjectID(ctx context.Context) (v uuid.UUID, err // ClearProjectID clears the value of the "project_id" field. func (m *CASMappingMutation) ClearProjectID() { - m.project_id = nil + m.project = nil m.clearedFields[casmapping.FieldProjectID] = struct{}{} } @@ -2759,7 +2758,7 @@ func (m *CASMappingMutation) ProjectIDCleared() bool { // ResetProjectID resets all changes to the "project_id" field. func (m *CASMappingMutation) ResetProjectID() { - m.project_id = nil + m.project = nil delete(m.clearedFields, casmapping.FieldProjectID) } @@ -2829,27 +2828,15 @@ func (m *CASMappingMutation) ResetOrganization() { m.clearedorganization = false } -// SetProjectID sets the "project" edge to the Project entity by id. -func (m *CASMappingMutation) SetProjectID(id uuid.UUID) { - m.project = &id -} - // ClearProject clears the "project" edge to the Project entity. func (m *CASMappingMutation) ClearProject() { m.clearedproject = true + m.clearedFields[casmapping.FieldProjectID] = struct{}{} } // ProjectCleared reports if the "project" edge to the Project entity was cleared. func (m *CASMappingMutation) ProjectCleared() bool { - return m.clearedproject -} - -// ProjectID returns the "project" edge ID in the mutation. -func (m *CASMappingMutation) ProjectID() (id uuid.UUID, exists bool) { - if m.project != nil { - return *m.project, true - } - return + return m.ProjectIDCleared() || m.clearedproject } // ProjectIDs returns the "project" edge IDs in the mutation. @@ -2915,7 +2902,7 @@ func (m *CASMappingMutation) Fields() []string { if m.organization != nil { fields = append(fields, casmapping.FieldOrganizationID) } - if m.project_id != nil { + if m.project != nil { fields = append(fields, casmapping.FieldProjectID) } return fields diff --git a/app/controlplane/pkg/data/ent/schema/casmapping.go b/app/controlplane/pkg/data/ent/schema/casmapping.go index ade5235df..0484e0094 100644 --- a/app/controlplane/pkg/data/ent/schema/casmapping.go +++ b/app/controlplane/pkg/data/ent/schema/casmapping.go @@ -49,7 +49,7 @@ func (CASMapping) Edges() []ent.Edge { return []ent.Edge{ edge.To("cas_backend", CASBackend.Type).Unique().Required().Immutable().Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), edge.To("organization", Organization.Type).Field("organization_id").Unique().Required().Immutable().Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), - edge.To("project", Project.Type).Unique().Immutable().Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), + edge.To("project", Project.Type).Field("project_id").Unique().Immutable().Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), } } From 05dac2e1664c86f4cb667e4294a6709c2796de62 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Wed, 25 Jun 2025 18:03:38 +0200 Subject: [PATCH 04/13] apply RBAC to CAS credential download Signed-off-by: Jose I. Paris --- app/controlplane/cmd/wire_gen.go | 2 +- .../internal/service/cascredential.go | 7 +- .../internal/service/casredirect.go | 7 +- app/controlplane/pkg/biz/casmapping.go | 71 +++++++++++++------ app/controlplane/pkg/biz/project.go | 21 ++++++ app/controlplane/pkg/biz/referrer.go | 25 ++----- .../pkg/biz/testhelpers/wire_gen.go | 2 +- app/controlplane/pkg/data/casmapping.go | 1 + 8 files changed, 92 insertions(+), 44 deletions(-) diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index b6b77b3d2..389f05376 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -165,7 +165,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l attestationUseCase := biz.NewAttestationUseCase(casClientUseCase, logger) fanOutDispatcher := dispatcher.New(integrationUseCase, workflowUseCase, workflowRunUseCase, readerWriter, casClientUseCase, availablePlugins, logger) casMappingRepo := data.NewCASMappingRepo(dataData, casBackendRepo, logger) - casMappingUseCase := biz.NewCASMappingUseCase(casMappingRepo, membershipRepo, logger) + casMappingUseCase := biz.NewCASMappingUseCase(casMappingRepo, membershipRepo, projectsRepo, logger) v6 := bootstrap.PrometheusIntegration orgMetricsRepo := data.NewOrgMetricsRepo(dataData, logger) orgMetricsUseCase, err := biz.NewOrgMetricsUseCase(orgMetricsRepo, organizationRepo, workflowUseCase, logger) diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index 23e809b92..f0075a972 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -21,6 +21,7 @@ import ( pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" errors "github.com/go-kratos/kratos/v2/errors" + "github.com/google/uuid" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" @@ -99,7 +100,11 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS mapping, err = s.casMappingUC.FindCASMappingForDownloadByUser(ctx, req.Digest, currentUser.ID) // otherwise, we'll try to find a mapping for the current API token associated orgs } else if currentAPIToken != nil { - mapping, err = s.casMappingUC.FindCASMappingForDownloadByOrg(ctx, req.Digest, []string{currentOrg.ID}) + orgID, err := uuid.Parse(currentOrg.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + mapping, err = s.casMappingUC.FindCASMappingForDownloadByOrg(ctx, req.Digest, []uuid.UUID{orgID}, nil) } // If we can't find a mapping, we'll use the default backend diff --git a/app/controlplane/internal/service/casredirect.go b/app/controlplane/internal/service/casredirect.go index cdce07bf2..c3f83d3e2 100644 --- a/app/controlplane/internal/service/casredirect.go +++ b/app/controlplane/internal/service/casredirect.go @@ -30,6 +30,7 @@ import ( casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas" kerrors "github.com/go-kratos/kratos/v2/errors" khttp "github.com/go-kratos/kratos/v2/transport/http" + "github.com/google/uuid" ) const ( @@ -82,7 +83,11 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown if currentUser != nil { mapping, err = s.casMappingUC.FindCASMappingForDownloadByUser(ctx, req.Digest, currentUser.ID) } else if currentAPIToken != nil { - mapping, err = s.casMappingUC.FindCASMappingForDownloadByOrg(ctx, req.Digest, []string{currentOrg.ID}) + orgID, err := uuid.Parse(currentOrg.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + mapping, err = s.casMappingUC.FindCASMappingForDownloadByOrg(ctx, req.Digest, []uuid.UUID{orgID}, nil) } if err != nil { diff --git a/app/controlplane/pkg/biz/casmapping.go b/app/controlplane/pkg/biz/casmapping.go index 8882cd8e6..d7788c4f0 100644 --- a/app/controlplane/pkg/biz/casmapping.go +++ b/app/controlplane/pkg/biz/casmapping.go @@ -18,8 +18,10 @@ package biz import ( "context" "fmt" + "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" @@ -34,7 +36,13 @@ type CASMapping struct { Digest string CreatedAt *time.Time // A public mapping means that the material/attestation can be downloaded by anyone - Public bool + Public bool + ProjectID uuid.UUID +} + +type CASMappingFindOptions struct { + Orgs []uuid.UUID + ProjectIDs []uuid.UUID } type CASMappingRepo interface { @@ -47,11 +55,12 @@ type CASMappingRepo interface { type CASMappingUseCase struct { repo CASMappingRepo membershipRepo MembershipRepo + projectsRepo ProjectsRepo logger *log.Helper } -func NewCASMappingUseCase(repo CASMappingRepo, mRepo MembershipRepo, logger log.Logger) *CASMappingUseCase { - return &CASMappingUseCase{repo, mRepo, servicelogger.ScopedHelper(logger, "cas-mapping-usecase")} +func NewCASMappingUseCase(repo CASMappingRepo, mRepo MembershipRepo, pRepo ProjectsRepo, logger log.Logger) *CASMappingUseCase { + return &CASMappingUseCase{repo, mRepo, pRepo, servicelogger.ScopedHelper(logger, "cas-mapping-usecase")} } type CASMappingCreateOpts struct { @@ -78,11 +87,11 @@ func (uc *CASMappingUseCase) FindByDigest(ctx context.Context, digest string) ([ return uc.repo.FindByDigest(ctx, digest) } -// FindCASMappingForDownloadByUser returns the CASMapping appropriate for the given digest and user -// This means, in order -// 1 - Any mapping that points to an organization which the user is member of -// 1.1 If there are multiple mappings, it will pick the default one or the first one -// 2 - Any mapping that is public +// FindCASMappingForDownloadByUser returns the CASMapping appropriate for the given digest and user. +// This means, in order: +// 1 - Any mapping that points to an organization which the user is member of. +// 1.1 If there are multiple mappings, it will pick the default one or the first one. +// 2 - Any mapping that is public. func (uc *CASMappingUseCase) FindCASMappingForDownloadByUser(ctx context.Context, digest string, userID string) (*CASMapping, error) { uc.logger.Infow("msg", "finding cas mapping for download", "digest", digest, "user", userID) @@ -91,18 +100,32 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByUser(ctx context.Context return nil, NewErrInvalidUUID(err) } - // Load organizations for the given user - memberships, err := uc.membershipRepo.FindByUser(ctx, userUUID) + // Load ALL memberships for the given user + memberships, err := uc.membershipRepo.ListAllByUser(ctx, userUUID) if err != nil { return nil, fmt.Errorf("failed to list memberships: %w", err) } - userOrgs := make([]string, 0, len(memberships)) + userOrgs := make([]uuid.UUID, 0) + // for every org with RBAC active, the list of allowed projects + projectIDs := make(map[uuid.UUID][]uuid.UUID) for _, m := range memberships { - userOrgs = append(userOrgs, m.OrganizationID.String()) + 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 + } + } } - mapping, err := uc.FindCASMappingForDownloadByOrg(ctx, digest, userOrgs) + mapping, err := uc.FindCASMappingForDownloadByOrg(ctx, digest, userOrgs, projectIDs) if err != nil { return nil, fmt.Errorf("failed to find cas mapping for download: %w", err) } @@ -110,7 +133,9 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByUser(ctx context.Context return mapping, nil } -func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context, digest string, orgs []string) (result *CASMapping, err error) { +// FindCASMappingForDownloadByOrg looks for the CAS mapping to download the referenced artifact in one of the passed organizations. +// The result will get filtered out if RBAC is enabled (projectIDs is not Nil) +func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context, digest string, orgs []uuid.UUID, projectIDs map[uuid.UUID][]uuid.UUID) (result *CASMapping, err error) { if _, err := cr_v1.NewHash(digest); err != nil { return nil, NewErrValidation(fmt.Errorf("invalid digest format: %w", err)) } @@ -139,8 +164,8 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context, return nil, NewErrNotFound("digest not found in any mapping") } - // 2 - CAS mappings associated with the given list of orgs - orgMappings, err := filterByOrgs(mappings, orgs) + // 2 - CAS mappings associated with the given list of orgs and project IDs + orgMappings, err := filterByOrgs(mappings, orgs, projectIDs) if err != nil { return nil, fmt.Errorf("failed to load mappings associated to an user: %w", err) } else if len(orgMappings) > 0 { @@ -159,14 +184,20 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context, return defaultOrFirst(publicMappings), nil } -// Extract only the mappings associated with a list of orgs -func filterByOrgs(mappings []*CASMapping, orgs []string) ([]*CASMapping, error) { +// Extract only the mappings associated with a list of orgs and optionally a list of projects +func filterByOrgs(mappings []*CASMapping, orgs []uuid.UUID, projectIDs map[uuid.UUID][]uuid.UUID) ([]*CASMapping, error) { result := make([]*CASMapping, 0) for _, mapping := range mappings { for _, o := range orgs { - if mapping.OrgID.String() == o { - result = append(result, mapping) + if mapping.OrgID == o { + if visibleProjects, ok := projectIDs[mapping.ProjectID]; ok { + if slices.Contains(visibleProjects, mapping.ProjectID) { + result = append(result, mapping) + } + } else { + result = append(result, mapping) + } } } } diff --git a/app/controlplane/pkg/biz/project.go b/app/controlplane/pkg/biz/project.go index ee72bb33f..f88c83831 100644 --- a/app/controlplane/pkg/biz/project.go +++ b/app/controlplane/pkg/biz/project.go @@ -17,8 +17,11 @@ package biz import ( "context" + "fmt" + "slices" "time" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/pkg/servicelogger" "github.com/go-kratos/kratos/v2/log" @@ -97,3 +100,21 @@ func (uc *ProjectUseCase) Create(ctx context.Context, orgID, name string) (*Proj return uc.projectsRepository.Create(ctx, orgUUID, name) } + +// getProjectsWithMembership returns the list of project IDs in the org for which the user has a membership +func getProjectsWithMembership(ctx context.Context, projectsRepo ProjectsRepo, orgID uuid.UUID, memberships []*Membership) ([]uuid.UUID, error) { + ids := make([]uuid.UUID, 0) + projects, err := 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 ids, nil +} diff --git a/app/controlplane/pkg/biz/referrer.go b/app/controlplane/pkg/biz/referrer.go index 80aa220e6..421a3b010 100644 --- a/app/controlplane/pkg/biz/referrer.go +++ b/app/controlplane/pkg/biz/referrer.go @@ -20,7 +20,6 @@ import ( "errors" "fmt" "io" - "slices" "sort" "time" @@ -184,8 +183,12 @@ func (s *ReferrerUseCase) GetFromRootUser(ctx context.Context, digest, rootKind, 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 := s.getProjectsWithMembership(ctx, m.ResourceID, memberships) + orgProjects, err := getProjectsWithMembership(ctx, s.projectsRepo, m.ResourceID, memberships) if err != nil { return nil, err } @@ -198,24 +201,6 @@ func (s *ReferrerUseCase) GetFromRootUser(ctx context.Context, digest, rootKind, 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 ids, nil -} - func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind string, orgIDs []uuid.UUID, projectIDs []uuid.UUID) (*StoredReferrer, error) { filters := make([]GetFromRootFilter, 0) if rootKind != "" { diff --git a/app/controlplane/pkg/biz/testhelpers/wire_gen.go b/app/controlplane/pkg/biz/testhelpers/wire_gen.go index a8f286a6e..e1aa28443 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire_gen.go +++ b/app/controlplane/pkg/biz/testhelpers/wire_gen.go @@ -116,7 +116,7 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r robotAccountRepo := data.NewRobotAccountRepo(dataData, logger) robotAccountUseCase := biz.NewRootAccountUseCase(robotAccountRepo, workflowRepo, auth, logger) casMappingRepo := data.NewCASMappingRepo(dataData, casBackendRepo, logger) - casMappingUseCase := biz.NewCASMappingUseCase(casMappingRepo, membershipRepo, logger) + casMappingUseCase := biz.NewCASMappingUseCase(casMappingRepo, membershipRepo, projectsRepo, logger) orgInvitationRepo := data.NewOrgInvitation(dataData, logger) orgInvitationUseCase, err := biz.NewOrgInvitationUseCase(orgInvitationRepo, membershipRepo, userRepo, auditorUseCase, logger) if err != nil { diff --git a/app/controlplane/pkg/data/casmapping.go b/app/controlplane/pkg/data/casmapping.go index dce6704b4..7bf136d33 100644 --- a/app/controlplane/pkg/data/casmapping.go +++ b/app/controlplane/pkg/data/casmapping.go @@ -165,5 +165,6 @@ func entCASMappingToBiz(input *ent.CASMapping, public bool) (*biz.CASMapping, er OrgID: input.OrganizationID, CreatedAt: toTimePtr(input.CreatedAt), Public: public, + ProjectID: input.ProjectID, }, nil } From 4a26b0b10ca2505e30e61fed1152f87fe9f79cfc Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Jun 2025 10:03:34 +0200 Subject: [PATCH 05/13] fix Signed-off-by: Jose I. Paris --- app/controlplane/pkg/biz/casmapping.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controlplane/pkg/biz/casmapping.go b/app/controlplane/pkg/biz/casmapping.go index d7788c4f0..58ded86da 100644 --- a/app/controlplane/pkg/biz/casmapping.go +++ b/app/controlplane/pkg/biz/casmapping.go @@ -191,7 +191,7 @@ func filterByOrgs(mappings []*CASMapping, orgs []uuid.UUID, projectIDs map[uuid. for _, mapping := range mappings { for _, o := range orgs { if mapping.OrgID == o { - if visibleProjects, ok := projectIDs[mapping.ProjectID]; ok { + if visibleProjects, ok := projectIDs[mapping.OrgID]; ok { if slices.Contains(visibleProjects, mapping.ProjectID) { result = append(result, mapping) } From bf7477a4f6370f7d884a4fcbf5d26a65af1c57c2 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Jun 2025 11:49:16 +0200 Subject: [PATCH 06/13] use default backend only if admin Signed-off-by: Jose I. Paris --- .../internal/service/cascredential.go | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index f0075a972..46eb318d3 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -84,13 +84,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS return nil, errors.Forbidden("forbidden", "not allowed to perform this operation") } - // Load the default CAS backend, we'll use it for uploads and as fallback on downloads - backend, err := s.casBackendUC.FindDefaultBackend(ctx, currentOrg.ID) - if err != nil && !biz.IsNotFound(err) { - return nil, handleUseCaseErr(err, s.log) - } else if backend == nil { - return nil, errors.NotFound("not found", "main CAS backend not found") - } + var backend *biz.CASBackend // Try to find the proper backend where the artifact is stored if role == casJWT.Downloader { @@ -121,6 +115,21 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS } } + // If the backend was not found, use the default backend for the current org, only if the user is admin and RBAC doesn't apply. + if backend == nil { + if currentAuthzSubject == string(authz.RoleAdmin) || currentAuthzSubject == string(authz.RoleOwner) { + // Load the default CAS backend, we'll use it for uploads and as fallback on downloads + backend, err = s.casBackendUC.FindDefaultBackend(ctx, currentOrg.ID) + if err != nil && !biz.IsNotFound(err) { + return nil, handleUseCaseErr(err, s.log) + } else if backend == nil { + return nil, errors.NotFound("not found", "main CAS backend not found") + } + } else { + return nil, errors.Forbidden("forbidden", "operation not allowed") + } + } + // inline backends don't have a download URL if backend.Inline { return nil, errors.BadRequest("invalid argument", "cannot upload or download artifacts from an inline CAS backend") From 58131f45887bac61bae6ba9f518b205b46e98278 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Jun 2025 11:54:16 +0200 Subject: [PATCH 07/13] apply suggestions Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/cascredential.go | 1 + app/controlplane/pkg/biz/casmapping.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index 46eb318d3..bc7ba4c36 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -98,6 +98,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS if err != nil { return nil, handleUseCaseErr(err, s.log) } + // TODO: pass projectIDs when RBAC for API tokens is supported mapping, err = s.casMappingUC.FindCASMappingForDownloadByOrg(ctx, req.Digest, []uuid.UUID{orgID}, nil) } diff --git a/app/controlplane/pkg/biz/casmapping.go b/app/controlplane/pkg/biz/casmapping.go index 58ded86da..e7bd9dce9 100644 --- a/app/controlplane/pkg/biz/casmapping.go +++ b/app/controlplane/pkg/biz/casmapping.go @@ -107,7 +107,7 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByUser(ctx context.Context } userOrgs := make([]uuid.UUID, 0) - // for every org with RBAC active, the list of allowed projects + // 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 { From 4cb7d3301e9eba0aa26c6359e797de6676ec4f8d Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Jun 2025 12:11:50 +0200 Subject: [PATCH 08/13] fix tests Signed-off-by: Jose I. Paris --- .../pkg/biz/casmapping_integration_test.go | 41 ++++++++----------- app/controlplane/pkg/biz/casmapping_test.go | 34 +++++---------- .../pkg/biz/mocks/CASMappingRepo.go | 18 ++++---- 3 files changed, 37 insertions(+), 56 deletions(-) diff --git a/app/controlplane/pkg/biz/casmapping_integration_test.go b/app/controlplane/pkg/biz/casmapping_integration_test.go index 918f6979a..98b43e149 100644 --- a/app/controlplane/pkg/biz/casmapping_integration_test.go +++ b/app/controlplane/pkg/biz/casmapping_integration_test.go @@ -48,15 +48,15 @@ func (s *casMappingIntegrationSuite) TestCASMappingForDownloadUser() { // 3. Digest: validDigest2, CASBackend: casBackend2, WorkflowRunID: workflowRun // 4. Digest: validDigest3, CASBackend: casBackend3, WorkflowRunID: workflowRun // 4. Digest: validDigestPublic, CASBackend: casBackend3, WorkflowRunID: workflowRunPublic - _, err := s.CASMapping.Create(context.TODO(), validDigest, s.casBackend1.ID.String(), s.workflowRun.ID.String()) + _, err := s.CASMapping.Create(context.TODO(), validDigest, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID}) require.NoError(s.T(), err) - _, err = s.CASMapping.Create(context.TODO(), validDigest, s.casBackend2.ID.String(), s.workflowRun.ID.String()) + _, err = s.CASMapping.Create(context.TODO(), validDigest, s.casBackend2.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID}) require.NoError(s.T(), err) - _, err = s.CASMapping.Create(context.TODO(), validDigest2, s.casBackend2.ID.String(), s.workflowRun.ID.String()) + _, err = s.CASMapping.Create(context.TODO(), validDigest2, s.casBackend2.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID}) require.NoError(s.T(), err) - _, err = s.CASMapping.Create(context.TODO(), validDigest3, s.casBackend3.ID.String(), s.workflowRun.ID.String()) + _, err = s.CASMapping.Create(context.TODO(), validDigest3, s.casBackend3.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID}) require.NoError(s.T(), err) - _, err = s.CASMapping.Create(context.TODO(), validDigestPublic, s.casBackend3.ID.String(), s.publicWorkflowRun.ID.String()) + _, err = s.CASMapping.Create(context.TODO(), validDigestPublic, s.casBackend3.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.publicWorkflowRun.ID}) require.NoError(s.T(), err) // Since the userOrg1And2 is member of org1 and org2, she should be able to download @@ -118,41 +118,41 @@ func (s *casMappingIntegrationSuite) TestCASMappingForDownloadUser() { func (s *casMappingIntegrationSuite) TestCASMappingForDownloadByOrg() { ctx := context.Background() - _, err := s.CASMapping.Create(ctx, validDigest, s.casBackend1.ID.String(), s.workflowRun.ID.String()) + _, err := s.CASMapping.Create(ctx, validDigest, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID}) require.NoError(s.T(), err) - _, err = s.CASMapping.Create(ctx, validDigestPublic, s.casBackend3.ID.String(), s.publicWorkflowRun.ID.String()) + _, err = s.CASMapping.Create(ctx, validDigestPublic, s.casBackend3.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.publicWorkflowRun.ID}) require.NoError(s.T(), err) - _, err = s.CASMapping.Create(ctx, validDigestWithoutRun, s.casBackend3.ID.String(), "") + _, err = s.CASMapping.Create(ctx, validDigestWithoutRun, s.casBackend3.ID.String(), nil) require.NoError(s.T(), err) // both validDigest and validDigest2 from two different orgs s.Run("validDigest is in org1", func() { - mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest, []string{s.org1.ID}) + mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest, []uuid.UUID{uuid.MustParse(s.org1.ID)}, nil) s.NoError(err) s.NotNil(mapping) s.Equal(s.casBackend1.ID, mapping.CASBackend.ID) }) s.Run("validDigestPublic is available from any org", func() { - mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestPublic, []string{uuid.NewString()}) + mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestPublic, []uuid.UUID{uuid.New()}, nil) s.NoError(err) s.NotNil(mapping) s.Equal(s.casBackend3.ID, mapping.CASBackend.ID) }) s.Run("validDigestWithoutRun is available only to org 3", func() { - mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestWithoutRun, []string{s.casBackend3.OrganizationID.String()}) + mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestWithoutRun, []uuid.UUID{s.casBackend3.OrganizationID}, nil) s.NoError(err) s.NotNil(mapping) s.Equal(s.casBackend3.ID, mapping.CASBackend.ID) - mapping, err = s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestWithoutRun, []string{s.org1.ID}) + mapping, err = s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestWithoutRun, []uuid.UUID{uuid.MustParse(s.org1.ID)}, nil) s.Error(err) s.Nil(mapping) }) s.Run("can't find an invalid digest", func() { - mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, invalidDigest, []string{s.org1.ID}) + mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, invalidDigest, []uuid.UUID{uuid.MustParse(s.org1.ID)}, nil) s.Error(err) s.Nil(mapping) }) @@ -163,13 +163,13 @@ func (s *casMappingIntegrationSuite) TestFindByDigest() { // 2. Digest: validDigest2, CASBackend: casBackend1, WorkflowRunID: workflowRun // 3. Digest: validDigest, CASBackend: casBackend2, WorkflowRunID: workflowRun // 4. Digest: validDigest, CASBackend: casBackend3, WorkflowRunID: publicWorkflowRun - _, err := s.CASMapping.Create(context.TODO(), validDigest, s.casBackend1.ID.String(), s.workflowRun.ID.String()) + _, err := s.CASMapping.Create(context.TODO(), validDigest, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID}) require.NoError(s.T(), err) - _, err = s.CASMapping.Create(context.TODO(), validDigest2, s.casBackend1.ID.String(), s.workflowRun.ID.String()) + _, err = s.CASMapping.Create(context.TODO(), validDigest2, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID}) require.NoError(s.T(), err) - _, err = s.CASMapping.Create(context.TODO(), validDigest, s.casBackend2.ID.String(), s.workflowRun.ID.String()) + _, err = s.CASMapping.Create(context.TODO(), validDigest, s.casBackend2.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID}) require.NoError(s.T(), err) - _, err = s.CASMapping.Create(context.TODO(), validDigest, s.casBackend3.ID.String(), s.publicWorkflowRun.ID.String()) + _, err = s.CASMapping.Create(context.TODO(), validDigest, s.casBackend3.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.publicWorkflowRun.ID}) require.NoError(s.T(), err) testcases := []struct { @@ -320,12 +320,7 @@ func (s *casMappingIntegrationSuite) TestCreate() { } s.Run(tc.name, func() { - var workflowRunID string - if tc.workflowRunID != nil { - workflowRunID = tc.workflowRunID.String() - } - - got, err := s.CASMapping.Create(context.TODO(), tc.digest, tc.casBackendID.String(), workflowRunID) + got, err := s.CASMapping.Create(context.TODO(), tc.digest, tc.casBackendID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: tc.workflowRunID}) if tc.wantErr { s.Error(err) } else { diff --git a/app/controlplane/pkg/biz/casmapping_test.go b/app/controlplane/pkg/biz/casmapping_test.go index 1fe87a90b..a91da3631 100644 --- a/app/controlplane/pkg/biz/casmapping_test.go +++ b/app/controlplane/pkg/biz/casmapping_test.go @@ -34,48 +34,34 @@ import ( func (s *casMappingSuite) TestCreate() { validUUID := uuid.New() - invalidUUID := "deadbeef" validDigest := "sha256:3b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d" invalidDigest := "sha256:deadbeef" testCases := []struct { - name string - digest string - casBackendID, workflowRunID string - wantErr bool + name string + digest string + casBackendID string + workflowRunID *uuid.UUID + wantErr bool }{ { name: "valid", digest: validDigest, casBackendID: validUUID.String(), - workflowRunID: validUUID.String(), + workflowRunID: &validUUID, }, { name: "invalid digest format", digest: invalidDigest, casBackendID: validUUID.String(), - workflowRunID: validUUID.String(), + workflowRunID: &validUUID, wantErr: true, }, { name: "invalid digest missing prefix", digest: "3b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d", casBackendID: validUUID.String(), - workflowRunID: validUUID.String(), - wantErr: true, - }, - { - name: "invalid CASBackend", - digest: validDigest, - casBackendID: invalidUUID, - workflowRunID: validUUID.String(), - wantErr: true, - }, - { - name: "invalid WorkflowRunID", - digest: validDigest, - casBackendID: validUUID.String(), - workflowRunID: invalidUUID, + workflowRunID: &validUUID, wantErr: true, }, } @@ -94,7 +80,7 @@ func (s *casMappingSuite) TestCreate() { for _, tc := range testCases { s.Run(tc.name, func() { - got, err := s.useCase.Create(context.TODO(), tc.digest, tc.casBackendID, tc.workflowRunID) + got, err := s.useCase.Create(context.TODO(), tc.digest, tc.casBackendID, &biz.CASMappingCreateOpts{WorkflowRunID: tc.workflowRunID}) if tc.wantErr { s.Error(err) } else { @@ -176,7 +162,7 @@ type casMappingSuite struct { func (s *casMappingSuite) SetupTest() { s.repo = repoM.NewCASMappingRepo(s.T()) - s.useCase = biz.NewCASMappingUseCase(s.repo, nil, nil) + s.useCase = biz.NewCASMappingUseCase(s.repo, nil, nil, nil) } func TestCASMapping(t *testing.T) { diff --git a/app/controlplane/pkg/biz/mocks/CASMappingRepo.go b/app/controlplane/pkg/biz/mocks/CASMappingRepo.go index 6665cf7a8..c0b67fb0b 100644 --- a/app/controlplane/pkg/biz/mocks/CASMappingRepo.go +++ b/app/controlplane/pkg/biz/mocks/CASMappingRepo.go @@ -17,9 +17,9 @@ type CASMappingRepo struct { mock.Mock } -// Create provides a mock function with given fields: ctx, digest, casBackendID, workflowRunID -func (_m *CASMappingRepo) Create(ctx context.Context, digest string, casBackendID uuid.UUID, workflowRunID *uuid.UUID) (*biz.CASMapping, error) { - ret := _m.Called(ctx, digest, casBackendID, workflowRunID) +// Create provides a mock function with given fields: ctx, digest, casBackendID, opts +func (_m *CASMappingRepo) Create(ctx context.Context, digest string, casBackendID uuid.UUID, opts *biz.CASMappingCreateOpts) (*biz.CASMapping, error) { + ret := _m.Called(ctx, digest, casBackendID, opts) if len(ret) == 0 { panic("no return value specified for Create") @@ -27,19 +27,19 @@ func (_m *CASMappingRepo) Create(ctx context.Context, digest string, casBackendI var r0 *biz.CASMapping var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, uuid.UUID, *uuid.UUID) (*biz.CASMapping, error)); ok { - return rf(ctx, digest, casBackendID, workflowRunID) + if rf, ok := ret.Get(0).(func(context.Context, string, uuid.UUID, *biz.CASMappingCreateOpts) (*biz.CASMapping, error)); ok { + return rf(ctx, digest, casBackendID, opts) } - if rf, ok := ret.Get(0).(func(context.Context, string, uuid.UUID, *uuid.UUID) *biz.CASMapping); ok { - r0 = rf(ctx, digest, casBackendID, workflowRunID) + if rf, ok := ret.Get(0).(func(context.Context, string, uuid.UUID, *biz.CASMappingCreateOpts) *biz.CASMapping); ok { + r0 = rf(ctx, digest, casBackendID, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*biz.CASMapping) } } - if rf, ok := ret.Get(1).(func(context.Context, string, uuid.UUID, *uuid.UUID) error); ok { - r1 = rf(ctx, digest, casBackendID, workflowRunID) + if rf, ok := ret.Get(1).(func(context.Context, string, uuid.UUID, *biz.CASMappingCreateOpts) error); ok { + r1 = rf(ctx, digest, casBackendID, opts) } else { r1 = ret.Error(1) } From a27e718a9ff4d54858a04498aea03fce675d4eef Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Jun 2025 12:14:37 +0200 Subject: [PATCH 09/13] fix tests Signed-off-by: Jose I. Paris --- app/controlplane/pkg/biz/casmapping_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controlplane/pkg/biz/casmapping_test.go b/app/controlplane/pkg/biz/casmapping_test.go index a91da3631..3979a4ead 100644 --- a/app/controlplane/pkg/biz/casmapping_test.go +++ b/app/controlplane/pkg/biz/casmapping_test.go @@ -76,7 +76,7 @@ func (s *casMappingSuite) TestCreate() { } // Mock successful repo call - s.repo.On("Create", mock.Anything, validDigest, validUUID, biz.ToPtr(validUUID)).Return(want, nil).Maybe() + s.repo.On("Create", mock.Anything, validDigest, validUUID, mock.Anything).Return(want, nil).Maybe() for _, tc := range testCases { s.Run(tc.name, func() { From 54a2e5eb0d568338a76c89d86e82538521480de9 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Jun 2025 12:17:25 +0200 Subject: [PATCH 10/13] log project ID Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/attestation.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 1e674ed60..ec250ae45 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -317,7 +317,7 @@ func (s *AttestationService) storeAttestation(ctx context.Context, envelope []by } for _, ref := range references { - s.log.Infow("msg", "creating CAS mapping", "name", ref.Name, "digest", ref.Digest, "workflowRun", workflowRunID, "casBackend", casBackend.ID.String()) + s.log.Infow("msg", "creating CAS mapping", "name", ref.Name, "digest", ref.Digest, "project", wf.ProjectID.String(), "workflowRun", workflowRunID, "casBackend", casBackend.ID.String()) if _, err := s.casMappingUseCase.Create(ctx, ref.Digest, casBackend.ID.String(), &biz.CASMappingCreateOpts{ WorkflowRunID: &wfRun.ID, ProjectID: &wf.ProjectID, From 41222c0d7d3f60fe57632209817cf164800befb0 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Jun 2025 12:24:20 +0200 Subject: [PATCH 11/13] fix err assignment Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/cascredential.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index bc7ba4c36..a3e0f5f47 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -89,12 +89,14 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS // Try to find the proper backend where the artifact is stored if role == casJWT.Downloader { var mapping *biz.CASMapping + // If we are logged in as a user, we'll try to find a mapping for that user if currentUser != nil { mapping, err = s.casMappingUC.FindCASMappingForDownloadByUser(ctx, req.Digest, currentUser.ID) // otherwise, we'll try to find a mapping for the current API token associated orgs } else if currentAPIToken != nil { - orgID, err := uuid.Parse(currentOrg.ID) + var orgID uuid.UUID + orgID, err = uuid.Parse(currentOrg.ID) if err != nil { return nil, handleUseCaseErr(err, s.log) } From d8db35ebbc1fe5ce12f1a8744e7a1a4ed3fdda5c Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Jun 2025 12:24:44 +0200 Subject: [PATCH 12/13] lint Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/cascredential.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index a3e0f5f47..4a06b9e68 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -89,7 +89,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS // Try to find the proper backend where the artifact is stored if role == casJWT.Downloader { var mapping *biz.CASMapping - + // If we are logged in as a user, we'll try to find a mapping for that user if currentUser != nil { mapping, err = s.casMappingUC.FindCASMappingForDownloadByUser(ctx, req.Digest, currentUser.ID) From 1170b1bac089e3220b3a62dcedd181b2d258c443 Mon Sep 17 00:00:00 2001 From: "Jose I. Paris" Date: Thu, 26 Jun 2025 12:54:06 +0200 Subject: [PATCH 13/13] fix lint Signed-off-by: Jose I. Paris --- app/controlplane/internal/service/casredirect.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controlplane/internal/service/casredirect.go b/app/controlplane/internal/service/casredirect.go index c3f83d3e2..9757b0193 100644 --- a/app/controlplane/internal/service/casredirect.go +++ b/app/controlplane/internal/service/casredirect.go @@ -83,7 +83,8 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown if currentUser != nil { mapping, err = s.casMappingUC.FindCASMappingForDownloadByUser(ctx, req.Digest, currentUser.ID) } else if currentAPIToken != nil { - orgID, err := uuid.Parse(currentOrg.ID) + var orgID uuid.UUID + orgID, err = uuid.Parse(currentOrg.ID) if err != nil { return nil, handleUseCaseErr(err, s.log) }