diff --git a/app/cli/pkg/action/referrer_discover.go b/app/cli/pkg/action/referrer_discover.go index 3f34652a0..410a74329 100644 --- a/app/cli/pkg/action/referrer_discover.go +++ b/app/cli/pkg/action/referrer_discover.go @@ -67,6 +67,9 @@ func NewReferrerDiscoverPublicIndex(cfg *ActionsOpts) *ReferrerDiscoverPublic { return &ReferrerDiscoverPublic{cfg} } +// Run calls the deprecated public shared index RPC, kept for backwards compatibility. +// +//nolint:staticcheck // the RPC is deprecated but still supported func (action *ReferrerDiscoverPublic) Run(ctx context.Context, digest, kind string, p *PaginationOpts) (*ReferrerDiscoverResult, error) { client := pb.NewReferrerServiceClient(action.cfg.CPConnection) resp, err := client.DiscoverPublicShared(ctx, &pb.DiscoverPublicSharedRequest{ diff --git a/app/controlplane/api/controlplane/v1/referrer.pb.go b/app/controlplane/api/controlplane/v1/referrer.pb.go index f7fe021ee..d98b09eb7 100644 --- a/app/controlplane/api/controlplane/v1/referrer.pb.go +++ b/app/controlplane/api/controlplane/v1/referrer.pb.go @@ -49,9 +49,16 @@ type ReferrerServiceDiscoverPrivateRequest struct { // Used to filter and resolve ambiguities Kind string `protobuf:"bytes,2,opt,name=kind,proto3" json:"kind,omitempty"` // Pagination options for the references list - Pagination *CursorPaginationRequest `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Pagination *CursorPaginationRequest `protobuf:"bytes,3,opt,name=pagination,proto3" json:"pagination,omitempty"` + // ProjectName optionally scopes the discovery to a project by name. + // Must be set together with project_version. + ProjectName string `protobuf:"bytes,4,opt,name=project_name,json=projectName,proto3" json:"project_name,omitempty"` + // ProjectVersion optionally scopes the discovery to a project version (by name, e.g. v1.2.0). + // The referrer and its references are confined to this project version. + // Must be set together with project_name. + ProjectVersion string `protobuf:"bytes,5,opt,name=project_version,json=projectVersion,proto3" json:"project_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReferrerServiceDiscoverPrivateRequest) Reset() { @@ -105,7 +112,24 @@ func (x *ReferrerServiceDiscoverPrivateRequest) GetPagination() *CursorPaginatio return nil } +func (x *ReferrerServiceDiscoverPrivateRequest) GetProjectName() string { + if x != nil { + return x.ProjectName + } + return "" +} + +func (x *ReferrerServiceDiscoverPrivateRequest) GetProjectVersion() string { + if x != nil { + return x.ProjectVersion + } + return "" +} + // DiscoverPublicSharedRequest is the request for the DiscoverPublicShared method +// Deprecated: the public shared index is being retired. +// +// Deprecated: Marked as deprecated in controlplane/v1/referrer.proto. type DiscoverPublicSharedRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Digest is the unique identifier of the referrer to discover @@ -393,21 +417,24 @@ var File_controlplane_v1_referrer_proto protoreflect.FileDescriptor const file_controlplane_v1_referrer_proto_rawDesc = "" + "\n" + - "\x1econtrolplane/v1/referrer.proto\x12\x0fcontrolplane.v1\x1a\x1bbuf/validate/validate.proto\x1a controlplane/v1/pagination.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"\xfc\x01\n" + + "\x1econtrolplane/v1/referrer.proto\x12\x0fcontrolplane.v1\x1a\x1bbuf/validate/validate.proto\x1a controlplane/v1/pagination.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a.protoc-gen-openapiv2/options/annotations.proto\"\xe7\x03\n" + "%ReferrerServiceDiscoverPrivateRequest\x12\x1f\n" + "\x06digest\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x06digest\x12\x12\n" + "\x04kind\x18\x02 \x01(\tR\x04kind\x12H\n" + "\n" + "pagination\x18\x03 \x01(\v2(.controlplane.v1.CursorPaginationRequestR\n" + - "pagination:T\x92AQ\n" + - "O*%ReferrerServiceDiscoverPrivateRequest2&Request to discover a private referrer\"\xee\x01\n" + + "pagination\x12!\n" + + "\fproject_name\x18\x04 \x01(\tR\vprojectName\x12'\n" + + "\x0fproject_version\x18\x05 \x01(\tR\x0eprojectVersion:\xf2\x01\x92AQ\n" + + "O*%ReferrerServiceDiscoverPrivateRequest2&Request to discover a private referrer\xbaH\x9a\x01\x1a\x97\x01\n" + + "#discover_project_version_dependency\x125project_name and project_version must be set together\x1a9(this.project_name == '') == (this.project_version == '')\"\xf0\x01\n" + "\x1bDiscoverPublicSharedRequest\x12\x1f\n" + "\x06digest\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x06digest\x12\x12\n" + "\x04kind\x18\x02 \x01(\tR\x04kind\x12H\n" + "\n" + "pagination\x18\x03 \x01(\v2(.controlplane.v1.CursorPaginationRequestR\n" + - "pagination:P\x92AM\n" + - "K*\x1bDiscoverPublicSharedRequest2,Request to discover a public shared referrer\"\xf3\x01\n" + + "pagination:R\x92AM\n" + + "K*\x1bDiscoverPublicSharedRequest2,Request to discover a public shared referrer\x18\x01\"\xf3\x01\n" + "\x1cDiscoverPublicSharedResponse\x125\n" + "\x06result\x18\x01 \x01(\v2\x1d.controlplane.v1.ReferrerItemR\x06result\x12I\n" + "\n" + @@ -438,10 +465,10 @@ const file_controlplane_v1_referrer_proto_rawDesc = "" + "\x10AnnotationsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01:B\x92A?\n" + - "=*\fReferrerItem2-It represents a referrer object in the system2\xa9\x05\n" + + "=*\fReferrerItem2-It represents a referrer object in the system2\xac\x05\n" + "\x0fReferrerService\x12\xa9\x02\n" + - "\x0fDiscoverPrivate\x126.controlplane.v1.ReferrerServiceDiscoverPrivateRequest\x1a7.controlplane.v1.ReferrerServiceDiscoverPrivateResponse\"\xa4\x01\x92A\x86\x01\x12\x19Discover private referrer\x1aWReturns the referrer item for a given digest in the organizations of the logged-in user:\x10application/json\x82\xd3\xe4\x93\x02\x14\x12\x12/discover/{digest}\x12\x96\x02\n" + - "\x14DiscoverPublicShared\x12,.controlplane.v1.DiscoverPublicSharedRequest\x1a-.controlplane.v1.DiscoverPublicSharedResponse\"\xa0\x01\x92A|\x12\x1fDiscover public shared referrer\x1aGReturns the referrer item for a given digest in the public shared index:\x10application/json\x82\xd3\xe4\x93\x02\x1b\x12\x19/discover/shared/{digest}\x1aQ\x92AN\n" + + "\x0fDiscoverPrivate\x126.controlplane.v1.ReferrerServiceDiscoverPrivateRequest\x1a7.controlplane.v1.ReferrerServiceDiscoverPrivateResponse\"\xa4\x01\x92A\x86\x01\x12\x19Discover private referrer\x1aWReturns the referrer item for a given digest in the organizations of the logged-in user:\x10application/json\x82\xd3\xe4\x93\x02\x14\x12\x12/discover/{digest}\x12\x99\x02\n" + + "\x14DiscoverPublicShared\x12,.controlplane.v1.DiscoverPublicSharedRequest\x1a-.controlplane.v1.DiscoverPublicSharedResponse\"\xa3\x01\x92A|\x12\x1fDiscover public shared referrer\x1aGReturns the referrer item for a given digest in the public shared index:\x10application/json\x82\xd3\xe4\x93\x02\x1b\x12\x19/discover/shared/{digest}\x88\x02\x01\x1aQ\x92AN\n" + "\x0fReferrerService\x12;Referrer service for discovering referred content by digestBLZJgithub.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1;v1b\x06proto3" var ( diff --git a/app/controlplane/api/controlplane/v1/referrer.proto b/app/controlplane/api/controlplane/v1/referrer.proto index 96f1b86df..525dcf3db 100644 --- a/app/controlplane/api/controlplane/v1/referrer.proto +++ b/app/controlplane/api/controlplane/v1/referrer.proto @@ -36,7 +36,9 @@ service ReferrerService { }; } // DiscoverPublicShared returns the referrer item for a given digest in the public shared index + // Deprecated: the public shared index is being retired. rpc DiscoverPublicShared(DiscoverPublicSharedRequest) returns (DiscoverPublicSharedResponse) { + option deprecated = true; option (google.api.http) = {get: "/discover/shared/{digest}"}; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Discover public shared referrer" @@ -60,6 +62,21 @@ message ReferrerServiceDiscoverPrivateRequest { string kind = 2; // Pagination options for the references list CursorPaginationRequest pagination = 3; + // ProjectName optionally scopes the discovery to a project by name. + // Must be set together with project_version. + string project_name = 4; + // ProjectVersion optionally scopes the discovery to a project version (by name, e.g. v1.2.0). + // The referrer and its references are confined to this project version. + // Must be set together with project_name. + string project_version = 5; + + // project_name and project_version must be provided together: a version name is unique only + // within a project, and a project name on its own would not scope the discovery. + option (buf.validate.message).cel = { + id: "discover_project_version_dependency" + expression: "(this.project_name == '') == (this.project_version == '')" + message: "project_name and project_version must be set together" + }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { @@ -70,7 +87,10 @@ message ReferrerServiceDiscoverPrivateRequest { } // DiscoverPublicSharedRequest is the request for the DiscoverPublicShared method +// Deprecated: the public shared index is being retired. message DiscoverPublicSharedRequest { + option deprecated = true; + // Digest is the unique identifier of the referrer to discover string digest = 1 [(buf.validate.field).string = {min_len: 1}]; // Kind is the optional type of referrer, i.e CONTAINER_IMAGE, GIT_HEAD, ... diff --git a/app/controlplane/api/controlplane/v1/referrer_grpc.pb.go b/app/controlplane/api/controlplane/v1/referrer_grpc.pb.go index b95175c80..3f2303ee9 100644 --- a/app/controlplane/api/controlplane/v1/referrer_grpc.pb.go +++ b/app/controlplane/api/controlplane/v1/referrer_grpc.pb.go @@ -44,7 +44,9 @@ const ( type ReferrerServiceClient interface { // DiscoverPrivate returns the referrer item for a given digest in the organizations of the logged-in user DiscoverPrivate(ctx context.Context, in *ReferrerServiceDiscoverPrivateRequest, opts ...grpc.CallOption) (*ReferrerServiceDiscoverPrivateResponse, error) + // Deprecated: Do not use. // DiscoverPublicShared returns the referrer item for a given digest in the public shared index + // Deprecated: the public shared index is being retired. DiscoverPublicShared(ctx context.Context, in *DiscoverPublicSharedRequest, opts ...grpc.CallOption) (*DiscoverPublicSharedResponse, error) } @@ -65,6 +67,7 @@ func (c *referrerServiceClient) DiscoverPrivate(ctx context.Context, in *Referre return out, nil } +// Deprecated: Do not use. func (c *referrerServiceClient) DiscoverPublicShared(ctx context.Context, in *DiscoverPublicSharedRequest, opts ...grpc.CallOption) (*DiscoverPublicSharedResponse, error) { out := new(DiscoverPublicSharedResponse) err := c.cc.Invoke(ctx, ReferrerService_DiscoverPublicShared_FullMethodName, in, out, opts...) @@ -80,7 +83,9 @@ func (c *referrerServiceClient) DiscoverPublicShared(ctx context.Context, in *Di type ReferrerServiceServer interface { // DiscoverPrivate returns the referrer item for a given digest in the organizations of the logged-in user DiscoverPrivate(context.Context, *ReferrerServiceDiscoverPrivateRequest) (*ReferrerServiceDiscoverPrivateResponse, error) + // Deprecated: Do not use. // DiscoverPublicShared returns the referrer item for a given digest in the public shared index + // Deprecated: the public shared index is being retired. DiscoverPublicShared(context.Context, *DiscoverPublicSharedRequest) (*DiscoverPublicSharedResponse, error) mustEmbedUnimplementedReferrerServiceServer() } diff --git a/app/controlplane/api/controlplane/v1/referrer_http.pb.go b/app/controlplane/api/controlplane/v1/referrer_http.pb.go index bf0b017da..14d169f4c 100644 --- a/app/controlplane/api/controlplane/v1/referrer_http.pb.go +++ b/app/controlplane/api/controlplane/v1/referrer_http.pb.go @@ -26,6 +26,7 @@ type ReferrerServiceHTTPServer interface { // DiscoverPrivate DiscoverPrivate returns the referrer item for a given digest in the organizations of the logged-in user DiscoverPrivate(context.Context, *ReferrerServiceDiscoverPrivateRequest) (*ReferrerServiceDiscoverPrivateResponse, error) // DiscoverPublicShared DiscoverPublicShared returns the referrer item for a given digest in the public shared index + // Deprecated: the public shared index is being retired. DiscoverPublicShared(context.Context, *DiscoverPublicSharedRequest) (*DiscoverPublicSharedResponse, error) } diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/referrer.ts b/app/controlplane/api/gen/frontend/controlplane/v1/referrer.ts index b63be3b7a..fc1f05161 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/referrer.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/referrer.ts @@ -18,9 +18,25 @@ export interface ReferrerServiceDiscoverPrivateRequest { kind: string; /** Pagination options for the references list */ pagination?: CursorPaginationRequest; + /** + * ProjectName optionally scopes the discovery to a project by name. + * Must be set together with project_version. + */ + projectName: string; + /** + * ProjectVersion optionally scopes the discovery to a project version (by name, e.g. v1.2.0). + * The referrer and its references are confined to this project version. + * Must be set together with project_name. + */ + projectVersion: string; } -/** DiscoverPublicSharedRequest is the request for the DiscoverPublicShared method */ +/** + * DiscoverPublicSharedRequest is the request for the DiscoverPublicShared method + * Deprecated: the public shared index is being retired. + * + * @deprecated + */ export interface DiscoverPublicSharedRequest { /** Digest is the unique identifier of the referrer to discover */ digest: string; @@ -80,7 +96,7 @@ export interface ReferrerItem_AnnotationsEntry { } function createBaseReferrerServiceDiscoverPrivateRequest(): ReferrerServiceDiscoverPrivateRequest { - return { digest: "", kind: "", pagination: undefined }; + return { digest: "", kind: "", pagination: undefined, projectName: "", projectVersion: "" }; } export const ReferrerServiceDiscoverPrivateRequest = { @@ -94,6 +110,12 @@ export const ReferrerServiceDiscoverPrivateRequest = { if (message.pagination !== undefined) { CursorPaginationRequest.encode(message.pagination, writer.uint32(26).fork()).ldelim(); } + if (message.projectName !== "") { + writer.uint32(34).string(message.projectName); + } + if (message.projectVersion !== "") { + writer.uint32(42).string(message.projectVersion); + } return writer; }, @@ -125,6 +147,20 @@ export const ReferrerServiceDiscoverPrivateRequest = { message.pagination = CursorPaginationRequest.decode(reader, reader.uint32()); continue; + case 4: + if (tag !== 34) { + break; + } + + message.projectName = reader.string(); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.projectVersion = reader.string(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -139,6 +175,8 @@ export const ReferrerServiceDiscoverPrivateRequest = { digest: isSet(object.digest) ? String(object.digest) : "", kind: isSet(object.kind) ? String(object.kind) : "", pagination: isSet(object.pagination) ? CursorPaginationRequest.fromJSON(object.pagination) : undefined, + projectName: isSet(object.projectName) ? String(object.projectName) : "", + projectVersion: isSet(object.projectVersion) ? String(object.projectVersion) : "", }; }, @@ -148,6 +186,8 @@ export const ReferrerServiceDiscoverPrivateRequest = { message.kind !== undefined && (obj.kind = message.kind); message.pagination !== undefined && (obj.pagination = message.pagination ? CursorPaginationRequest.toJSON(message.pagination) : undefined); + message.projectName !== undefined && (obj.projectName = message.projectName); + message.projectVersion !== undefined && (obj.projectVersion = message.projectVersion); return obj; }, @@ -166,6 +206,8 @@ export const ReferrerServiceDiscoverPrivateRequest = { message.pagination = (object.pagination !== undefined && object.pagination !== null) ? CursorPaginationRequest.fromPartial(object.pagination) : undefined; + message.projectName = object.projectName ?? ""; + message.projectVersion = object.projectVersion ?? ""; return message; }, }; @@ -758,7 +800,12 @@ export interface ReferrerService { request: DeepPartial, metadata?: grpc.Metadata, ): Promise; - /** DiscoverPublicShared returns the referrer item for a given digest in the public shared index */ + /** + * DiscoverPublicShared returns the referrer item for a given digest in the public shared index + * Deprecated: the public shared index is being retired. + * + * @deprecated + */ DiscoverPublicShared( request: DeepPartial, metadata?: grpc.Metadata, diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.DiscoverPublicSharedRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.DiscoverPublicSharedRequest.jsonschema.json index 08efa148f..c1e135f13 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.DiscoverPublicSharedRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.DiscoverPublicSharedRequest.jsonschema.json @@ -2,7 +2,7 @@ "$id": "controlplane.v1.DiscoverPublicSharedRequest.jsonschema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "description": "DiscoverPublicSharedRequest is the request for the DiscoverPublicShared method", + "description": "DiscoverPublicSharedRequest is the request for the DiscoverPublicShared method\n Deprecated: the public shared index is being retired.", "properties": { "digest": { "description": "Digest is the unique identifier of the referrer to discover", diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.DiscoverPublicSharedRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.DiscoverPublicSharedRequest.schema.json index 0a175476f..498bb60b5 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.DiscoverPublicSharedRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.DiscoverPublicSharedRequest.schema.json @@ -2,7 +2,7 @@ "$id": "controlplane.v1.DiscoverPublicSharedRequest.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "description": "DiscoverPublicSharedRequest is the request for the DiscoverPublicShared method", + "description": "DiscoverPublicSharedRequest is the request for the DiscoverPublicShared method\n Deprecated: the public shared index is being retired.", "properties": { "digest": { "description": "Digest is the unique identifier of the referrer to discover", diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ReferrerServiceDiscoverPrivateRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ReferrerServiceDiscoverPrivateRequest.jsonschema.json index 535b0abfb..b9e2ed810 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.ReferrerServiceDiscoverPrivateRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ReferrerServiceDiscoverPrivateRequest.jsonschema.json @@ -3,6 +3,16 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "ReferrerServiceDiscoverPrivateRequest is the request for the DiscoverPrivate method", + "patternProperties": { + "^(project_name)$": { + "description": "ProjectName optionally scopes the discovery to a project by name.\n Must be set together with project_version.", + "type": "string" + }, + "^(project_version)$": { + "description": "ProjectVersion optionally scopes the discovery to a project version (by name, e.g. v1.2.0).\n The referrer and its references are confined to this project version.\n Must be set together with project_name.", + "type": "string" + } + }, "properties": { "digest": { "description": "Digest is the unique identifier of the referrer to discover", @@ -16,6 +26,14 @@ "pagination": { "$ref": "controlplane.v1.CursorPaginationRequest.jsonschema.json", "description": "Pagination options for the references list" + }, + "projectName": { + "description": "ProjectName optionally scopes the discovery to a project by name.\n Must be set together with project_version.", + "type": "string" + }, + "projectVersion": { + "description": "ProjectVersion optionally scopes the discovery to a project version (by name, e.g. v1.2.0).\n The referrer and its references are confined to this project version.\n Must be set together with project_name.", + "type": "string" } }, "title": "Referrer Service Discover Private Request", diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.ReferrerServiceDiscoverPrivateRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.ReferrerServiceDiscoverPrivateRequest.schema.json index 339c19728..e252aa99c 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.ReferrerServiceDiscoverPrivateRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.ReferrerServiceDiscoverPrivateRequest.schema.json @@ -3,6 +3,16 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, "description": "ReferrerServiceDiscoverPrivateRequest is the request for the DiscoverPrivate method", + "patternProperties": { + "^(projectName)$": { + "description": "ProjectName optionally scopes the discovery to a project by name.\n Must be set together with project_version.", + "type": "string" + }, + "^(projectVersion)$": { + "description": "ProjectVersion optionally scopes the discovery to a project version (by name, e.g. v1.2.0).\n The referrer and its references are confined to this project version.\n Must be set together with project_name.", + "type": "string" + } + }, "properties": { "digest": { "description": "Digest is the unique identifier of the referrer to discover", @@ -16,6 +26,14 @@ "pagination": { "$ref": "controlplane.v1.CursorPaginationRequest.schema.json", "description": "Pagination options for the references list" + }, + "project_name": { + "description": "ProjectName optionally scopes the discovery to a project by name.\n Must be set together with project_version.", + "type": "string" + }, + "project_version": { + "description": "ProjectVersion optionally scopes the discovery to a project version (by name, e.g. v1.2.0).\n The referrer and its references are confined to this project version.\n Must be set together with project_name.", + "type": "string" } }, "title": "Referrer Service Discover Private Request", diff --git a/app/controlplane/api/gen/openapi/openapi.yaml b/app/controlplane/api/gen/openapi/openapi.yaml index c781887c2..48b94286e 100644 --- a/app/controlplane/api/gen/openapi/openapi.yaml +++ b/app/controlplane/api/gen/openapi/openapi.yaml @@ -102,6 +102,25 @@ paths: schema: format: int32 type: integer + - description: |- + ProjectName optionally scopes the discovery to a project by name. + Must be set together with project_version. + in: query + name: project_name + schema: + type: string + - description: >- + ProjectVersion optionally scopes the discovery to a project version + (by name, e.g. v1.2.0). + + The referrer and its references are confined to this project + version. + + Must be set together with project_name. + in: query + name: project_version + schema: + type: string responses: '200': content: diff --git a/app/controlplane/internal/service/referrer.go b/app/controlplane/internal/service/referrer.go index 0d5cfd61b..f783adb93 100644 --- a/app/controlplane/internal/service/referrer.go +++ b/app/controlplane/internal/service/referrer.go @@ -56,12 +56,19 @@ func (s *ReferrerService) DiscoverPrivate(ctx context.Context, req *pb.ReferrerS return nil, err } + // Optionally scope the discovery to a project version. Both fields are required together, + // which is enforced at the proto validation layer. + var extraFilters []biz.GetFromRootFilter + if req.GetProjectVersion() != "" { + extraFilters = append(extraFilters, biz.WithProjectVersion(req.GetProjectName(), req.GetProjectVersion())) + } + // if we are logged in as user we find the referrer from the user // otherwise for the current organization associated with the API token var referrer *biz.StoredReferrer var nextCursor string if currentUser != nil { - referrer, nextCursor, err = s.referrerUC.GetFromRootUser(ctx, req.GetDigest(), req.GetKind(), currentUser.ID, paginationOpts) + referrer, nextCursor, err = s.referrerUC.GetFromRootUser(ctx, req.GetDigest(), req.GetKind(), currentUser.ID, paginationOpts, extraFilters...) } else if currentToken != nil { var orgUUID uuid.UUID orgUUID, err = uuid.Parse(currentOrg.ID) @@ -76,7 +83,7 @@ func (s *ReferrerService) DiscoverPrivate(ctx context.Context, req *pb.ReferrerS orgsProjectsMap[orgUUID] = visibleProjects } - referrer, nextCursor, err = s.referrerUC.GetFromRoot(ctx, req.GetDigest(), req.GetKind(), []uuid.UUID{orgUUID}, orgsProjectsMap, paginationOpts) + referrer, nextCursor, err = s.referrerUC.GetFromRoot(ctx, req.GetDigest(), req.GetKind(), []uuid.UUID{orgUUID}, orgsProjectsMap, paginationOpts, extraFilters...) } if err != nil { return nil, handleUseCaseErr(err, s.log) @@ -88,6 +95,9 @@ func (s *ReferrerService) DiscoverPrivate(ctx context.Context, req *pb.ReferrerS }, nil } +// DiscoverPublicShared implements the deprecated public shared index RPC, kept for backwards compatibility. +// +//nolint:staticcheck // the RPC is deprecated but still served func (s *ReferrerService) DiscoverPublicShared(ctx context.Context, req *pb.DiscoverPublicSharedRequest) (*pb.DiscoverPublicSharedResponse, error) { paginationOpts, err := referrerPaginationOptsFromProto(req.GetPagination()) if err != nil { diff --git a/app/controlplane/pkg/biz/referrer.go b/app/controlplane/pkg/biz/referrer.go index 48753e44e..c3f179240 100644 --- a/app/controlplane/pkg/biz/referrer.go +++ b/app/controlplane/pkg/biz/referrer.go @@ -132,6 +132,10 @@ type GetFromRootFilters struct { // ProjectIDs stores visible projects by org for the requesting user. // If an org entry doesn't exist, it means that RBAC is not applied, hence all projects in that org are visible ProjectIDs map[OrgID][]ProjectID + // ProjectName and ProjectVersion scope the discovery to a specific project version. + // Both must be set together (a version name is unique only within a project). + ProjectName *string + ProjectVersion *string } type GetFromRootFilter func(*GetFromRootFilters) @@ -142,6 +146,14 @@ func WithKind(kind string) func(*GetFromRootFilters) { } } +// WithProjectVersion scopes the discovery to the given project name and version. +func WithProjectVersion(projectName, projectVersion string) func(*GetFromRootFilters) { + return func(o *GetFromRootFilters) { + o.ProjectName = &projectName + o.ProjectVersion = &projectVersion + } +} + // WithVisibleProjectIDs sets visible projects by org for organizations with RBAC enabled for the user (role is OrgMember) func WithVisibleProjectIDs(projectIDs map[OrgID][]ProjectID) func(*GetFromRootFilters) { return func(o *GetFromRootFilters) { @@ -188,7 +200,7 @@ func (s *ReferrerUseCase) ExtractAndPersist(ctx context.Context, att *dsse.Envel // GetFromRootUser returns the referrer identified by the provided content digest, including its first-level references. // For example if sha:deadbeef represents an attestation, the result will contain the attestation + materials associated to it. // It only returns referrers that belong to organizations the user is member of. -func (s *ReferrerUseCase) GetFromRootUser(ctx context.Context, digest, rootKind, userID string, p *pagination.CursorOptions) (*StoredReferrer, string, error) { +func (s *ReferrerUseCase) GetFromRootUser(ctx context.Context, digest, rootKind, userID string, p *pagination.CursorOptions, extraFilters ...GetFromRootFilter) (*StoredReferrer, string, error) { ctx, span := otelx.Start(ctx, referrerTracer, "ReferrerUseCase.GetFromRootUser") defer span.End() @@ -205,10 +217,10 @@ func (s *ReferrerUseCase) GetFromRootUser(ctx context.Context, digest, rootKind, // We pass the list of organizationsIDs from where to look for the referrer // For now we just pass the list of organizations the user is member of // in the future we will expand this to publicly available orgs and so on. - return s.GetFromRoot(ctx, digest, rootKind, userOrgs, projectIDs, p) + return s.GetFromRoot(ctx, digest, rootKind, userOrgs, projectIDs, p, extraFilters...) } -func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind string, orgIDs []uuid.UUID, projectIDs map[OrgID][]ProjectID, p *pagination.CursorOptions) (*StoredReferrer, string, error) { +func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind string, orgIDs []uuid.UUID, projectIDs map[OrgID][]ProjectID, p *pagination.CursorOptions, extraFilters ...GetFromRootFilter) (*StoredReferrer, string, error) { ctx, span := otelx.Start(ctx, referrerTracer, "ReferrerUseCase.GetFromRoot") defer span.End() @@ -219,6 +231,7 @@ func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind stri if projectIDs != nil { filters = append(filters, WithVisibleProjectIDs(projectIDs)) } + filters = append(filters, extraFilters...) ref, nextCursor, err := s.repo.GetFromRoot(ctx, digest, orgIDs, p, filters...) if err != nil { @@ -276,7 +289,8 @@ func (s *ReferrerUseCase) GetFromRootInPublicSharedIndex(ctx context.Context, di } const ( - referrerAttestationType = "ATTESTATION" + // ReferrerAttestationType is the kind of the referrer that represents an attestation. + ReferrerAttestationType = "ATTESTATION" referrerGitHeadType = "GIT_HEAD_COMMIT" ) @@ -302,11 +316,11 @@ func extractReferrers(att *dsse.Envelope, digest cr_v1.Hash, repo ReferrerRepo) attestationHash := digest.String() attestationReferrer := &Referrer{ Digest: attestationHash, - Kind: referrerAttestationType, + Kind: ReferrerAttestationType, Downloadable: true, } - referrersMap[newRef(attestationHash, referrerAttestationType)] = attestationReferrer + referrersMap[newRef(attestationHash, ReferrerAttestationType)] = attestationReferrer // 2 - Predicate that's referenced from the attestation predicate, err := chainloop.ExtractPredicate(att) @@ -344,8 +358,8 @@ func extractReferrers(att *dsse.Envelope, digest cr_v1.Hash, repo ReferrerRepo) // If we are inserting an attestation as a dependent, we want to make sure it already exists // stored in the system. This is so we can ensure that the attestations nodes are created through // an attestation process, not as a referenced provided by the user - if material.Type == referrerAttestationType { - if exists, err := repo.Exist(context.Background(), material.Hash.String(), WithKind(referrerAttestationType)); err != nil { + if material.Type == ReferrerAttestationType { + if exists, err := repo.Exist(context.Background(), material.Hash.String(), WithKind(ReferrerAttestationType)); err != nil { return nil, fmt.Errorf("checking if attestation exists: %w", err) } else if !exists { return nil, fmt.Errorf("attestation material does not exist %q", material.Hash.String()) diff --git a/app/controlplane/pkg/biz/referrer_integration_test.go b/app/controlplane/pkg/biz/referrer_integration_test.go index 3543fd568..7b8081f25 100644 --- a/app/controlplane/pkg/biz/referrer_integration_test.go +++ b/app/controlplane/pkg/biz/referrer_integration_test.go @@ -20,8 +20,10 @@ import ( "context" "encoding/json" "os" + "strings" "sync" "testing" + "time" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/testhelpers" @@ -531,6 +533,95 @@ func (s *referrerIntegrationTestSuite) TestPagination() { }) } +func (s *referrerIntegrationTestSuite) TestGetFromRootProjectVersionFilter() { + // Load attestation and persist its referrers under workflow1 (project "test") + envelope, envBytes := testEnvelope(s.T(), "testdata/attestations/with-git-subject.json") + h, _, err := v1.SHA256(bytes.NewReader(envBytes)) + require.NoError(s.T(), err) + ctx := context.Background() + + err = s.Referrer.ExtractAndPersist(ctx, envelope, h, s.workflow1.ID.String()) + require.NoError(s.T(), err) + + // The SBOM is one of the materials referenced by the attestation + const sbomDigest = "sha256:16159bb881eb4ab7eb5d8afc5350b0feeed1e31c0a268e355e74f9ccbe885e0c" + + // Create a workflow run for project version "v1.0.0" on workflow1 and link the attestation digest to it + contractVersion, err := s.WorkflowContract.Describe(ctx, s.org1.ID, s.workflow1.ContractID.String(), 0) + require.NoError(s.T(), err) + casBackend, err := s.CASBackend.CreateOrUpdate(ctx, s.org1.ID, "repo", "username", "pass", backendType, true) + require.NoError(s.T(), err) + run, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ + WorkflowID: s.workflow1.ID.String(), ContractRevision: contractVersion, CASBackendID: casBackend.ID, + ProjectVersion: "v1.0.0", + }) + require.NoError(s.T(), err) + require.NoError(s.T(), s.Repos.WorkflowRunRepo.SaveAttestationDigest(ctx, run.ID, h.String())) + + s.Run("attestation root is returned when project+version match", func() { + got, _, err := s.Referrer.GetFromRootUser(ctx, h.String(), "", s.user.ID, nil, biz.WithProjectVersion("test", "v1.0.0")) + s.NoError(err) + s.Require().NotNil(got) + s.Equal(h.String(), got.Digest) + // children (materials) are still returned + s.NotEmpty(got.References) + }) + + s.Run("attestation root is not found for a different version", func() { + got, _, err := s.Referrer.GetFromRootUser(ctx, h.String(), "", s.user.ID, nil, biz.WithProjectVersion("test", "v9.9.9")) + s.True(biz.IsNotFound(err)) + s.Nil(got) + }) + + s.Run("attestation root is not found for a different project", func() { + got, _, err := s.Referrer.GetFromRootUser(ctx, h.String(), "", s.user.ID, nil, biz.WithProjectVersion("does-not-exist", "v1.0.0")) + s.True(biz.IsNotFound(err)) + s.Nil(got) + }) + + s.Run("material root is returned when reachable from an in-version attestation", func() { + got, _, err := s.Referrer.GetFromRootUser(ctx, sbomDigest, "", s.user.ID, nil, biz.WithProjectVersion("test", "v1.0.0")) + s.NoError(err) + s.Require().NotNil(got) + s.Equal(sbomDigest, got.Digest) + // its parent attestation (in version) is returned as a reference + require.Len(s.T(), got.References, 1) + s.Equal(h.String(), got.References[0].Digest) + }) + + s.Run("material root is not found for a different version", func() { + got, _, err := s.Referrer.GetFromRootUser(ctx, sbomDigest, "", s.user.ID, nil, biz.WithProjectVersion("test", "v9.9.9")) + s.True(biz.IsNotFound(err)) + s.Nil(got) + }) + + s.Run("without the filter the referrer is returned regardless of version", func() { + got, _, err := s.Referrer.GetFromRootUser(ctx, h.String(), "", s.user.ID, nil) + s.NoError(err) + s.Require().NotNil(got) + s.Equal(h.String(), got.Digest) + }) + + s.Run("material root cannot bypass version scoping by supplying a cursor", func() { + // A second project version whose run points to an unrelated attestation digest, so the + // SBOM (only referenced by the v1.0.0 attestation) does not belong to it. + run2, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ + WorkflowID: s.workflow1.ID.String(), ContractRevision: contractVersion, CASBackendID: casBackend.ID, + ProjectVersion: "v2.0.0", + }) + require.NoError(s.T(), err) + require.NoError(s.T(), s.Repos.WorkflowRunRepo.SaveAttestationDigest(ctx, run2.ID, "sha256:"+strings.Repeat("a", 64))) + + // Paging past the first page must not skip the membership check (regression for the + // firstPage gate that allowed a cursor to bypass version scoping). + cursor, err := pagination.NewCursor(pagination.EncodeCursor(time.Now(), uuid.New()), 10) + require.NoError(s.T(), err) + got, _, err := s.Referrer.GetFromRootUser(ctx, sbomDigest, "", s.user.ID, cursor, biz.WithProjectVersion("test", "v2.0.0")) + s.True(biz.IsNotFound(err)) + s.Nil(got) + }) +} + type referrerIntegrationTestSuite struct { testhelpers.UseCasesEachTestSuite org1, org2 *biz.Organization diff --git a/app/controlplane/pkg/data/referrer.go b/app/controlplane/pkg/data/referrer.go index b16a78723..5e4fc556f 100644 --- a/app/controlplane/pkg/data/referrer.go +++ b/app/controlplane/pkg/data/referrer.go @@ -25,8 +25,11 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent" "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/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/referrer" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowrun" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/chainloop-dev/chainloop/pkg/otelx" "github.com/go-kratos/kratos/v2/log" @@ -193,8 +196,23 @@ func (r *ReferrerRepo) GetFromRoot(ctx context.Context, digest string, orgIDs [] return nil, "", biz.NewErrReferrerAmbiguous(digest, kinds) } + // If a project version filter is requested, resolve the bounded set of attestation + // digests that belong to that project version. We enter from the version side (its runs) + // so the lookup stays bounded by the size of the version, never by the referrer graph. + var versionAttDigests []string + if opts.ProjectName != nil && opts.ProjectVersion != nil { + versionAttDigests, err = r.attestationDigestsInProjectVersion(ctx, *opts.ProjectName, *opts.ProjectVersion, orgIDs) + if err != nil { + return nil, "", fmt.Errorf("failed to resolve project version: %w", err) + } + // The version has no attestations (unknown project/version or empty), so nothing matches + if len(versionAttDigests) == 0 { + return nil, "", nil + } + } + // Find the referrer recursively starting from the root - res, nextCursor, err := r.doGet(ctx, refs[0], orgIDs, opts.ProjectIDs, opts.Public, p, 0) + res, nextCursor, err := r.doGet(ctx, refs[0], orgIDs, opts.ProjectIDs, opts.Public, versionAttDigests, p, 0) if err != nil && !biz.IsErrUnauthorized(err) { return nil, "", fmt.Errorf("failed to get referrer: %w", err) } @@ -202,12 +220,34 @@ func (r *ReferrerRepo) GetFromRoot(ctx context.Context, digest string, orgIDs [] return res, nextCursor, nil } +// attestationDigestsInProjectVersion returns the attestation digests of the workflow runs that +// belong to the given project name + version, scoped to the allowed organizations. The query +// enters from the project version (a bounded set of runs), so it does not depend on how many +// referrers reference a given material. +func (r *ReferrerRepo) attestationDigestsInProjectVersion(ctx context.Context, projectName, version string, orgIDs []uuid.UUID) ([]string, error) { + return r.data.DB.WorkflowRun.Query(). + Where( + workflowrun.AttestationDigestNEQ(""), + workflowrun.HasVersionWith( + projectversion.VersionEQ(version), + projectversion.DeletedAtIsNil(), + projectversion.HasProjectWith( + project.NameEQ(projectName), + project.DeletedAtIsNil(), + project.HasOrganizationWith(organization.IDIn(orgIDs...)), + ), + ), + ). + Select(workflowrun.FieldAttestationDigest). + Strings(ctx) +} + // max number of recursive levels to traverse // we just care about 1 level, i.e att -> commit, or commit -> attestation // we also need to limit this because there might be cycles const maxTraverseLevels = 1 -func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrgs []uuid.UUID, visibleProjectsMap map[uuid.UUID][]uuid.UUID, public *bool, p *pagination.CursorOptions, level int) (*biz.StoredReferrer, string, error) { +func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrgs []uuid.UUID, visibleProjectsMap map[uuid.UUID][]uuid.UUID, public *bool, versionAttDigests []string, p *pagination.CursorOptions, level int) (*biz.StoredReferrer, string, error) { // Assemble the referrer to return res := &biz.StoredReferrer{ ID: root.ID, @@ -229,6 +269,16 @@ func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrg return nil, "", biz.NewErrUnauthorizedStr("referrer not allowed") } + // When a project version filter is active, the requested root referrer must belong to that + // version. An attestation belongs to the version iff its digest is one of the version's + // attestation digests. A material belongs to the version iff it is referenced by one of those + // attestations, which is enforced below through the references query (an empty result means + // the material is not part of the version). + versionFilterActive := versionAttDigests != nil + if versionFilterActive && level == 0 && root.Kind == biz.ReferrerAttestationType && !slices.Contains(versionAttDigests, root.Digest) { + return nil, "", biz.NewErrUnauthorizedStr("referrer not part of the requested project version") + } + // Next: We'll find the references recursively up to a max of maxTraverseLevels levels if level >= maxTraverseLevels { return res, "", nil @@ -250,6 +300,16 @@ func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrg // Attach the workflow predicate predicateReferrer = append(predicateReferrer, referrer.HasWorkflowsWith(predicateWF...)) + // When scoping to a project version, attestation references must belong to that version. + // Non-attestation references (materials/subjects) are kept as-is: they inherit the version + // through the attestation that references them. + if versionFilterActive { + predicateReferrer = append(predicateReferrer, referrer.Or( + referrer.KindNEQ(biz.ReferrerAttestationType), + referrer.DigestIn(versionAttDigests...), + )) + } + // Defense-in-depth: if the caller did not supply pagination options, fall back // to the package-wide default instead of emitting an unbounded query. This // guarantees the response is bounded even when a future biz-layer caller @@ -289,7 +349,7 @@ func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrg // Add the references to the result for _, reference := range refs { // Call recursively the function — pagination only applies to the first level - ref, _, err := r.doGet(ctx, reference, allowedOrgs, visibleProjectsMap, public, nil, level+1) + ref, _, err := r.doGet(ctx, reference, allowedOrgs, visibleProjectsMap, public, versionAttDigests, nil, level+1) if err != nil && !biz.IsErrUnauthorized(err) { return nil, "", fmt.Errorf("failed to get referrer: %w", err) } @@ -299,6 +359,27 @@ func (r *ReferrerRepo) doGet(ctx context.Context, root *ent.Referrer, allowedOrg } } + // A non-attestation root (a material/subject) belongs to the requested project version only + // if it is referenced by at least one attestation in that version. When the current page + // yields no references we cannot conclude absence from the page alone (a later page can be + // empty simply because we paged past the results), so we run a pagination-independent + // existence check before rejecting the root. + if versionFilterActive && level == 0 && root.Kind != biz.ReferrerAttestationType && len(res.References) == 0 { + inVersion, err := root.QueryReferences(). + Where( + referrer.KindEQ(biz.ReferrerAttestationType), + referrer.DigestIn(versionAttDigests...), + referrer.HasWorkflowsWith(predicateWF...), + ). + Exist(ctx) + if err != nil { + return nil, "", fmt.Errorf("failed to validate project version membership: %w", err) + } + if !inVersion { + return nil, "", biz.NewErrUnauthorizedStr("referrer not part of the requested project version") + } + } + return res, nextCursor, nil }