diff --git a/app/cli/cmd/artifact.go b/app/cli/cmd/artifact.go index 996130123..911a4c491 100644 --- a/app/cli/cmd/artifact.go +++ b/app/cli/cmd/artifact.go @@ -35,11 +35,12 @@ func newArtifactCmd() *cobra.Command { return cmd } -func wrappedArtifactConn(cpConn *grpc.ClientConn, role pb.CASCredentialsServiceGetRequest_Role) (*grpc.ClientConn, error) { +func wrappedArtifactConn(cpConn *grpc.ClientConn, role pb.CASCredentialsServiceGetRequest_Role, digest string) (*grpc.ClientConn, error) { // Retrieve temporary credentials for uploading client := pb.NewCASCredentialsServiceClient(cpConn) resp, err := client.Get(context.Background(), &pb.CASCredentialsServiceGetRequest{ - Role: role, + Role: role, + Digest: digest, }) if err != nil { return nil, err diff --git a/app/cli/cmd/artifact_download.go b/app/cli/cmd/artifact_download.go index 8f487c32c..ae59d8a64 100644 --- a/app/cli/cmd/artifact_download.go +++ b/app/cli/cmd/artifact_download.go @@ -34,7 +34,7 @@ func newArtifactDownloadCmd() *cobra.Command { // Retrieve temporary credentials for uploading artifactCASConn, err = wrappedArtifactConn(actionOpts.CPConnection, - pb.CASCredentialsServiceGetRequest_ROLE_DOWNLOADER) + pb.CASCredentialsServiceGetRequest_ROLE_DOWNLOADER, digest) if err != nil { return err } diff --git a/app/cli/cmd/artifact_upload.go b/app/cli/cmd/artifact_upload.go index 26181b00d..a91ba7278 100644 --- a/app/cli/cmd/artifact_upload.go +++ b/app/cli/cmd/artifact_upload.go @@ -33,7 +33,7 @@ func newArtifactUploadCmd() *cobra.Command { var err error // Retrieve temporary credentials for uploading - artifactCASConn, err = wrappedArtifactConn(actionOpts.CPConnection, pb.CASCredentialsServiceGetRequest_ROLE_UPLOADER) + artifactCASConn, err = wrappedArtifactConn(actionOpts.CPConnection, pb.CASCredentialsServiceGetRequest_ROLE_UPLOADER, "") if err != nil { return err } diff --git a/app/controlplane/api/controlplane/v1/cas_credentials.pb.go b/app/controlplane/api/controlplane/v1/cas_credentials.pb.go index 3ccf5ed0b..ebe5ba444 100644 --- a/app/controlplane/api/controlplane/v1/cas_credentials.pb.go +++ b/app/controlplane/api/controlplane/v1/cas_credentials.pb.go @@ -91,6 +91,8 @@ type CASCredentialsServiceGetRequest struct { unknownFields protoimpl.UnknownFields Role CASCredentialsServiceGetRequest_Role `protobuf:"varint,1,opt,name=role,proto3,enum=controlplane.v1.CASCredentialsServiceGetRequest_Role" json:"role,omitempty"` + // during the download we need the digest to find the proper cas backend + Digest string `protobuf:"bytes,2,opt,name=digest,proto3" json:"digest,omitempty"` } func (x *CASCredentialsServiceGetRequest) Reset() { @@ -132,6 +134,13 @@ func (x *CASCredentialsServiceGetRequest) GetRole() CASCredentialsServiceGetRequ return CASCredentialsServiceGetRequest_ROLE_UNSPECIFIED } +func (x *CASCredentialsServiceGetRequest) GetDigest() string { + if x != nil { + return x.Digest + } + return "" +} + type CASCredentialsServiceGetResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -234,42 +243,43 @@ var file_controlplane_v1_cas_credentials_proto_rawDesc = []byte{ 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0xbe, 0x01, 0x0a, 0x1f, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x6f, 0x22, 0xd6, 0x01, 0x0a, 0x1f, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x55, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x35, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x42, 0x0a, 0xfa, 0x42, 0x07, 0x82, - 0x01, 0x04, 0x18, 0x01, 0x18, 0x02, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x44, 0x0a, 0x04, - 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x4f, - 0x4c, 0x45, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, 0x4f, 0x41, 0x44, 0x45, 0x52, 0x10, 0x01, 0x12, - 0x11, 0x0a, 0x0d, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x55, 0x50, 0x4c, 0x4f, 0x41, 0x44, 0x45, 0x52, - 0x10, 0x02, 0x22, 0x94, 0x01, 0x0a, 0x20, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x50, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, - 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, - 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, - 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x1a, 0x1e, 0x0a, 0x06, 0x52, 0x65, 0x73, - 0x75, 0x6c, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0x83, 0x01, 0x0a, 0x15, 0x43, 0x41, - 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x6a, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x30, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x41, 0x53, - 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x01, 0x04, 0x18, 0x01, 0x18, 0x02, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, + 0x67, 0x65, 0x73, 0x74, 0x22, 0x44, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x10, + 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x4f, 0x4c, 0x45, 0x5f, 0x44, 0x4f, 0x57, 0x4e, 0x4c, + 0x4f, 0x41, 0x44, 0x45, 0x52, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x52, 0x4f, 0x4c, 0x45, 0x5f, + 0x55, 0x50, 0x4c, 0x4f, 0x41, 0x44, 0x45, 0x52, 0x10, 0x02, 0x22, 0x94, 0x01, 0x0a, 0x20, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, - 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x50, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x38, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x1a, 0x1e, 0x0a, 0x06, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x32, 0x83, 0x01, 0x0a, 0x15, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6a, 0x0a, 0x03, 0x47, + 0x65, 0x74, 0x12, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, + 0x61, 0x6e, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x41, 0x53, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x61, 0x6c, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2d, + 0x64, 0x65, 0x76, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x61, 0x70, + 0x70, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x70, 0x6c, 0x61, 0x6e, 0x65, 0x2f, + 0x76, 0x31, 0x3b, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/app/controlplane/api/controlplane/v1/cas_credentials.pb.validate.go b/app/controlplane/api/controlplane/v1/cas_credentials.pb.validate.go index 8669f661d..9a2362970 100644 --- a/app/controlplane/api/controlplane/v1/cas_credentials.pb.validate.go +++ b/app/controlplane/api/controlplane/v1/cas_credentials.pb.validate.go @@ -68,6 +68,8 @@ func (m *CASCredentialsServiceGetRequest) validate(all bool) error { errors = append(errors, err) } + // no validation rules for Digest + if len(errors) > 0 { return CASCredentialsServiceGetRequestMultiError(errors) } diff --git a/app/controlplane/api/controlplane/v1/cas_credentials.proto b/app/controlplane/api/controlplane/v1/cas_credentials.proto index 0a57facec..b240b4971 100644 --- a/app/controlplane/api/controlplane/v1/cas_credentials.proto +++ b/app/controlplane/api/controlplane/v1/cas_credentials.proto @@ -26,6 +26,8 @@ service CASCredentialsService { message CASCredentialsServiceGetRequest { Role role = 1 [(validate.rules).enum = {in: [1,2]}]; + // during the download we need the digest to find the proper cas backend + string digest = 2; enum Role { ROLE_UNSPECIFIED = 0; diff --git a/app/controlplane/api/controlplane/v1/status_http.pb.go b/app/controlplane/api/controlplane/v1/status_http.pb.go index 2763fbfee..c9fc1fe05 100644 --- a/app/controlplane/api/controlplane/v1/status_http.pb.go +++ b/app/controlplane/api/controlplane/v1/status_http.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-http. DO NOT EDIT. // versions: -// - protoc-gen-go-http v2.7.0 +// - protoc-gen-go-http v2.6.3 // - protoc (unknown) // source: controlplane/v1/status.proto diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/cas_credentials.ts b/app/controlplane/api/gen/frontend/controlplane/v1/cas_credentials.ts index 0408ce65e..20bc9d56c 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/cas_credentials.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/cas_credentials.ts @@ -7,6 +7,8 @@ export const protobufPackage = "controlplane.v1"; export interface CASCredentialsServiceGetRequest { role: CASCredentialsServiceGetRequest_Role; + /** during the download we need the digest to find the proper cas backend */ + digest: string; } export enum CASCredentialsServiceGetRequest_Role { @@ -57,7 +59,7 @@ export interface CASCredentialsServiceGetResponse_Result { } function createBaseCASCredentialsServiceGetRequest(): CASCredentialsServiceGetRequest { - return { role: 0 }; + return { role: 0, digest: "" }; } export const CASCredentialsServiceGetRequest = { @@ -65,6 +67,9 @@ export const CASCredentialsServiceGetRequest = { if (message.role !== 0) { writer.uint32(8).int32(message.role); } + if (message.digest !== "") { + writer.uint32(18).string(message.digest); + } return writer; }, @@ -82,6 +87,13 @@ export const CASCredentialsServiceGetRequest = { message.role = reader.int32() as any; continue; + case 2: + if (tag !== 18) { + break; + } + + message.digest = reader.string(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -92,12 +104,16 @@ export const CASCredentialsServiceGetRequest = { }, fromJSON(object: any): CASCredentialsServiceGetRequest { - return { role: isSet(object.role) ? cASCredentialsServiceGetRequest_RoleFromJSON(object.role) : 0 }; + return { + role: isSet(object.role) ? cASCredentialsServiceGetRequest_RoleFromJSON(object.role) : 0, + digest: isSet(object.digest) ? String(object.digest) : "", + }; }, toJSON(message: CASCredentialsServiceGetRequest): unknown { const obj: any = {}; message.role !== undefined && (obj.role = cASCredentialsServiceGetRequest_RoleToJSON(message.role)); + message.digest !== undefined && (obj.digest = message.digest); return obj; }, @@ -110,6 +126,7 @@ export const CASCredentialsServiceGetRequest = { ): CASCredentialsServiceGetRequest { const message = createBaseCASCredentialsServiceGetRequest(); message.role = object.role ?? 0; + message.digest = object.digest ?? ""; return message; }, }; diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index e1560231b..e3152f654 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -92,7 +92,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, logger) + casMappingUseCase := biz.NewCASMappingUseCase(casMappingRepo, membershipRepo, logger) newAttestationServiceOpts := &service.NewAttestationServiceOpts{ WorkflowRunUC: workflowRunUseCase, WorkflowUC: workflowUseCase, @@ -109,7 +109,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l attestationService := service.NewAttestationService(newAttestationServiceOpts) workflowContractService := service.NewWorkflowSchemaService(workflowContractUseCase, v2...) contextService := service.NewContextService(casBackendUseCase, v2...) - casCredentialsService := service.NewCASCredentialsService(casCredentialsUseCase, casBackendUseCase, v2...) + casCredentialsService := service.NewCASCredentialsService(casCredentialsUseCase, casMappingUseCase, casBackendUseCase, v2...) orgMetricsRepo := data.NewOrgMetricsRepo(dataData, logger) orgMetricsUseCase, err := biz.NewOrgMetricsUseCase(orgMetricsRepo, logger) if err != nil { @@ -120,7 +120,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l integrationsService := service.NewIntegrationsService(integrationUseCase, workflowUseCase, availablePlugins, v2...) organizationService := service.NewOrganizationService(membershipUseCase, v2...) casBackendService := service.NewCASBackendService(casBackendUseCase, providers, v2...) - casRedirectService, err := service.NewCASRedirectService(casBackendUseCase, casCredentialsUseCase, bootstrap_CASServer, v2...) + casRedirectService, err := service.NewCASRedirectService(casMappingUseCase, casCredentialsUseCase, bootstrap_CASServer, v2...) if err != nil { cleanup() return nil, nil, err diff --git a/app/controlplane/internal/biz/casmapping.go b/app/controlplane/internal/biz/casmapping.go index 77988fae4..3422d94d9 100644 --- a/app/controlplane/internal/biz/casmapping.go +++ b/app/controlplane/internal/biz/casmapping.go @@ -19,10 +19,12 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "time" "github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop" + "github.com/chainloop-dev/chainloop/internal/servicelogger" "github.com/go-kratos/kratos/v2/log" cr_v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/uuid" @@ -30,22 +32,26 @@ import ( ) type CASMapping struct { - ID, CASBackendID, OrgID, WorkflowRunID uuid.UUID - Digest string - CreatedAt *time.Time + ID, OrgID, WorkflowRunID uuid.UUID + CASBackend *CASBackend + Digest string + CreatedAt *time.Time } type CASMappingRepo interface { Create(ctx context.Context, digest string, casBackendID, workflowRunID uuid.UUID) (*CASMapping, error) + // List all the CAS mappings for the given digest + FindByDigest(ctx context.Context, digest string) ([]*CASMapping, error) } type CASMappingUseCase struct { - repo CASMappingRepo - logger *log.Helper + repo CASMappingRepo + membershipRepo MembershipRepo + logger *log.Helper } -func NewCASMappingUseCase(repo CASMappingRepo, logger log.Logger) *CASMappingUseCase { - return &CASMappingUseCase{repo, log.NewHelper(logger)} +func NewCASMappingUseCase(repo CASMappingRepo, mRepo MembershipRepo, logger log.Logger) *CASMappingUseCase { + return &CASMappingUseCase{repo, mRepo, servicelogger.ScopedHelper(logger, "cas-mapping-usecase")} } func (uc *CASMappingUseCase) Create(ctx context.Context, digest string, casBackendID, workflowRunID string) (*CASMapping, error) { @@ -67,6 +73,75 @@ func (uc *CASMappingUseCase) Create(ctx context.Context, digest string, casBacke return uc.repo.Create(ctx, digest, casBackendUUID, workflowRunUUID) } +// FindCASMappingForDownload returns the CASMapping appropriate for the given digest and user +// This means any mapping that points to an organization which the user is member of +// If there are multiple mappings, it will try to pick the one that points to a default backend +// Otherwise the first one +func (uc *CASMappingUseCase) FindCASMappingForDownload(ctx context.Context, digest string, userID string) (*CASMapping, error) { + uc.logger.Infow("msg", "finding cas mapping for download", "digest", digest, "user", userID) + + userUUID, err := uuid.Parse(userID) + if err != nil { + return nil, NewErrInvalidUUID(err) + } + + if _, err = cr_v1.NewHash(digest); err != nil { + return nil, NewErrValidation(fmt.Errorf("invalid digest format: %w", err)) + } + + // list all the CAS allMappings for the given digest + allMappings, err := uc.repo.FindByDigest(ctx, digest) + if err != nil { + return nil, fmt.Errorf("failed to list cas mappings: %w", err) + } + + uc.logger.Debugw("msg", fmt.Sprintf("found %d entries globally", len(allMappings)), "digest", digest, "user", userID) + // The given digest has not been uploaded to any CAS backend + if len(allMappings) == 0 { + return nil, NewErrNotFound("digest not found in any mapping") + } + + // filter the ones that the user has access to. + // This means any mapping that points to an organization which the user is member of + userMappings := make([]*CASMapping, 0) + memberships, err := uc.membershipRepo.FindByUser(ctx, userUUID) + if err != nil { + return nil, fmt.Errorf("failed to list memberships: %w", err) + } + + for _, mapping := range allMappings { + for _, m := range memberships { + if mapping.OrgID == m.OrganizationID { + userMappings = append(userMappings, mapping) + } + } + } + + uc.logger.Debugw("msg", fmt.Sprintf("found %d entries for the user", len(userMappings)), "digest", digest, "user", userID) + + // The user has not access to + if len(userMappings) == 0 { + uc.logger.Warnw("msg", "digest exist but user does not have access to it", "digest", digest, "user", userID) + return nil, NewErrUnauthorized(errors.New("unauthorized access to the artifact")) + } + + // Pick the appropriate mapping from multiple ones + // for now it will work as follows + // 1 - If there is only one mapping, return it + // 2 - if there are more than 1, we try to pick the one that points to a default backend + // 3 - Otherwise the first one + result := userMappings[0] + for _, mapping := range userMappings { + if mapping.CASBackend.Default { + result = mapping + break + } + } + + uc.logger.Infow("msg", "mapping found!", "digest", digest, "user", userID, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default) + return result, nil +} + type CASMappingLookupRef struct { Name, Digest string } diff --git a/app/controlplane/internal/biz/casmapping_integration_test.go b/app/controlplane/internal/biz/casmapping_integration_test.go index 9ab04c5f3..6539b11c2 100644 --- a/app/controlplane/internal/biz/casmapping_integration_test.go +++ b/app/controlplane/internal/biz/casmapping_integration_test.go @@ -27,13 +27,76 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) -func (s *casMappingIntegrationSuite) TestCreate() { - validDigest := "sha256:3b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d" - invalidDigest := "sha256:deadbeef" +const ( + validDigest = "sha256:3b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d" + validDigest2 = "sha256:2b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d" + validDigest3 = "sha256:1b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d" + invalidDigest = "sha256:deadbeef" +) + +func (s *casMappingIntegrationSuite) TestCASMappingForDownload() { + // Let's create 3 CASMappings: + // 1. Digest: validDigest, CASBackend: casBackend1, WorkflowRunID: workflowRun + // 2. Digest: validDigest, CASBackend: casBackend2, WorkflowRunID: workflowRun + // 3. Digest: validDigest2, CASBackend: casBackend2, WorkflowRunID: workflowRun + // 4. Digest: validDigest3, CASBackend: casBackend3, WorkflowRunID: workflowRun + _, err := s.CASMapping.Create(context.TODO(), validDigest, s.casBackend1.ID.String(), s.workflowRun.ID.String()) + require.NoError(s.T(), err) + _, err = s.CASMapping.Create(context.TODO(), validDigest, s.casBackend2.ID.String(), s.workflowRun.ID.String()) + require.NoError(s.T(), err) + _, err = s.CASMapping.Create(context.TODO(), validDigest2, s.casBackend2.ID.String(), s.workflowRun.ID.String()) + require.NoError(s.T(), err) + _, err = s.CASMapping.Create(context.TODO(), validDigest3, s.casBackend3.ID.String(), s.workflowRun.ID.String()) + require.NoError(s.T(), err) + + // Since the userOrg1And2 is member of org1 and org2, she should be able to download + // both validDigest and validDigest2 from two different orgs + s.Run("userOrg1And2 can download validDigest from org1", func() { + mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest, s.userOrg1And2.ID) + s.NoError(err) + s.NotNil(mapping) + s.Equal(s.casBackend1.ID, mapping.CASBackend.ID) + }) + + s.Run("userOrg1And2 can download validDigest2 from org2", func() { + mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest2, s.userOrg1And2.ID) + s.NoError(err) + s.NotNil(mapping) + s.Equal(s.casBackend2.ID, mapping.CASBackend.ID) + }) + + s.Run("userOrg1And2 can not download validDigest3 from org3", func() { + mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest3, s.userOrg1And2.ID) + s.Error(err) + s.Nil(mapping) + }) + + s.Run("userOrg2 can download validDigest2 from org2", func() { + mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest2, s.userOrg2.ID) + s.NoError(err) + s.NotNil(mapping) + s.Equal(s.casBackend2.ID, mapping.CASBackend.ID) + }) + + s.Run("userOrg2 can download validDigest from org2", func() { + mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest, s.userOrg2.ID) + s.NoError(err) + s.NotNil(mapping) + s.Equal(s.casBackend2.ID, mapping.CASBackend.ID) + }) + + s.Run("userOrg2 can not download invalidDigest", func() { + mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), invalidDigest, s.userOrg2.ID) + s.Error(err) + s.Nil(mapping) + }) +} +func (s *casMappingIntegrationSuite) TestCreate() { testCases := []struct { name string digest string @@ -44,26 +107,26 @@ func (s *casMappingIntegrationSuite) TestCreate() { { name: "valid", digest: validDigest, - casBackendID: s.casBackend.ID, + casBackendID: s.casBackend1.ID, workflowRunID: s.workflowRun.ID, }, { name: "created again with same digest", digest: validDigest, - casBackendID: s.casBackend.ID, + casBackendID: s.casBackend1.ID, workflowRunID: s.workflowRun.ID, }, { name: "invalid digest format", digest: invalidDigest, - casBackendID: s.casBackend.ID, + casBackendID: s.casBackend1.ID, workflowRunID: s.workflowRun.ID, wantErr: true, }, { name: "invalid digest missing prefix", digest: "3b0f04c276be095e62f3ac03b9991913c37df1fcd44548e75069adce313aba4d", - casBackendID: s.casBackend.ID, + casBackendID: s.casBackend1.ID, workflowRunID: s.workflowRun.ID, wantErr: true, }, @@ -77,7 +140,7 @@ func (s *casMappingIntegrationSuite) TestCreate() { { name: "non-existing WorkflowRunID", digest: validDigest, - casBackendID: s.casBackend.ID, + casBackendID: s.casBackend1.ID, workflowRunID: uuid.New(), wantErr: true, }, @@ -85,9 +148,9 @@ func (s *casMappingIntegrationSuite) TestCreate() { want := &biz.CASMapping{ Digest: validDigest, - CASBackendID: s.casBackend.ID, + CASBackend: &biz.CASBackend{ID: s.casBackend1.ID}, WorkflowRunID: s.workflowRun.ID, - OrgID: s.casBackend.OrganizationID, + OrgID: s.casBackend1.OrganizationID, } for _, tc := range testCases { @@ -97,9 +160,14 @@ func (s *casMappingIntegrationSuite) TestCreate() { s.Error(err) } else { s.NoError(err) - if diff := cmp.Diff(want, got, cmpopts.IgnoreFields(biz.CASMapping{}, "CreatedAt", "ID")); diff != "" { + if diff := cmp.Diff(want, got, + cmpopts.IgnoreFields(biz.CASMapping{}, "CreatedAt", "ID"), + cmpopts.IgnoreTypes(biz.CASBackend{}), + ); diff != "" { assert.Failf(s.T(), "mismatch (-want +got):\n%s", diff) } + + assert.Equal(s.T(), want.CASBackend.ID, got.CASBackend.ID) } }) } @@ -107,8 +175,10 @@ func (s *casMappingIntegrationSuite) TestCreate() { type casMappingIntegrationSuite struct { testhelpers.UseCasesEachTestSuite - casBackend *biz.CASBackend - workflowRun *biz.WorkflowRun + casBackend1, casBackend2, casBackend3 *biz.CASBackend + workflowRun *biz.WorkflowRun + userOrg1And2, userOrg2 *biz.User + org1, org2, orgNoUsers *biz.Organization } func (s *casMappingIntegrationSuite) SetupTest() { @@ -125,28 +195,52 @@ func (s *casMappingIntegrationSuite) SetupTest() { s.TestingUseCases = testhelpers.NewTestingUseCases(s.T(), testhelpers.WithCredsReaderWriter(credsWriter)) // Create casBackend in the database - org, err := s.Organization.Create(ctx, "testing org 1 with one backend") + s.org1, err = s.Organization.Create(ctx, "testing org 1 with one backend") + assert.NoError(err) + s.casBackend1, err = s.CASBackend.Create(ctx, s.org1.ID, "my-location", "backend 1 description", biz.CASBackendOCI, nil, true) + assert.NoError(err) + s.org2, err = s.Organization.Create(ctx, "testing org 2") assert.NoError(err) - s.casBackend, err = s.CASBackend.Create(ctx, org.ID, "my-location", "backend 1 description", biz.CASBackendOCI, nil, true) + s.casBackend2, err = s.CASBackend.Create(ctx, s.org2.ID, "my-location", "backend 1 description", biz.CASBackendOCI, nil, true) + assert.NoError(err) + // Create casBackend associated with an org which users are not member of + s.orgNoUsers, err = s.Organization.Create(ctx, "org without users") + assert.NoError(err) + s.casBackend3, err = s.CASBackend.Create(ctx, s.orgNoUsers.ID, "my-location", "backend 1 description", biz.CASBackendOCI, nil, true) assert.NoError(err) // Create workflowRun in the database // Workflow - workflow, err := s.Workflow.Create(ctx, &biz.CreateOpts{Name: "test workflow", OrgID: org.ID}) + workflow, err := s.Workflow.Create(ctx, &biz.CreateOpts{Name: "test workflow", OrgID: s.org1.ID}) assert.NoError(err) // Robot account - robotAccount, err := s.RobotAccount.Create(ctx, "name", org.ID, workflow.ID.String()) + robotAccount, err := s.RobotAccount.Create(ctx, "name", s.org1.ID, workflow.ID.String()) assert.NoError(err) // Find contract revision - contractVersion, err := s.WorkflowContract.Describe(ctx, org.ID, workflow.ContractID.String(), 0) + contractVersion, err := s.WorkflowContract.Describe(ctx, s.org1.ID, workflow.ContractID.String(), 0) assert.NoError(err) s.workflowRun, err = s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ - WorkflowID: workflow.ID.String(), RobotaccountID: robotAccount.ID.String(), ContractRevisionUUID: contractVersion.Version.ID, CASBackendID: s.casBackend.ID, + WorkflowID: workflow.ID.String(), RobotaccountID: robotAccount.ID.String(), ContractRevisionUUID: contractVersion.Version.ID, CASBackendID: s.casBackend1.ID, RunnerType: "runnerType", RunnerRunURL: "runURL", }) + + assert.NoError(err) + + // Create User + s.userOrg1And2, err = s.User.FindOrCreateByEmail(ctx, "foo@test.com") + assert.NoError(err) + + s.userOrg2, err = s.User.FindOrCreateByEmail(ctx, "foo-org2@test.com") + assert.NoError(err) + + _, err = s.Membership.Create(ctx, s.org1.ID, s.userOrg1And2.ID, false) + assert.NoError(err) + _, err = s.Membership.Create(ctx, s.org2.ID, s.userOrg1And2.ID, true) + assert.NoError(err) + _, err = s.Membership.Create(ctx, s.org2.ID, s.userOrg2.ID, true) assert.NoError(err) } diff --git a/app/controlplane/internal/biz/casmapping_test.go b/app/controlplane/internal/biz/casmapping_test.go index 5c3421ed8..971cfe608 100644 --- a/app/controlplane/internal/biz/casmapping_test.go +++ b/app/controlplane/internal/biz/casmapping_test.go @@ -81,7 +81,7 @@ func (s *casMappingSuite) TestCreate() { want := &biz.CASMapping{ ID: validUUID, Digest: validDigest, - CASBackendID: validUUID, + CASBackend: &biz.CASBackend{ID: validUUID}, WorkflowRunID: validUUID, OrgID: validUUID, } @@ -170,7 +170,7 @@ type casMappingSuite struct { func (s *casMappingSuite) SetupTest() { s.repo = repoM.NewCASMappingRepo(s.T()) - s.useCase = biz.NewCASMappingUseCase(s.repo, nil) + s.useCase = biz.NewCASMappingUseCase(s.repo, nil, nil) } func TestCASMapping(t *testing.T) { diff --git a/app/controlplane/internal/biz/errors.go b/app/controlplane/internal/biz/errors.go index 9a2fbae00..f744511df 100644 --- a/app/controlplane/internal/biz/errors.go +++ b/app/controlplane/internal/biz/errors.go @@ -67,3 +67,19 @@ func (e ErrValidation) Error() string { func IsErrValidation(err error) bool { return errors.As(err, &ErrValidation{}) } + +type ErrUnauthorized struct { + err error +} + +func NewErrUnauthorized(err error) ErrUnauthorized { + return ErrUnauthorized{err} +} + +func (e ErrUnauthorized) Error() string { + return fmt.Sprintf("authorization error: %s", e.err.Error()) +} + +func IsErrUnauthorized(err error) bool { + return errors.As(err, &ErrUnauthorized{}) +} diff --git a/app/controlplane/internal/biz/mocks/CASMappingRepo.go b/app/controlplane/internal/biz/mocks/CASMappingRepo.go index ad6794577..0ed3a93f6 100644 --- a/app/controlplane/internal/biz/mocks/CASMappingRepo.go +++ b/app/controlplane/internal/biz/mocks/CASMappingRepo.go @@ -43,6 +43,32 @@ func (_m *CASMappingRepo) Create(ctx context.Context, digest string, casBackendI return r0, r1 } +// FindByDigest provides a mock function with given fields: ctx, digest +func (_m *CASMappingRepo) FindByDigest(ctx context.Context, digest string) ([]*biz.CASMapping, error) { + ret := _m.Called(ctx, digest) + + var r0 []*biz.CASMapping + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]*biz.CASMapping, error)); ok { + return rf(ctx, digest) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []*biz.CASMapping); ok { + r0 = rf(ctx, digest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*biz.CASMapping) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, digest) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + type mockConstructorTestingTNewCASMappingRepo interface { mock.TestingT Cleanup(func()) diff --git a/app/controlplane/internal/biz/testhelpers/wire_gen.go b/app/controlplane/internal/biz/testhelpers/wire_gen.go index 675ecb4db..16b727a28 100644 --- a/app/controlplane/internal/biz/testhelpers/wire_gen.go +++ b/app/controlplane/internal/biz/testhelpers/wire_gen.go @@ -68,7 +68,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, logger) + casMappingUseCase := biz.NewCASMappingUseCase(casMappingRepo, membershipRepo, logger) testingUseCases := &TestingUseCases{ DB: testDatabase, Data: dataData, diff --git a/app/controlplane/internal/data/casmapping.go b/app/controlplane/internal/data/casmapping.go index 868e01b05..2c3dfc5ff 100644 --- a/app/controlplane/internal/data/casmapping.go +++ b/app/controlplane/internal/data/casmapping.go @@ -62,6 +62,30 @@ func (r *CASMappingRepo) Create(ctx context.Context, digest string, casBackendID return r.findByID(ctx, mapping.ID) } +func (r *CASMappingRepo) FindByDigest(ctx context.Context, digest string) ([]*biz.CASMapping, error) { + mappings, err := r.data.db.CASMapping.Query(). + Where(casmapping.Digest(digest)). + WithCasBackend(). + WithOrganization(). + WithWorkflowRun(). + All(ctx) + if err != nil { + return nil, fmt.Errorf("failed to list cas mappings: %w", err) + } + + res := make([]*biz.CASMapping, 0, len(mappings)) + for _, m := range mappings { + r, err := entCASMappingToBiz(m) + if err != nil { + return nil, fmt.Errorf("failed to convert cas mapping: %w", err) + } + + res = append(res, r) + } + + return res, nil +} + // FindByID finds a CAS Mapping by ID // If not found, returns nil and no error func (r *CASMappingRepo) findByID(ctx context.Context, id uuid.UUID) (*biz.CASMapping, error) { @@ -100,7 +124,7 @@ func entCASMappingToBiz(input *ent.CASMapping) (*biz.CASMapping, error) { return &biz.CASMapping{ ID: input.ID, Digest: input.Digest, - CASBackendID: casBackend.ID, + CASBackend: entCASBackendToBiz(casBackend), WorkflowRunID: workflowRun.ID, OrgID: org.ID, CreatedAt: toTimePtr(input.CreatedAt), diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index 5c1258ebc..68f06fea5 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -30,21 +30,25 @@ type CASCredentialsService struct { *service pb.UnimplementedCASCredentialsServiceServer - casUC *biz.CASCredentialsUseCase - ociUC *biz.CASBackendUseCase + casUC *biz.CASCredentialsUseCase + casBackendUC *biz.CASBackendUseCase + casMappingUC *biz.CASMappingUseCase } -func NewCASCredentialsService(casUC *biz.CASCredentialsUseCase, ociUC *biz.CASBackendUseCase, opts ...NewOpt) *CASCredentialsService { +func NewCASCredentialsService(casUC *biz.CASCredentialsUseCase, casmUC *biz.CASMappingUseCase, casBUC *biz.CASBackendUseCase, opts ...NewOpt) *CASCredentialsService { return &CASCredentialsService{ service: newService(opts...), casUC: casUC, - ociUC: ociUC, + // we use the casMappingUC to find the backend to download from + casMappingUC: casmUC, + // we use the casBackendUC to find the default upload backend + casBackendUC: casBUC, } } // Get will generate temporary credentials to be used against the CAS service for the current organization func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsServiceGetRequest) (*pb.CASCredentialsServiceGetResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentUser, currentOrg, err := loadCurrentUserAndOrg(ctx) if err != nil { return nil, err } @@ -57,16 +61,33 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS role = casJWT.Uploader } - // Get repository to provide the secret name - backend, err := s.ociUC.FindDefaultBackend(ctx, currentOrg.ID) + // 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, sl.LogAndMaskErr(err, s.log) } else if backend == nil { return nil, errors.NotFound("not found", "main repository not found") } - // If we want to upload an artifact but we have selected an inline backend we fail - if backend.Provider == biz.CASBackendInline { + if role == casJWT.Downloader { + // Try to find the proper backend where the artifact is stored + mapping, err := s.casMappingUC.FindCASMappingForDownload(ctx, req.Digest, currentUser.ID) + // If we can't find a mapping, we'll use the default backend + if err != nil && !biz.IsNotFound(err) && !biz.IsErrUnauthorized(err) { + if biz.IsErrValidation(err) { + return nil, errors.BadRequest("invalid", err.Error()) + } + + return nil, sl.LogAndMaskErr(err, s.log) + } + + if mapping != nil { + backend = mapping.CASBackend + } + } + + // 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") } diff --git a/app/controlplane/internal/service/casredirect.go b/app/controlplane/internal/service/casredirect.go index 62e353f29..9b4a02b37 100644 --- a/app/controlplane/internal/service/casredirect.go +++ b/app/controlplane/internal/service/casredirect.go @@ -30,7 +30,6 @@ import ( sl "github.com/chainloop-dev/chainloop/internal/servicelogger" kerrors "github.com/go-kratos/kratos/v2/errors" khttp "github.com/go-kratos/kratos/v2/transport/http" - cr_v1 "github.com/google/go-containerregistry/pkg/v1" ) const ( @@ -47,19 +46,19 @@ type CASRedirectService struct { pb.UnimplementedCASRedirectServiceServer *service - casUC *biz.CASBackendUseCase + casMappingUC *biz.CASMappingUseCase casCredsUseCase *biz.CASCredentialsUseCase casServerConf *conf.Bootstrap_CASServer } -func NewCASRedirectService(casUC *biz.CASBackendUseCase, casCredsUC *biz.CASCredentialsUseCase, conf *conf.Bootstrap_CASServer, opts ...NewOpt) (*CASRedirectService, error) { +func NewCASRedirectService(casmUC *biz.CASMappingUseCase, casCredsUC *biz.CASCredentialsUseCase, conf *conf.Bootstrap_CASServer, opts ...NewOpt) (*CASRedirectService, error) { if conf == nil || conf.GetDownloadUrl() == "" { return nil, errors.New("CASServer.downloadURL configuration is missing") } return &CASRedirectService{ service: newService(opts...), - casUC: casUC, + casMappingUC: casmUC, casCredsUseCase: casCredsUC, casServerConf: conf, }, nil @@ -69,29 +68,30 @@ func NewCASRedirectService(casUC *biz.CASBackendUseCase, casCredsUC *biz.CASCred // The URL includes a JWT token that is used to authenticate the request, this token has all the information required to validate the request // The result would look like "https://cas.chainloop.dev/download/sha256:[DIGEST]?t=tokenJWT func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDownloadURLRequest) (*pb.GetDownloadURLResponse, error) { - _, currentOrg, err := loadCurrentUserAndOrg(ctx) + currentUser, _, err := loadCurrentUserAndOrg(ctx) if err != nil { return nil, err } - // Load default CAS backend in the current ORG - // TODO, in the future instead we will lookup the CAS backend where the artifact is stored - backend, err := s.casUC.FindDefaultBackend(ctx, currentOrg.ID) - if err != nil && !biz.IsNotFound(err) { + // Find the CAS backend that should be used for the download, if any + mapping, err := s.casMappingUC.FindCASMappingForDownload(ctx, req.Digest, currentUser.ID) + if err != nil { + // We don't want to leak the fact that the asset exists but the user does not have permissions + // that's why we return a generic 404 in unauthorized scenarios too + if biz.IsNotFound(err) || biz.IsErrUnauthorized(err) { + return nil, kerrors.NotFound("not found", "artifact not found") + } else if biz.IsErrValidation(err) { + return nil, kerrors.BadRequest("invalid", err.Error()) + } + return nil, sl.LogAndMaskErr(err, s.log) - } else if backend == nil { - return nil, kerrors.NotFound("not found", "default CAS backend not found") } + backend := mapping.CASBackend + // inline backends don't have a download URL if backend.Inline { - return nil, kerrors.NotFound("not found", "default CAS backend is inline") - } - - // parse the digest to make sure is a valid sha256 sum - hash, err := cr_v1.NewHash(req.Digest) - if err != nil { - return nil, kerrors.BadRequest("invalid digest", "the format must be \"sha256:[HexValue]\"") + return nil, kerrors.NotFound("not found", "CAS backend is inline") } // Create an URL to download the artifact from the CAS backend @@ -101,7 +101,7 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown } // 1 - append the digest /download/[digest] - downloadURL := downloadBase.JoinPath(hash.String()) + downloadURL := downloadBase.JoinPath(req.Digest) // 2- add authentication token to the query params ?t=[token] if backend.SecretName != "" {