From 094a972fe3834357b70d5bfceccafc2413e30f2d Mon Sep 17 00:00:00 2001 From: Vikram Vaswani <2571660+vvaswani@users.noreply.github.com> Date: Thu, 22 Jan 2026 04:06:16 +0530 Subject: [PATCH 1/2] feat: support relative paths for remote policies Signed-off-by: Vikram Vaswani <2571660+vvaswani@users.noreply.github.com> --- .../workflowcontract/v1/crafting_schema.proto | 14 +++-- pkg/policies/policies.go | 53 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto index a653f88ad..0fdf1c18d 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.proto +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.proto @@ -321,16 +321,20 @@ message PolicyInput { message PolicySpecV2 { oneof source { // path to a policy script. It might consist of a URI reference - string path = 1; + // deprecated: use ref instead + string path = 1 [deprecated = true]; // embedded source code (only Rego supported currently) string embedded = 2; + // generic reference for file:// and http(s):// schemes + string ref = 3; + option (buf.validate.oneof).required = true; } // if set, it will match any material supported by Chainloop - CraftingSchema.Material.MaterialType kind = 3 [(buf.validate.field).enum = { + CraftingSchema.Material.MaterialType kind = 4 [(buf.validate.field).enum = { not_in: [3] }]; } @@ -339,11 +343,15 @@ message PolicySpecV2 { message AutoMatch { oneof source { // path to a policy script. It might consist of a URI reference - string path = 1; + // deprecated: use ref instead + string path = 1 [deprecated = true]; // embedded source code (only Rego supported currently) string embedded = 2; + // generic reference for file:// and http(s):// schemes + string ref = 3; + option (buf.validate.oneof).required = true; } } diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index 5e8c67964..1ec256652 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -20,6 +20,7 @@ import ( "encoding/base64" "errors" "fmt" + "net/url" "path/filepath" "slices" "strings" @@ -723,13 +724,65 @@ func isURLPath(path string) bool { return scheme == httpScheme || scheme == httpsScheme } +// resolveReference resolves a reference (which may be relative) against a base path +// If ref is absolute (has a scheme), it returns ref as-is +// If basePath is a file:// path, it uses filepath.Join for resolution +// If basePath is an http(s):// URL, it uses URL parsing for resolution +func resolveReference(ref, basePath string) (string, error) { + // Check if ref is already absolute (has a scheme) + refScheme, _ := RefParts(ref) + if refScheme != "" { + // Already absolute, return as-is + return ref, nil + } + + // Get the scheme of basePath + baseScheme, baseLoc := RefParts(basePath) + + switch baseScheme { + case fileScheme: + // File path resolution + return filepath.Join(filepath.Dir(baseLoc), ref), nil + case httpScheme, httpsScheme: + // HTTP(S) URL resolution + baseURL, err := url.Parse(basePath) + if err != nil { + return "", fmt.Errorf("invalid base URL %q: %w", basePath, err) + } + + // Parse the reference relative to the base URL + resolvedURL, err := baseURL.Parse(ref) + if err != nil { + return "", fmt.Errorf("failed to resolve reference %q against base %q: %w", ref, basePath, err) + } + + return resolvedURL.String(), nil + case "": + // No scheme in basePath, treat as file path + return filepath.Join(filepath.Dir(basePath), ref), nil + default: + return "", fmt.Errorf("unsupported base path scheme: %s", baseScheme) + } +} + func loadPolicyScript(spec *v1.PolicySpecV2, basePath string) ([]byte, error) { var content []byte var err error switch source := spec.GetSource().(type) { case *v1.PolicySpecV2_Embedded: content = []byte(source.Embedded) + case *v1.PolicySpecV2_Ref: + // New ref field with relative URL resolution + scriptPath, err := resolveReference(source.Ref, basePath) + if err != nil { + return nil, fmt.Errorf("resolving policy reference: %w", err) + } + content, err = blob.LoadFileOrURL(scriptPath) + if err != nil { + return nil, fmt.Errorf("loading policy content: %w", err) + } case *v1.PolicySpecV2_Path: + // Deprecated: kept for backward compatibility var scriptPath string // If the path is a URL, use it directly. Otherwise, resolve it relative to basePath if isURLPath(source.Path) { From 0242e5b9be8c5c95707ca77c8c374216001c3542 Mon Sep 17 00:00:00 2001 From: Vikram Vaswani <2571660+vvaswani@users.noreply.github.com> Date: Sun, 25 Jan 2026 03:15:46 +0530 Subject: [PATCH 2/2] regen proto Signed-off-by: Vikram Vaswani <2571660+vvaswani@users.noreply.github.com> --- .../workflowcontract/v1/crafting_schema.ts | 58 ++++++++++++++-- ...kflowcontract.v1.AutoMatch.jsonschema.json | 6 +- .../workflowcontract.v1.AutoMatch.schema.json | 6 +- ...owcontract.v1.PolicySpecV2.jsonschema.json | 6 +- ...rkflowcontract.v1.PolicySpecV2.schema.json | 6 +- .../workflowcontract/v1/crafting_schema.pb.go | 66 ++++++++++++++++--- 6 files changed, 127 insertions(+), 21 deletions(-) diff --git a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts index 5703a9ca2..fa4ba1871 100644 --- a/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts +++ b/app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts @@ -511,7 +511,12 @@ export interface PolicyInput { } export interface PolicySpecV2 { - /** path to a policy script. It might consist of a URI reference */ + /** + * path to a policy script. It might consist of a URI reference + * deprecated: use ref instead + * + * @deprecated + */ path?: | string | undefined; @@ -519,18 +524,31 @@ export interface PolicySpecV2 { embedded?: | string | undefined; + /** generic reference for file:// and http(s):// schemes */ + ref?: + | string + | undefined; /** if set, it will match any material supported by Chainloop */ kind: CraftingSchema_Material_MaterialType; } /** Auto-matching policy specification */ export interface AutoMatch { - /** path to a policy script. It might consist of a URI reference */ + /** + * path to a policy script. It might consist of a URI reference + * deprecated: use ref instead + * + * @deprecated + */ path?: | string | undefined; /** embedded source code (only Rego supported currently) */ - embedded?: string | undefined; + embedded?: + | string + | undefined; + /** generic reference for file:// and http(s):// schemes */ + ref?: string | undefined; } /** Represents a group attachment in a contract */ @@ -2150,7 +2168,7 @@ export const PolicyInput = { }; function createBasePolicySpecV2(): PolicySpecV2 { - return { path: undefined, embedded: undefined, kind: 0 }; + return { path: undefined, embedded: undefined, ref: undefined, kind: 0 }; } export const PolicySpecV2 = { @@ -2161,8 +2179,11 @@ export const PolicySpecV2 = { if (message.embedded !== undefined) { writer.uint32(18).string(message.embedded); } + if (message.ref !== undefined) { + writer.uint32(26).string(message.ref); + } if (message.kind !== 0) { - writer.uint32(24).int32(message.kind); + writer.uint32(32).int32(message.kind); } return writer; }, @@ -2189,7 +2210,14 @@ export const PolicySpecV2 = { message.embedded = reader.string(); continue; case 3: - if (tag !== 24) { + if (tag !== 26) { + break; + } + + message.ref = reader.string(); + continue; + case 4: + if (tag !== 32) { break; } @@ -2208,6 +2236,7 @@ export const PolicySpecV2 = { return { path: isSet(object.path) ? String(object.path) : undefined, embedded: isSet(object.embedded) ? String(object.embedded) : undefined, + ref: isSet(object.ref) ? String(object.ref) : undefined, kind: isSet(object.kind) ? craftingSchema_Material_MaterialTypeFromJSON(object.kind) : 0, }; }, @@ -2216,6 +2245,7 @@ export const PolicySpecV2 = { const obj: any = {}; message.path !== undefined && (obj.path = message.path); message.embedded !== undefined && (obj.embedded = message.embedded); + message.ref !== undefined && (obj.ref = message.ref); message.kind !== undefined && (obj.kind = craftingSchema_Material_MaterialTypeToJSON(message.kind)); return obj; }, @@ -2228,13 +2258,14 @@ export const PolicySpecV2 = { const message = createBasePolicySpecV2(); message.path = object.path ?? undefined; message.embedded = object.embedded ?? undefined; + message.ref = object.ref ?? undefined; message.kind = object.kind ?? 0; return message; }, }; function createBaseAutoMatch(): AutoMatch { - return { path: undefined, embedded: undefined }; + return { path: undefined, embedded: undefined, ref: undefined }; } export const AutoMatch = { @@ -2245,6 +2276,9 @@ export const AutoMatch = { if (message.embedded !== undefined) { writer.uint32(18).string(message.embedded); } + if (message.ref !== undefined) { + writer.uint32(26).string(message.ref); + } return writer; }, @@ -2269,6 +2303,13 @@ export const AutoMatch = { message.embedded = reader.string(); continue; + case 3: + if (tag !== 26) { + break; + } + + message.ref = reader.string(); + continue; } if ((tag & 7) === 4 || tag === 0) { break; @@ -2282,6 +2323,7 @@ export const AutoMatch = { return { path: isSet(object.path) ? String(object.path) : undefined, embedded: isSet(object.embedded) ? String(object.embedded) : undefined, + ref: isSet(object.ref) ? String(object.ref) : undefined, }; }, @@ -2289,6 +2331,7 @@ export const AutoMatch = { const obj: any = {}; message.path !== undefined && (obj.path = message.path); message.embedded !== undefined && (obj.embedded = message.embedded); + message.ref !== undefined && (obj.ref = message.ref); return obj; }, @@ -2300,6 +2343,7 @@ export const AutoMatch = { const message = createBaseAutoMatch(); message.path = object.path ?? undefined; message.embedded = object.embedded ?? undefined; + message.ref = object.ref ?? undefined; return message; }, }; diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.AutoMatch.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.AutoMatch.jsonschema.json index 6cbf4b11d..3e647d611 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.AutoMatch.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.AutoMatch.jsonschema.json @@ -9,7 +9,11 @@ "type": "string" }, "path": { - "description": "path to a policy script. It might consist of a URI reference", + "description": "path to a policy script. It might consist of a URI reference\n deprecated: use ref instead", + "type": "string" + }, + "ref": { + "description": "generic reference for file:// and http(s):// schemes", "type": "string" } }, diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.AutoMatch.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.AutoMatch.schema.json index 6e2a2391e..80bb388f0 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.AutoMatch.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.AutoMatch.schema.json @@ -9,7 +9,11 @@ "type": "string" }, "path": { - "description": "path to a policy script. It might consist of a URI reference", + "description": "path to a policy script. It might consist of a URI reference\n deprecated: use ref instead", + "type": "string" + }, + "ref": { + "description": "generic reference for file:// and http(s):// schemes", "type": "string" } }, diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json index 462938725..9ab6b0199 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.jsonschema.json @@ -51,7 +51,11 @@ "description": "if set, it will match any material supported by Chainloop" }, "path": { - "description": "path to a policy script. It might consist of a URI reference", + "description": "path to a policy script. It might consist of a URI reference\n deprecated: use ref instead", + "type": "string" + }, + "ref": { + "description": "generic reference for file:// and http(s):// schemes", "type": "string" } }, diff --git a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json index e6e5f9af8..8661a5d44 100644 --- a/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json +++ b/app/controlplane/api/gen/jsonschema/workflowcontract.v1.PolicySpecV2.schema.json @@ -51,7 +51,11 @@ "description": "if set, it will match any material supported by Chainloop" }, "path": { - "description": "path to a policy script. It might consist of a URI reference", + "description": "path to a policy script. It might consist of a URI reference\n deprecated: use ref instead", + "type": "string" + }, + "ref": { + "description": "generic reference for file:// and http(s):// schemes", "type": "string" } }, diff --git a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go index 879c60cd3..d4a5b50fa 100644 --- a/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go +++ b/app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go @@ -1113,9 +1113,10 @@ type PolicySpecV2 struct { // // *PolicySpecV2_Path // *PolicySpecV2_Embedded + // *PolicySpecV2_Ref Source isPolicySpecV2_Source `protobuf_oneof:"source"` // if set, it will match any material supported by Chainloop - Kind CraftingSchema_Material_MaterialType `protobuf:"varint,3,opt,name=kind,proto3,enum=workflowcontract.v1.CraftingSchema_Material_MaterialType" json:"kind,omitempty"` + Kind CraftingSchema_Material_MaterialType `protobuf:"varint,4,opt,name=kind,proto3,enum=workflowcontract.v1.CraftingSchema_Material_MaterialType" json:"kind,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1157,6 +1158,7 @@ func (x *PolicySpecV2) GetSource() isPolicySpecV2_Source { return nil } +// Deprecated: Marked as deprecated in workflowcontract/v1/crafting_schema.proto. func (x *PolicySpecV2) GetPath() string { if x != nil { if x, ok := x.Source.(*PolicySpecV2_Path); ok { @@ -1175,6 +1177,15 @@ func (x *PolicySpecV2) GetEmbedded() string { return "" } +func (x *PolicySpecV2) GetRef() string { + if x != nil { + if x, ok := x.Source.(*PolicySpecV2_Ref); ok { + return x.Ref + } + } + return "" +} + func (x *PolicySpecV2) GetKind() CraftingSchema_Material_MaterialType { if x != nil { return x.Kind @@ -1188,6 +1199,9 @@ type isPolicySpecV2_Source interface { type PolicySpecV2_Path struct { // path to a policy script. It might consist of a URI reference + // deprecated: use ref instead + // + // Deprecated: Marked as deprecated in workflowcontract/v1/crafting_schema.proto. Path string `protobuf:"bytes,1,opt,name=path,proto3,oneof"` } @@ -1196,10 +1210,17 @@ type PolicySpecV2_Embedded struct { Embedded string `protobuf:"bytes,2,opt,name=embedded,proto3,oneof"` } +type PolicySpecV2_Ref struct { + // generic reference for file:// and http(s):// schemes + Ref string `protobuf:"bytes,3,opt,name=ref,proto3,oneof"` +} + func (*PolicySpecV2_Path) isPolicySpecV2_Source() {} func (*PolicySpecV2_Embedded) isPolicySpecV2_Source() {} +func (*PolicySpecV2_Ref) isPolicySpecV2_Source() {} + // Auto-matching policy specification type AutoMatch struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1207,6 +1228,7 @@ type AutoMatch struct { // // *AutoMatch_Path // *AutoMatch_Embedded + // *AutoMatch_Ref Source isAutoMatch_Source `protobuf_oneof:"source"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1249,6 +1271,7 @@ func (x *AutoMatch) GetSource() isAutoMatch_Source { return nil } +// Deprecated: Marked as deprecated in workflowcontract/v1/crafting_schema.proto. func (x *AutoMatch) GetPath() string { if x != nil { if x, ok := x.Source.(*AutoMatch_Path); ok { @@ -1267,12 +1290,24 @@ func (x *AutoMatch) GetEmbedded() string { return "" } +func (x *AutoMatch) GetRef() string { + if x != nil { + if x, ok := x.Source.(*AutoMatch_Ref); ok { + return x.Ref + } + } + return "" +} + type isAutoMatch_Source interface { isAutoMatch_Source() } type AutoMatch_Path struct { // path to a policy script. It might consist of a URI reference + // deprecated: use ref instead + // + // Deprecated: Marked as deprecated in workflowcontract/v1/crafting_schema.proto. Path string `protobuf:"bytes,1,opt,name=path,proto3,oneof"` } @@ -1281,10 +1316,17 @@ type AutoMatch_Embedded struct { Embedded string `protobuf:"bytes,2,opt,name=embedded,proto3,oneof"` } +type AutoMatch_Ref struct { + // generic reference for file:// and http(s):// schemes + Ref string `protobuf:"bytes,3,opt,name=ref,proto3,oneof"` +} + func (*AutoMatch_Path) isAutoMatch_Source() {} func (*AutoMatch_Embedded) isAutoMatch_Source() {} +func (*AutoMatch_Ref) isAutoMatch_Source() {} + // Represents a group attachment in a contract type PolicyGroupAttachment struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1929,15 +1971,17 @@ const file_workflowcontract_v1_crafting_schema_proto_rawDesc = "" + "\x14name.go_map_variable\x12:must contain only lowercase letters, numbers, and hyphens.\x1a'this.matches('^[a-zA-Z][a-zA-Z0-9_]*$')R\x04name\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x1a\n" + "\brequired\x18\x03 \x01(\bR\brequired\x12\x18\n" + - "\adefault\x18\x04 \x01(\tR\adefault\"\xac\x01\n" + - "\fPolicySpecV2\x12\x14\n" + - "\x04path\x18\x01 \x01(\tH\x00R\x04path\x12\x1c\n" + - "\bembedded\x18\x02 \x01(\tH\x00R\bembedded\x12W\n" + - "\x04kind\x18\x03 \x01(\x0e29.workflowcontract.v1.CraftingSchema.Material.MaterialTypeB\b\xbaH\x05\x82\x01\x02 \x03R\x04kindB\x0f\n" + - "\x06source\x12\x05\xbaH\x02\b\x01\"P\n" + - "\tAutoMatch\x12\x14\n" + - "\x04path\x18\x01 \x01(\tH\x00R\x04path\x12\x1c\n" + - "\bembedded\x18\x02 \x01(\tH\x00R\bembeddedB\x0f\n" + + "\adefault\x18\x04 \x01(\tR\adefault\"\xc4\x01\n" + + "\fPolicySpecV2\x12\x18\n" + + "\x04path\x18\x01 \x01(\tB\x02\x18\x01H\x00R\x04path\x12\x1c\n" + + "\bembedded\x18\x02 \x01(\tH\x00R\bembedded\x12\x12\n" + + "\x03ref\x18\x03 \x01(\tH\x00R\x03ref\x12W\n" + + "\x04kind\x18\x04 \x01(\x0e29.workflowcontract.v1.CraftingSchema.Material.MaterialTypeB\b\xbaH\x05\x82\x01\x02 \x03R\x04kindB\x0f\n" + + "\x06source\x12\x05\xbaH\x02\b\x01\"h\n" + + "\tAutoMatch\x12\x18\n" + + "\x04path\x18\x01 \x01(\tB\x02\x18\x01H\x00R\x04path\x12\x1c\n" + + "\bembedded\x18\x02 \x01(\tH\x00R\bembedded\x12\x12\n" + + "\x03ref\x18\x03 \x01(\tH\x00R\x03refB\x0f\n" + "\x06source\x12\x05\xbaH\x02\b\x01\"\xc9\x01\n" + "\x15PolicyGroupAttachment\x12\x19\n" + "\x03ref\x18\x01 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x03ref\x12H\n" + @@ -2070,10 +2114,12 @@ func file_workflowcontract_v1_crafting_schema_proto_init() { file_workflowcontract_v1_crafting_schema_proto_msgTypes[10].OneofWrappers = []any{ (*PolicySpecV2_Path)(nil), (*PolicySpecV2_Embedded)(nil), + (*PolicySpecV2_Ref)(nil), } file_workflowcontract_v1_crafting_schema_proto_msgTypes[11].OneofWrappers = []any{ (*AutoMatch_Path)(nil), (*AutoMatch_Embedded)(nil), + (*AutoMatch_Ref)(nil), } type x struct{} out := protoimpl.TypeBuilder{