From b3b3b9d77a8b3db7d6861b1890c8e0be8962697d Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 12 Aug 2025 00:26:39 +0200 Subject: [PATCH 1/8] feat: add project create event Signed-off-by: Miguel Martinez --- .../pkg/auditor/events/project.go | 23 +++++++++++++++++++ .../pkg/auditor/events/project_test.go | 12 ++++++++++ .../testdata/projects/project_created.json | 16 +++++++++++++ app/controlplane/pkg/biz/project.go | 15 +++++++++++- app/controlplane/pkg/biz/projectversion.go | 4 ++-- 5 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 app/controlplane/pkg/auditor/events/testdata/projects/project_created.json diff --git a/app/controlplane/pkg/auditor/events/project.go b/app/controlplane/pkg/auditor/events/project.go index bc440644f..86d1d1d5f 100644 --- a/app/controlplane/pkg/auditor/events/project.go +++ b/app/controlplane/pkg/auditor/events/project.go @@ -27,6 +27,7 @@ import ( ) var ( + _ auditor.LogEntry = (*ProjectCreated)(nil) _ auditor.LogEntry = (*ProjectMembershipAdded)(nil) _ auditor.LogEntry = (*ProjectMembershipRemoved)(nil) _ auditor.LogEntry = (*ProjectMemberRoleUpdated)(nil) @@ -34,6 +35,7 @@ var ( const ( ProjectType auditor.TargetType = "Project" + ProjectCreatedActionType string = "ProjectCreated" ProjectMembershipAddedActionType string = "ProjectMembershipAdded" ProjectMembershipRemovedActionType string = "ProjectMembershipRemoved" ProjectMemberRoleUpdatedType string = "ProjectMemberRoleUpdated" @@ -65,6 +67,27 @@ func (p *ProjectBase) ActionInfo() (json.RawMessage, error) { return json.Marshal(&p) } +// ProjectCreated represents the creation of a project +type ProjectCreated struct { + *ProjectBase +} + +func (p *ProjectCreated) ActionType() string { + return ProjectCreatedActionType +} + +func (p *ProjectCreated) ActionInfo() (json.RawMessage, error) { + if _, err := p.ProjectBase.ActionInfo(); err != nil { + return nil, err + } + + return json.Marshal(&p) +} + +func (p *ProjectCreated) Description() string { + return fmt.Sprintf("%s has created the project '%s'", auditor.GetActorIdentifier(), p.ProjectName) +} + // Helper function to make role names more user-friendly func prettyRole(role string) string { // Convert the role to a prettier format diff --git a/app/controlplane/pkg/auditor/events/project_test.go b/app/controlplane/pkg/auditor/events/project_test.go index 84ddf29f5..e52f3f47c 100644 --- a/app/controlplane/pkg/auditor/events/project_test.go +++ b/app/controlplane/pkg/auditor/events/project_test.go @@ -49,6 +49,18 @@ func TestProjectEvents(t *testing.T) { actor auditor.ActorType actorID uuid.UUID }{ + { + name: "ProjectCreated", + event: &events.ProjectCreated{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + ProjectName: projectName, + }, + }, + expected: "testdata/projects/project_created.json", + actor: auditor.ActorTypeUser, + actorID: userUUID, + }, { name: "ProjectMembershipAdded", event: &events.ProjectMembershipAdded{ diff --git a/app/controlplane/pkg/auditor/events/testdata/projects/project_created.json b/app/controlplane/pkg/auditor/events/testdata/projects/project_created.json new file mode 100644 index 000000000..668dddebf --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/projects/project_created.json @@ -0,0 +1,16 @@ +{ + "ActionType": "ProjectCreated", + "TargetType": "Project", + "TargetID": "3089bb36-e27b-428b-8009-d015c8737c56", + "ActorType": "USER", + "ActorID": "1089bb36-e27b-428b-8009-d015c8737c54", + "ActorEmail": "john@cyberdyne.io", + "ActorName": "John Connor", + "OrgID": "1089bb36-e27b-428b-8009-d015c8737c54", + "Description": "John Connor has created the project 'test-project'", + "Info": { + "project_id": "3089bb36-e27b-428b-8009-d015c8737c56", + "project_name": "test-project" + }, + "Digest": "sha256:4d72f70e3f7311eecbb72451c71c6dceec3d38f8304cd1a064288148959a3991" +} \ No newline at end of file diff --git a/app/controlplane/pkg/biz/project.go b/app/controlplane/pkg/biz/project.go index ee0545740..a07abfc27 100644 --- a/app/controlplane/pkg/biz/project.go +++ b/app/controlplane/pkg/biz/project.go @@ -208,7 +208,20 @@ func (uc *ProjectUseCase) Create(ctx context.Context, orgID, name string) (*Proj return nil, NewErrInvalidUUID(err) } - return uc.projectsRepository.Create(ctx, orgUUID, name) + project, err := uc.projectsRepository.Create(ctx, orgUUID, name) + if err != nil { + return nil, err + } + + // Dispatch audit event for project creation + uc.auditorUC.Dispatch(ctx, &events.ProjectCreated{ + ProjectBase: &events.ProjectBase{ + ProjectID: &project.ID, + ProjectName: project.Name, + }, + }, &orgUUID) + + return project, nil } // ListMembers lists the members of a project with pagination. diff --git a/app/controlplane/pkg/biz/projectversion.go b/app/controlplane/pkg/biz/projectversion.go index 5937e3229..336faad5a 100644 --- a/app/controlplane/pkg/biz/projectversion.go +++ b/app/controlplane/pkg/biz/projectversion.go @@ -78,8 +78,8 @@ func (uc *ProjectVersionUseCase) UpdateReleaseStatus(ctx context.Context, versio return nil, NewErrInvalidUUID(err) } - preRelease := !isRelease - return uc.projectRepo.Update(ctx, versionUUID, &ProjectVersionUpdateOpts{Prerelease: &preRelease}) + preReleaseValue := !isRelease + return uc.projectRepo.Update(ctx, versionUUID, &ProjectVersionUpdateOpts{Prerelease: &preReleaseValue}) } func (uc *ProjectVersionUseCase) Create(ctx context.Context, projectID, version string, prerelease bool) (*ProjectVersion, error) { From db17b90ecb3763847923f24bdc840ff9f6df517f Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 12 Aug 2025 10:43:49 +0200 Subject: [PATCH 2/8] send project creation event Signed-off-by: Miguel Martinez --- app/controlplane/pkg/biz/workflow.go | 64 ++++++++++++++++------------ 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/app/controlplane/pkg/biz/workflow.go b/app/controlplane/pkg/biz/workflow.go index e7c8cd536..81523a867 100644 --- a/app/controlplane/pkg/biz/workflow.go +++ b/app/controlplane/pkg/biz/workflow.go @@ -155,6 +155,15 @@ func (uc *WorkflowUseCase) Create(ctx context.Context, opts *WorkflowCreateOpts) return nil, NewErrValidation(err) } + // Check if the project already exists to determine if we need to emit a ProjectCreated event + orgUUID, err := uuid.Parse(opts.OrgID) + if err != nil { + return nil, fmt.Errorf("failed to parse org ID %q: %w", opts.OrgID, err) + } + + existingProject, err := uc.projectRepo.FindProjectByOrgIDAndName(ctx, orgUUID, opts.Project) + projectAlreadyExists := err == nil && existingProject != nil + wf, err := uc.wfRepo.Create(ctx, opts) if err != nil { if IsErrAlreadyExists(err) { @@ -166,43 +175,44 @@ func (uc *WorkflowUseCase) Create(ctx context.Context, opts *WorkflowCreateOpts) // Set project admin if a new project has been created if opts.Owner != nil { - orgUUID, err := uuid.Parse(opts.OrgID) - if err != nil { - return nil, fmt.Errorf("failed to parse org ID %q: %w", opts.OrgID, err) - } if err = uc.membershipUC.SetProjectOwner(ctx, orgUUID, wf.ProjectID, *opts.Owner); err != nil { return nil, fmt.Errorf("failed to set project owner: %w", err) } } - orgUUID, err := uuid.Parse(opts.OrgID) - if err != nil { - uc.logger.Warn("failed to parse org id", "err", err) - } else { - // Dispatch events to the audit log regarding the workflow - uc.auditorUC.Dispatch(ctx, &events.WorkflowCreated{ - WorkflowBase: &events.WorkflowBase{ - WorkflowID: &wf.ID, - WorkflowName: wf.Name, - ProjectName: opts.Project, + // Dispatch audit event for project creation if a new project was created + if !projectAlreadyExists { + uc.auditorUC.Dispatch(ctx, &events.ProjectCreated{ + ProjectBase: &events.ProjectBase{ + ProjectID: &wf.ProjectID, + ProjectName: opts.Project, }, - WorkflowContractID: &wf.ContractID, - WorkflowContractName: wf.ContractName, - WorkflowDescription: &opts.Description, - Team: &opts.Team, - Public: opts.Public, }, &orgUUID) + } - // Dispatch events to the audit log regarding the contract - uc.auditorUC.Dispatch(ctx, &events.WorkflowContractAttached{ - WorkflowContractBase: &events.WorkflowContractBase{ - WorkflowContractID: &wf.ContractID, - WorkflowContractName: wf.ContractName, - }, + // Dispatch events to the audit log regarding the workflow + uc.auditorUC.Dispatch(ctx, &events.WorkflowCreated{ + WorkflowBase: &events.WorkflowBase{ WorkflowID: &wf.ID, WorkflowName: wf.Name, - }, &orgUUID) - } + ProjectName: opts.Project, + }, + WorkflowContractID: &wf.ContractID, + WorkflowContractName: wf.ContractName, + WorkflowDescription: &opts.Description, + Team: &opts.Team, + Public: opts.Public, + }, &orgUUID) + + // Dispatch events to the audit log regarding the contract + uc.auditorUC.Dispatch(ctx, &events.WorkflowContractAttached{ + WorkflowContractBase: &events.WorkflowContractBase{ + WorkflowContractID: &wf.ContractID, + WorkflowContractName: wf.ContractName, + }, + WorkflowID: &wf.ID, + WorkflowName: wf.Name, + }, &orgUUID) return wf, nil } From d29cf8721f10a1fb19d9a9d9dcd031a2247ed890 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 12 Aug 2025 10:47:59 +0200 Subject: [PATCH 3/8] send project creation event Signed-off-by: Miguel Martinez --- app/controlplane/configs/config.devel.yaml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/controlplane/configs/config.devel.yaml b/app/controlplane/configs/config.devel.yaml index bac253809..00e58e496 100644 --- a/app/controlplane/configs/config.devel.yaml +++ b/app/controlplane/configs/config.devel.yaml @@ -16,11 +16,11 @@ server: # certificate: "../../devel/devkeys/selfsigned/controlplane.crt" # private_key: "../../devel/devkeys/selfsigned/controlplane.key" -#nats_server: -# uri: nats://0.0.0.0:4222 +nats_server: + uri: nats://0.0.0.0:4222 # Restrict organization creation to role:instance:admin -restrict_org_creation: true +restrict_org_creation: false certificate_authorities: - issuer: true @@ -83,10 +83,10 @@ auth: # Private key used to sign the JWTs meant to be consumed by the CAS cas_robot_account_private_key_path: ${CAS_KEY_PATH:../../devel/devkeys/cas.pem} # allow_list: - # rules: ["@chainloop.local"] - # selected_routes: ["/controlplane.v1.WorkflowRunService/List"] - # custom_message: "you need to require access here http://foo.com" - # allow_db_overrides: true # if false, the user access flag in the DB will mirror the allowlist, otherwise it will be respected + # rules: ["@chainloop.local"] + # selected_routes: ["/controlplane.v1.WorkflowRunService/List"] + # custom_message: "you need to require access here http://foo.com" + # allow_db_overrides: true # if false, the user access flag in the DB will mirror the allowlist, otherwise it will be respected # referrer_shared_index: # enabled: true @@ -104,7 +104,6 @@ prometheus_integration: # url: http://localhost:8002/v1 enable_profiler: true - # federated_authentication: # enabled: true # url: http://localhost:8002/machine-identity/verify-token From dbb7765880becf9a9ca37f9365b3cd1d0cb1bff9 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 12 Aug 2025 10:50:03 +0200 Subject: [PATCH 4/8] fix test Signed-off-by: Miguel Martinez --- app/controlplane/configs/config.devel.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controlplane/configs/config.devel.yaml b/app/controlplane/configs/config.devel.yaml index 00e58e496..e7acbe0ec 100644 --- a/app/controlplane/configs/config.devel.yaml +++ b/app/controlplane/configs/config.devel.yaml @@ -16,8 +16,8 @@ server: # certificate: "../../devel/devkeys/selfsigned/controlplane.crt" # private_key: "../../devel/devkeys/selfsigned/controlplane.key" -nats_server: - uri: nats://0.0.0.0:4222 +# nats_server: +# uri: nats://0.0.0.0:4222 # Restrict organization creation to role:instance:admin restrict_org_creation: false From 14d9eb0d15bc5eeeaa10ebeb7978ce70d52728f4 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 12 Aug 2025 10:56:47 +0200 Subject: [PATCH 5/8] fix order Signed-off-by: Miguel Martinez --- app/controlplane/pkg/biz/workflow.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controlplane/pkg/biz/workflow.go b/app/controlplane/pkg/biz/workflow.go index 81523a867..621464c13 100644 --- a/app/controlplane/pkg/biz/workflow.go +++ b/app/controlplane/pkg/biz/workflow.go @@ -161,9 +161,6 @@ func (uc *WorkflowUseCase) Create(ctx context.Context, opts *WorkflowCreateOpts) return nil, fmt.Errorf("failed to parse org ID %q: %w", opts.OrgID, err) } - existingProject, err := uc.projectRepo.FindProjectByOrgIDAndName(ctx, orgUUID, opts.Project) - projectAlreadyExists := err == nil && existingProject != nil - wf, err := uc.wfRepo.Create(ctx, opts) if err != nil { if IsErrAlreadyExists(err) { @@ -180,8 +177,13 @@ func (uc *WorkflowUseCase) Create(ctx context.Context, opts *WorkflowCreateOpts) } } + existingProject, err := uc.projectRepo.FindProjectByOrgIDAndName(ctx, orgUUID, opts.Project) + if err != nil { + return nil, fmt.Errorf("failed to find project: %w", err) + } + // Dispatch audit event for project creation if a new project was created - if !projectAlreadyExists { + if existingProject == nil { uc.auditorUC.Dispatch(ctx, &events.ProjectCreated{ ProjectBase: &events.ProjectBase{ ProjectID: &wf.ProjectID, From 601513fc57a3ddaaed40a573c67840d865d4dc13 Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 12 Aug 2025 11:23:35 +0200 Subject: [PATCH 6/8] feat: add project version created event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ProjectVersionCreated audit event that triggers when a new project version is created during the attestation process. This provides audit trail for version creation similar to the existing project creation events. Changes: - Add ProjectVersionCreated event structure with version ID, version string, and prerelease flag - Update WorkflowRunRepo interface to track version creation during workflow run creation - Add auditor support to WorkflowRunUseCase to dispatch version creation events - Update wire dependency injection to pass auditor to WorkflowRunUseCase - Add comprehensive tests for the new event 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Signed-off-by: Miguel Martinez --- .../api/gen/temp-openapi/apidocs.swagger.json | 236 ++++++++++++++++++ app/controlplane/cmd/wire_gen.go | 2 +- .../pkg/auditor/events/project.go | 34 +++ .../pkg/auditor/events/project_test.go | 17 ++ .../projects/project_version_created.json | 19 ++ .../pkg/biz/mocks/WorkflowRunRepo.go | 10 +- .../pkg/biz/testhelpers/wire_gen.go | 2 +- app/controlplane/pkg/biz/workflowrun.go | 30 ++- app/controlplane/pkg/biz/workflowrun_test.go | 2 +- app/controlplane/pkg/data/workflowrun.go | 12 +- 10 files changed, 348 insertions(+), 16 deletions(-) create mode 100644 app/controlplane/api/gen/temp-openapi/apidocs.swagger.json create mode 100644 app/controlplane/pkg/auditor/events/testdata/projects/project_version_created.json diff --git a/app/controlplane/api/gen/temp-openapi/apidocs.swagger.json b/app/controlplane/api/gen/temp-openapi/apidocs.swagger.json new file mode 100644 index 000000000..876e2ba97 --- /dev/null +++ b/app/controlplane/api/gen/temp-openapi/apidocs.swagger.json @@ -0,0 +1,236 @@ +{ + "swagger": "2.0", + "info": { + "title": "Chainloop Controlplane API", + "termsOfService": "https://chainloop.dev/terms", + "version": "1.0", + "contact": { + "name": "Chainloop Support", + "url": "https://chainloop.dev", + "email": "support@chainloop.dev" + } + }, + "tags": [ + { + "name": "ReferrerService", + "description": "Referrer service for discovering referred content by digest" + } + ], + "host": "cp.chainloop.dev", + "schemes": [ + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/discover/shared/{digest}": { + "get": { + "summary": "Discover public shared referrer", + "description": "Returns the referrer item for a given digest in the public shared index", + "operationId": "ReferrerService_DiscoverPublicShared", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1DiscoverPublicSharedResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "digest", + "description": "Digest is the unique identifier of the referrer to discover", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "kind", + "description": "Kind is the optional type of referrer, i.e CONTAINER_IMAGE, GIT_HEAD, ...\nUsed to filter and resolve ambiguities", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ReferrerService" + ], + "produces": [ + "application/json" + ] + } + }, + "/discover/{digest}": { + "get": { + "summary": "Discover private referrer", + "description": "Returns the referrer item for a given digest in the organizations of the logged-in user", + "operationId": "ReferrerService_DiscoverPrivate", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/v1ReferrerServiceDiscoverPrivateResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "digest", + "description": "Digest is the unique identifier of the referrer to discover", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "kind", + "description": "Kind is the optional type of referrer, i.e CONTAINER_IMAGE, GIT_HEAD, ...\nUsed to filter and resolve ambiguities", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "ReferrerService" + ], + "produces": [ + "application/json" + ] + } + } + }, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "@type": { + "type": "string" + } + }, + "additionalProperties": {} + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/protobufAny" + } + } + } + }, + "v1DiscoverPublicSharedResponse": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/v1ReferrerItem", + "title": "Result is the discovered referrer item" + } + }, + "description": "Response for the DiscoverPublicShared method", + "title": "DiscoverPublicSharedResponse" + }, + "v1ReferrerItem": { + "type": "object", + "properties": { + "digest": { + "type": "string", + "title": "Digest of the referrer, i.e sha256:deadbeef or sha1:beefdead" + }, + "kind": { + "type": "string", + "description": "Kind of referrer, i.e CONTAINER_IMAGE, GIT_HEAD, ..." + }, + "downloadable": { + "type": "boolean", + "title": "Downloadable indicates whether the referrer is downloadable or not from CAS" + }, + "public": { + "type": "boolean", + "title": "Public indicates whether the referrer is public since it belongs to a public workflow" + }, + "references": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/v1ReferrerItem" + }, + "title": "References contains the list of related referrer items" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "CreatedAt is the timestamp when the referrer was created" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "title": "Metadata contains additional descriptive information about the referrer" + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "title": "Annotations are key-value pairs associated with the referrer" + } + }, + "description": "It represents a referrer object in the system", + "title": "ReferrerItem" + }, + "v1ReferrerServiceDiscoverPrivateResponse": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/v1ReferrerItem", + "title": "Result is the discovered referrer item" + } + }, + "description": "Response for the DiscoverPrivate method", + "title": "ReferrerServiceDiscoverPrivateResponse" + } + }, + "securityDefinitions": { + "bearerToken": { + "type": "apiKey", + "description": "Bearer token for authentication", + "name": "Authorization", + "in": "header" + } + }, + "security": [ + { + "bearerToken": [] + } + ], + "externalDocs": { + "description": "Chainloop Official Documentation", + "url": "https://docs.chainloop.dev" + } +} diff --git a/app/controlplane/cmd/wire_gen.go b/app/controlplane/cmd/wire_gen.go index 6c13dc68f..b70e4edb8 100644 --- a/app/controlplane/cmd/wire_gen.go +++ b/app/controlplane/cmd/wire_gen.go @@ -152,7 +152,7 @@ func wireApp(bootstrap *conf.Bootstrap, readerWriter credentials.ReaderWriter, l cleanup() return nil, nil, err } - workflowRunUseCase, err := biz.NewWorkflowRunUseCase(workflowRunRepo, workflowRepo, signingUseCase, logger) + workflowRunUseCase, err := biz.NewWorkflowRunUseCase(workflowRunRepo, workflowRepo, signingUseCase, auditorUseCase, logger) if err != nil { cleanup() return nil, nil, err diff --git a/app/controlplane/pkg/auditor/events/project.go b/app/controlplane/pkg/auditor/events/project.go index 86d1d1d5f..a3443f542 100644 --- a/app/controlplane/pkg/auditor/events/project.go +++ b/app/controlplane/pkg/auditor/events/project.go @@ -28,6 +28,7 @@ import ( var ( _ auditor.LogEntry = (*ProjectCreated)(nil) + _ auditor.LogEntry = (*ProjectVersionCreated)(nil) _ auditor.LogEntry = (*ProjectMembershipAdded)(nil) _ auditor.LogEntry = (*ProjectMembershipRemoved)(nil) _ auditor.LogEntry = (*ProjectMemberRoleUpdated)(nil) @@ -36,6 +37,7 @@ var ( const ( ProjectType auditor.TargetType = "Project" ProjectCreatedActionType string = "ProjectCreated" + ProjectVersionCreatedActionType string = "ProjectVersionCreated" ProjectMembershipAddedActionType string = "ProjectMembershipAdded" ProjectMembershipRemovedActionType string = "ProjectMembershipRemoved" ProjectMemberRoleUpdatedType string = "ProjectMemberRoleUpdated" @@ -88,6 +90,38 @@ func (p *ProjectCreated) Description() string { return fmt.Sprintf("%s has created the project '%s'", auditor.GetActorIdentifier(), p.ProjectName) } +// ProjectVersionCreated represents the creation of a project version +type ProjectVersionCreated struct { + *ProjectBase + VersionID *uuid.UUID `json:"version_id,omitempty"` + Version string `json:"version,omitempty"` + Prerelease bool `json:"prerelease"` +} + +func (p *ProjectVersionCreated) ActionType() string { + return ProjectVersionCreatedActionType +} + +func (p *ProjectVersionCreated) ActionInfo() (json.RawMessage, error) { + if _, err := p.ProjectBase.ActionInfo(); err != nil { + return nil, err + } + + if p.VersionID == nil || p.Version == "" { + return nil, errors.New("version id and version are required") + } + + return json.Marshal(&p) +} + +func (p *ProjectVersionCreated) Description() string { + releaseType := "release" + if p.Prerelease { + releaseType = "prerelease" + } + return fmt.Sprintf("%s has created %s version '%s' for project '%s'", auditor.GetActorIdentifier(), releaseType, p.Version, p.ProjectName) +} + // Helper function to make role names more user-friendly func prettyRole(role string) string { // Convert the role to a prettier format diff --git a/app/controlplane/pkg/auditor/events/project_test.go b/app/controlplane/pkg/auditor/events/project_test.go index e52f3f47c..45db2c0cd 100644 --- a/app/controlplane/pkg/auditor/events/project_test.go +++ b/app/controlplane/pkg/auditor/events/project_test.go @@ -39,6 +39,8 @@ func TestProjectEvents(t *testing.T) { require.NoError(t, err) memberUUID, err := uuid.Parse("4089bb36-e27b-428b-8009-d015c8737c57") require.NoError(t, err) + versionUUID, err := uuid.Parse("5089bb36-e27b-428b-8009-d015c8737c58") + require.NoError(t, err) projectName := "test-project" userEmail := "test@example.com" @@ -61,6 +63,21 @@ func TestProjectEvents(t *testing.T) { actor: auditor.ActorTypeUser, actorID: userUUID, }, + { + name: "ProjectVersionCreated", + event: &events.ProjectVersionCreated{ + ProjectBase: &events.ProjectBase{ + ProjectID: &projectUUID, + ProjectName: projectName, + }, + VersionID: &versionUUID, + Version: "v1.0.0", + Prerelease: false, + }, + expected: "testdata/projects/project_version_created.json", + actor: auditor.ActorTypeUser, + actorID: userUUID, + }, { name: "ProjectMembershipAdded", event: &events.ProjectMembershipAdded{ diff --git a/app/controlplane/pkg/auditor/events/testdata/projects/project_version_created.json b/app/controlplane/pkg/auditor/events/testdata/projects/project_version_created.json new file mode 100644 index 000000000..c54cd053a --- /dev/null +++ b/app/controlplane/pkg/auditor/events/testdata/projects/project_version_created.json @@ -0,0 +1,19 @@ +{ + "ActionType": "ProjectVersionCreated", + "TargetType": "Project", + "TargetID": "3089bb36-e27b-428b-8009-d015c8737c56", + "ActorType": "USER", + "ActorID": "1089bb36-e27b-428b-8009-d015c8737c54", + "ActorEmail": "john@cyberdyne.io", + "ActorName": "John Connor", + "OrgID": "1089bb36-e27b-428b-8009-d015c8737c54", + "Description": "John Connor has created release version 'v1.0.0' for project 'test-project'", + "Info": { + "project_id": "3089bb36-e27b-428b-8009-d015c8737c56", + "project_name": "test-project", + "version_id": "5089bb36-e27b-428b-8009-d015c8737c58", + "version": "v1.0.0", + "prerelease": false + }, + "Digest": "sha256:4cd962168303a29e0a3adb668b37086e50b15a92548d93a569eccf9a63db5122" +} \ No newline at end of file diff --git a/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go b/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go index d45087030..bb26edf79 100644 --- a/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go +++ b/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go @@ -24,23 +24,23 @@ type WorkflowRunRepo struct { } // Create provides a mock function with given fields: ctx, opts -func (_m *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoCreateOpts) (*biz.WorkflowRun, error) { +func (_m *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoCreateOpts) (*biz.WorkflowRunRepoCreateResult, error) { ret := _m.Called(ctx, opts) if len(ret) == 0 { panic("no return value specified for Create") } - var r0 *biz.WorkflowRun + var r0 *biz.WorkflowRunRepoCreateResult var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *biz.WorkflowRunRepoCreateOpts) (*biz.WorkflowRun, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *biz.WorkflowRunRepoCreateOpts) (*biz.WorkflowRunRepoCreateResult, error)); ok { return rf(ctx, opts) } - if rf, ok := ret.Get(0).(func(context.Context, *biz.WorkflowRunRepoCreateOpts) *biz.WorkflowRun); ok { + if rf, ok := ret.Get(0).(func(context.Context, *biz.WorkflowRunRepoCreateOpts) *biz.WorkflowRunRepoCreateResult); ok { r0 = rf(ctx, opts) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*biz.WorkflowRun) + r0 = ret.Get(0).(*biz.WorkflowRunRepoCreateResult) } } diff --git a/app/controlplane/pkg/biz/testhelpers/wire_gen.go b/app/controlplane/pkg/biz/testhelpers/wire_gen.go index 792161285..4c54d5f7b 100644 --- a/app/controlplane/pkg/biz/testhelpers/wire_gen.go +++ b/app/controlplane/pkg/biz/testhelpers/wire_gen.go @@ -89,7 +89,7 @@ func WireTestData(testDatabase *TestDatabase, t *testing.T, logger log.Logger, r cleanup() return nil, nil, err } - workflowRunUseCase, err := biz.NewWorkflowRunUseCase(workflowRunRepo, workflowRepo, signingUseCase, logger) + workflowRunUseCase, err := biz.NewWorkflowRunUseCase(workflowRunRepo, workflowRepo, signingUseCase, auditorUseCase, logger) if err != nil { cleanup() return nil, nil, err diff --git a/app/controlplane/pkg/biz/workflowrun.go b/app/controlplane/pkg/biz/workflowrun.go index 440a05ba4..5d23768a7 100644 --- a/app/controlplane/pkg/biz/workflowrun.go +++ b/app/controlplane/pkg/biz/workflowrun.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/chainloop-dev/chainloop/pkg/attestation" "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop" @@ -77,8 +78,13 @@ const ( WorkflowRunCancelled WorkflowRunStatus = "canceled" ) +type WorkflowRunRepoCreateResult struct { + Run *WorkflowRun + VersionCreated bool +} + type WorkflowRunRepo interface { - Create(ctx context.Context, opts *WorkflowRunRepoCreateOpts) (*WorkflowRun, error) + Create(ctx context.Context, opts *WorkflowRunRepoCreateOpts) (*WorkflowRunRepoCreateResult, error) FindByID(ctx context.Context, ID uuid.UUID) (*WorkflowRun, error) FindByAttestationDigest(ctx context.Context, digest string) (*WorkflowRun, error) FindByIDInOrg(ctx context.Context, orgID, ID uuid.UUID) (*WorkflowRun, error) @@ -97,17 +103,18 @@ type WorkflowRunUseCase struct { wfRunRepo WorkflowRunRepo wfRepo WorkflowRepo logger *log.Helper + auditorUC *AuditorUseCase signingUseCase *SigningUseCase } -func NewWorkflowRunUseCase(wfrRepo WorkflowRunRepo, wfRepo WorkflowRepo, suc *SigningUseCase, logger log.Logger) (*WorkflowRunUseCase, error) { +func NewWorkflowRunUseCase(wfrRepo WorkflowRunRepo, wfRepo WorkflowRepo, suc *SigningUseCase, auditorUC *AuditorUseCase, logger log.Logger) (*WorkflowRunUseCase, error) { if logger == nil { logger = log.NewStdLogger(io.Discard) } return &WorkflowRunUseCase{ - wfRunRepo: wfrRepo, wfRepo: wfRepo, + wfRunRepo: wfrRepo, wfRepo: wfRepo, auditorUC: auditorUC, signingUseCase: suc, logger: log.NewHelper(logger), }, nil @@ -224,7 +231,7 @@ func (uc *WorkflowRunUseCase) Create(ctx context.Context, opts *WorkflowRunCreat // For now we only associate the workflow run to one backend. // This might change in the future so we prepare the data layer to support an array of associated backends backends := []uuid.UUID{opts.CASBackendID} - run, err := uc.wfRunRepo.Create(ctx, + result, err := uc.wfRunRepo.Create(ctx, &WorkflowRunRepoCreateOpts{ WorkflowID: workflowUUID, SchemaVersionID: contractRevision.Version.ID, @@ -239,7 +246,20 @@ func (uc *WorkflowRunUseCase) Create(ctx context.Context, opts *WorkflowRunCreat return nil, err } - return run, nil + // Dispatch audit event for project version creation if a new version was created + if result.VersionCreated && result.Run != nil && result.Run.Workflow != nil && uc.auditorUC != nil { + uc.auditorUC.Dispatch(ctx, &events.ProjectVersionCreated{ + ProjectBase: &events.ProjectBase{ + ProjectID: &result.Run.Workflow.ProjectID, + ProjectName: result.Run.Workflow.Project, + }, + VersionID: &result.Run.ProjectVersion.ID, + Version: result.Run.ProjectVersion.Version, + Prerelease: result.Run.ProjectVersion.Prerelease, + }, &result.Run.Workflow.OrgID) + } + + return result.Run, nil } // The workflowRun belongs to the provided workflowRun diff --git a/app/controlplane/pkg/biz/workflowrun_test.go b/app/controlplane/pkg/biz/workflowrun_test.go index d3bca70be..3c9894a7c 100644 --- a/app/controlplane/pkg/biz/workflowrun_test.go +++ b/app/controlplane/pkg/biz/workflowrun_test.go @@ -53,7 +53,7 @@ type workflowrunTestSuite struct { func (s *workflowrunTestSuite) SetupTest() { s.repo = repoM.NewWorkflowRunRepo(s.T()) - uc, err := biz.NewWorkflowRunUseCase(s.repo, nil, nil, nil) + uc, err := biz.NewWorkflowRunUseCase(s.repo, nil, nil, nil, nil) require.NoError(s.T(), err) s.useCase = uc s.validID = uuid.New() diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index 8fd07a30c..635236860 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -47,7 +47,7 @@ func NewWorkflowRunRepo(data *Data, logger log.Logger) biz.WorkflowRunRepo { } } -func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoCreateOpts) (run *biz.WorkflowRun, err error) { +func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoCreateOpts) (*biz.WorkflowRunRepoCreateResult, error) { // Make this outside of the transaction to reduce the size of the blocking transaction wf, err := r.data.DB.Workflow.Get(ctx, opts.WorkflowID) if err != nil { @@ -62,6 +62,7 @@ func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoC } var p *ent.WorkflowRun + versionCreated := false // Create version and workflow in a transaction if err = WithTx(ctx, r.data.DB, func(tx *ent.Tx) error { if version == nil { @@ -69,6 +70,7 @@ func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoC if err != nil { return fmt.Errorf("creating version: %w", err) } + versionCreated = true } // Create workflow run @@ -110,7 +112,7 @@ func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoC return nil, err } - run, err = entWrToBizWr(ctx, p) + run, err := entWrToBizWr(ctx, p) if err != nil { return nil, fmt.Errorf("converting to biz: %w", err) } @@ -123,7 +125,11 @@ func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoC } run.ProjectVersion = entProjectVersionToBiz(version) - return run, err + + return &biz.WorkflowRunRepoCreateResult{ + Run: run, + VersionCreated: versionCreated, + }, nil } func eagerLoadWorkflowRun(client *ent.Client) *ent.WorkflowRunQuery { From e3fc0004026c431327cc14a62ae9ae01e7281b6a Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 12 Aug 2025 11:55:56 +0200 Subject: [PATCH 7/8] feat: add project events Signed-off-by: Miguel Martinez --- app/controlplane/pkg/biz/auditor.go | 2 ++ app/controlplane/pkg/biz/projectversion.go | 1 + app/controlplane/pkg/biz/workflow.go | 7 ++----- app/controlplane/pkg/biz/workflowrun.go | 11 ++++++----- app/controlplane/pkg/data/projectversion.go | 1 + app/controlplane/pkg/data/workflowrun.go | 7 +++++++ 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/controlplane/pkg/biz/auditor.go b/app/controlplane/pkg/biz/auditor.go index 87ecb37b3..70655fe1f 100644 --- a/app/controlplane/pkg/biz/auditor.go +++ b/app/controlplane/pkg/biz/auditor.go @@ -73,12 +73,14 @@ func (uc *AuditorUseCase) Dispatch(ctx context.Context, entry auditor.LogEntry, payload, err := auditor.GenerateAuditEvent(entry, opts...) if err != nil { + uc.log.Error("failed to generate audit event", "error", err) sentry.CaptureException(fmt.Errorf("failed to generate audit event: %w", err)) return } // Send event to event bus if err := uc.publisher.Publish(payload); err != nil { + uc.log.Error("failed to publish event", "error", err) sentry.CaptureException(fmt.Errorf("failed to publish event: %w", err)) } } diff --git a/app/controlplane/pkg/biz/projectversion.go b/app/controlplane/pkg/biz/projectversion.go index 336faad5a..db17cd433 100644 --- a/app/controlplane/pkg/biz/projectversion.go +++ b/app/controlplane/pkg/biz/projectversion.go @@ -38,6 +38,7 @@ type ProjectVersion struct { CreatedAt *time.Time // ReleasedAt is the time when the version was released. ReleasedAt *time.Time + ProjectID uuid.UUID } type ProjectVersionRepo interface { diff --git a/app/controlplane/pkg/biz/workflow.go b/app/controlplane/pkg/biz/workflow.go index 621464c13..05add7665 100644 --- a/app/controlplane/pkg/biz/workflow.go +++ b/app/controlplane/pkg/biz/workflow.go @@ -161,6 +161,8 @@ func (uc *WorkflowUseCase) Create(ctx context.Context, opts *WorkflowCreateOpts) return nil, fmt.Errorf("failed to parse org ID %q: %w", opts.OrgID, err) } + existingProject, _ := uc.projectRepo.FindProjectByOrgIDAndName(ctx, orgUUID, opts.Project) + wf, err := uc.wfRepo.Create(ctx, opts) if err != nil { if IsErrAlreadyExists(err) { @@ -177,11 +179,6 @@ func (uc *WorkflowUseCase) Create(ctx context.Context, opts *WorkflowCreateOpts) } } - existingProject, err := uc.projectRepo.FindProjectByOrgIDAndName(ctx, orgUUID, opts.Project) - if err != nil { - return nil, fmt.Errorf("failed to find project: %w", err) - } - // Dispatch audit event for project creation if a new project was created if existingProject == nil { uc.auditorUC.Dispatch(ctx, &events.ProjectCreated{ diff --git a/app/controlplane/pkg/biz/workflowrun.go b/app/controlplane/pkg/biz/workflowrun.go index 5d23768a7..2959658a9 100644 --- a/app/controlplane/pkg/biz/workflowrun.go +++ b/app/controlplane/pkg/biz/workflowrun.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2025 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -80,6 +80,7 @@ const ( type WorkflowRunRepoCreateResult struct { Run *WorkflowRun + Project *Project VersionCreated bool } @@ -247,16 +248,16 @@ func (uc *WorkflowRunUseCase) Create(ctx context.Context, opts *WorkflowRunCreat } // Dispatch audit event for project version creation if a new version was created - if result.VersionCreated && result.Run != nil && result.Run.Workflow != nil && uc.auditorUC != nil { + if result.VersionCreated && uc.auditorUC != nil && result.Project != nil { uc.auditorUC.Dispatch(ctx, &events.ProjectVersionCreated{ ProjectBase: &events.ProjectBase{ - ProjectID: &result.Run.Workflow.ProjectID, - ProjectName: result.Run.Workflow.Project, + ProjectID: &result.Project.ID, + ProjectName: result.Project.Name, }, VersionID: &result.Run.ProjectVersion.ID, Version: result.Run.ProjectVersion.Version, Prerelease: result.Run.ProjectVersion.Prerelease, - }, &result.Run.Workflow.OrgID) + }, &result.Project.OrgID) } return result.Run, nil diff --git a/app/controlplane/pkg/data/projectversion.go b/app/controlplane/pkg/data/projectversion.go index 3c7d50b0b..b95b4d250 100644 --- a/app/controlplane/pkg/data/projectversion.go +++ b/app/controlplane/pkg/data/projectversion.go @@ -126,6 +126,7 @@ func entProjectVersionToBiz(v *ent.ProjectVersion) *biz.ProjectVersion { TotalWorkflowRuns: v.WorkflowRunCount, CreatedAt: toTimePtr(v.CreatedAt), ReleasedAt: toTimePtr(v.ReleasedAt), + ProjectID: v.ProjectID, } return pv diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index 635236860..e69cd8dba 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -26,6 +26,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/attestation" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowrun" @@ -124,9 +125,15 @@ func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoC return nil, fmt.Errorf("reloading project version: %w", err) } + project, err := r.data.DB.Project.Query().Where(project.ID(wf.ProjectID)).First(ctx) + if err != nil { + return nil, fmt.Errorf("reloading project: %w", err) + } + run.ProjectVersion = entProjectVersionToBiz(version) return &biz.WorkflowRunRepoCreateResult{ + Project: entProjectToBiz(project), Run: run, VersionCreated: versionCreated, }, nil From c6757139da8630e7d51abd681dbee2af000d1dbd Mon Sep 17 00:00:00 2001 From: Miguel Martinez Date: Tue, 12 Aug 2025 11:57:56 +0200 Subject: [PATCH 8/8] add events Signed-off-by: Miguel Martinez --- .../api/gen/temp-openapi/apidocs.swagger.json | 236 ------------------ 1 file changed, 236 deletions(-) delete mode 100644 app/controlplane/api/gen/temp-openapi/apidocs.swagger.json diff --git a/app/controlplane/api/gen/temp-openapi/apidocs.swagger.json b/app/controlplane/api/gen/temp-openapi/apidocs.swagger.json deleted file mode 100644 index 876e2ba97..000000000 --- a/app/controlplane/api/gen/temp-openapi/apidocs.swagger.json +++ /dev/null @@ -1,236 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "title": "Chainloop Controlplane API", - "termsOfService": "https://chainloop.dev/terms", - "version": "1.0", - "contact": { - "name": "Chainloop Support", - "url": "https://chainloop.dev", - "email": "support@chainloop.dev" - } - }, - "tags": [ - { - "name": "ReferrerService", - "description": "Referrer service for discovering referred content by digest" - } - ], - "host": "cp.chainloop.dev", - "schemes": [ - "https" - ], - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "paths": { - "/discover/shared/{digest}": { - "get": { - "summary": "Discover public shared referrer", - "description": "Returns the referrer item for a given digest in the public shared index", - "operationId": "ReferrerService_DiscoverPublicShared", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/v1DiscoverPublicSharedResponse" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, - "parameters": [ - { - "name": "digest", - "description": "Digest is the unique identifier of the referrer to discover", - "in": "path", - "required": true, - "type": "string" - }, - { - "name": "kind", - "description": "Kind is the optional type of referrer, i.e CONTAINER_IMAGE, GIT_HEAD, ...\nUsed to filter and resolve ambiguities", - "in": "query", - "required": false, - "type": "string" - } - ], - "tags": [ - "ReferrerService" - ], - "produces": [ - "application/json" - ] - } - }, - "/discover/{digest}": { - "get": { - "summary": "Discover private referrer", - "description": "Returns the referrer item for a given digest in the organizations of the logged-in user", - "operationId": "ReferrerService_DiscoverPrivate", - "responses": { - "200": { - "description": "A successful response.", - "schema": { - "$ref": "#/definitions/v1ReferrerServiceDiscoverPrivateResponse" - } - }, - "default": { - "description": "An unexpected error response.", - "schema": { - "$ref": "#/definitions/rpcStatus" - } - } - }, - "parameters": [ - { - "name": "digest", - "description": "Digest is the unique identifier of the referrer to discover", - "in": "path", - "required": true, - "type": "string" - }, - { - "name": "kind", - "description": "Kind is the optional type of referrer, i.e CONTAINER_IMAGE, GIT_HEAD, ...\nUsed to filter and resolve ambiguities", - "in": "query", - "required": false, - "type": "string" - } - ], - "tags": [ - "ReferrerService" - ], - "produces": [ - "application/json" - ] - } - } - }, - "definitions": { - "protobufAny": { - "type": "object", - "properties": { - "@type": { - "type": "string" - } - }, - "additionalProperties": {} - }, - "rpcStatus": { - "type": "object", - "properties": { - "code": { - "type": "integer", - "format": "int32" - }, - "message": { - "type": "string" - }, - "details": { - "type": "array", - "items": { - "type": "object", - "$ref": "#/definitions/protobufAny" - } - } - } - }, - "v1DiscoverPublicSharedResponse": { - "type": "object", - "properties": { - "result": { - "$ref": "#/definitions/v1ReferrerItem", - "title": "Result is the discovered referrer item" - } - }, - "description": "Response for the DiscoverPublicShared method", - "title": "DiscoverPublicSharedResponse" - }, - "v1ReferrerItem": { - "type": "object", - "properties": { - "digest": { - "type": "string", - "title": "Digest of the referrer, i.e sha256:deadbeef or sha1:beefdead" - }, - "kind": { - "type": "string", - "description": "Kind of referrer, i.e CONTAINER_IMAGE, GIT_HEAD, ..." - }, - "downloadable": { - "type": "boolean", - "title": "Downloadable indicates whether the referrer is downloadable or not from CAS" - }, - "public": { - "type": "boolean", - "title": "Public indicates whether the referrer is public since it belongs to a public workflow" - }, - "references": { - "type": "array", - "items": { - "type": "object", - "$ref": "#/definitions/v1ReferrerItem" - }, - "title": "References contains the list of related referrer items" - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "CreatedAt is the timestamp when the referrer was created" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "title": "Metadata contains additional descriptive information about the referrer" - }, - "annotations": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "title": "Annotations are key-value pairs associated with the referrer" - } - }, - "description": "It represents a referrer object in the system", - "title": "ReferrerItem" - }, - "v1ReferrerServiceDiscoverPrivateResponse": { - "type": "object", - "properties": { - "result": { - "$ref": "#/definitions/v1ReferrerItem", - "title": "Result is the discovered referrer item" - } - }, - "description": "Response for the DiscoverPrivate method", - "title": "ReferrerServiceDiscoverPrivateResponse" - } - }, - "securityDefinitions": { - "bearerToken": { - "type": "apiKey", - "description": "Bearer token for authentication", - "name": "Authorization", - "in": "header" - } - }, - "security": [ - { - "bearerToken": [] - } - ], - "externalDocs": { - "description": "Chainloop Official Documentation", - "url": "https://docs.chainloop.dev" - } -}