diff --git a/Makefile b/Makefile index 08542f5..700ab2b 100644 --- a/Makefile +++ b/Makefile @@ -33,11 +33,16 @@ lint: go vet $(PKG); \ fi +GOBIN_PATH := $(shell go env GOPATH)/bin + sbom: @mkdir -p $(BIN_DIR) @if command -v cyclonedx-gomod >/dev/null 2>&1; then \ cyclonedx-gomod mod -json -output $(BIN_DIR)/sbom.cdx.json .; \ echo "SBOM written to $(BIN_DIR)/sbom.cdx.json (cyclonedx-gomod)"; \ + elif [ -x "$(GOBIN_PATH)/cyclonedx-gomod" ]; then \ + "$(GOBIN_PATH)/cyclonedx-gomod" mod -json -output $(BIN_DIR)/sbom.cdx.json .; \ + echo "SBOM written to $(BIN_DIR)/sbom.cdx.json (cyclonedx-gomod via GOPATH)"; \ elif command -v syft >/dev/null 2>&1; then \ syft . -o cyclonedx-json=$(BIN_DIR)/sbom.cdx.json; \ echo "SBOM written to $(BIN_DIR)/sbom.cdx.json (syft)"; \ diff --git a/examples/markitect/resource_manifest.yaml b/examples/markitect/resource_manifest.yaml new file mode 100644 index 0000000..415104f --- /dev/null +++ b/examples/markitect/resource_manifest.yaml @@ -0,0 +1,40 @@ +# Pinned example of the FlexAuthResourceManifest shape. +# +# Source: markitect-tool/examples/policy/flex-auth-resource-manifest.yaml +# (emitted by markitect_tool.policy.enterprise.FlexAuthResourceManifest in +# MKTT-WP-0014). Schema: ../../schemas/resource_manifest.schema.json. + +id: markitect-example-knowledge-base +system: markitect-tool +actions: + - read + - query + - search + - package + - export +resources: + - id: knowledge-base:markitect-example + type: knowledge_base + labels: + - public + trust_zone: public + owner: team:platform-architecture + - id: document:public-note + type: document + parent: knowledge-base:markitect-example + path: examples/policy/public-note.md + labels: + - public + trust_zone: public + owner: team:platform-architecture + - id: document:internal-note + type: document + parent: knowledge-base:markitect-example + path: examples/policy/private/internal-note.md + labels: + - internal + trust_zone: internal + owner: team:platform-architecture +metadata: + source: markitect example policy fixtures + flex_auth_contract: resource-registration-v0 diff --git a/go.mod b/go.mod index 203d46b..e877523 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/netkingdom/flex-auth go 1.22 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/api/resource_manifest.go b/pkg/api/resource_manifest.go new file mode 100644 index 0000000..0a8d38c --- /dev/null +++ b/pkg/api/resource_manifest.go @@ -0,0 +1,30 @@ +package api + +// ResourceManifest is the shape a protected system publishes to register +// its resources with flex-auth. The shape is pinned against the +// Markitect-side emitter in markitect-tool (MKTT-WP-0014); see +// schemas/resource_manifest.schema.json for the JSON Schema and +// examples/markitect/resource_manifest.yaml for the canonical example. +type ResourceManifest struct { + ID string `json:"id" yaml:"id"` + System string `json:"system" yaml:"system"` + Resources []Resource `json:"resources" yaml:"resources"` + Actions []string `json:"actions,omitempty" yaml:"actions,omitempty"` + Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"` +} + +// Resource is one entry in a ResourceManifest. +type Resource struct { + ID string `json:"id" yaml:"id"` + Type string `json:"type" yaml:"type"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + Parent string `json:"parent,omitempty" yaml:"parent,omitempty"` + Labels []string `json:"labels,omitempty" yaml:"labels,omitempty"` + TrustZone string `json:"trust_zone,omitempty" yaml:"trust_zone,omitempty"` + Owner string `json:"owner,omitempty" yaml:"owner,omitempty"` + Attributes map[string]any `json:"attributes,omitempty" yaml:"attributes,omitempty"` +} + +// FlexAuthContractV0 is the metadata.flex_auth_contract value that +// signals the v0 resource-registration contract. +const FlexAuthContractV0 = "resource-registration-v0" diff --git a/pkg/api/resource_manifest_test.go b/pkg/api/resource_manifest_test.go new file mode 100644 index 0000000..52eec3f --- /dev/null +++ b/pkg/api/resource_manifest_test.go @@ -0,0 +1,89 @@ +package api_test + +import ( + "os" + "path/filepath" + "testing" + + "gopkg.in/yaml.v3" + + "github.com/netkingdom/flex-auth/pkg/api" +) + +// TestResourceManifestExampleParses is the golden test for the pinned +// FlexAuthResourceManifest shape. It loads examples/markitect/resource_manifest.yaml +// and verifies every field the Markitect emitter produces. +func TestResourceManifestExampleParses(t *testing.T) { + path := filepath.Join("..", "..", "examples", "markitect", "resource_manifest.yaml") + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + + var got api.ResourceManifest + if err := yaml.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + + if got.ID != "markitect-example-knowledge-base" { + t.Errorf("ID = %q; want markitect-example-knowledge-base", got.ID) + } + if got.System != "markitect-tool" { + t.Errorf("System = %q; want markitect-tool", got.System) + } + if got.Metadata["flex_auth_contract"] != api.FlexAuthContractV0 { + t.Errorf("metadata.flex_auth_contract = %v; want %q", got.Metadata["flex_auth_contract"], api.FlexAuthContractV0) + } + + wantActions := []string{"read", "query", "search", "package", "export"} + if len(got.Actions) != len(wantActions) { + t.Fatalf("Actions len = %d; want %d", len(got.Actions), len(wantActions)) + } + for i, a := range wantActions { + if got.Actions[i] != a { + t.Errorf("Actions[%d] = %q; want %q", i, got.Actions[i], a) + } + } + + if len(got.Resources) != 3 { + t.Fatalf("Resources len = %d; want 3", len(got.Resources)) + } + + kb := got.Resources[0] + if kb.ID != "knowledge-base:markitect-example" || kb.Type != "knowledge_base" { + t.Errorf("resources[0] = %+v; want knowledge-base header", kb) + } + if kb.TrustZone != "public" { + t.Errorf("resources[0].trust_zone = %q; want public", kb.TrustZone) + } + + internal := got.Resources[2] + if internal.ID != "document:internal-note" { + t.Errorf("resources[2].ID = %q; want document:internal-note", internal.ID) + } + if internal.Parent != "knowledge-base:markitect-example" { + t.Errorf("resources[2].parent = %q; want knowledge-base:markitect-example", internal.Parent) + } + if internal.TrustZone != "internal" { + t.Errorf("resources[2].trust_zone = %q; want internal", internal.TrustZone) + } + if len(internal.Labels) != 1 || internal.Labels[0] != "internal" { + t.Errorf("resources[2].labels = %v; want [internal]", internal.Labels) + } +} + +func TestResourceManifestRequiredFields(t *testing.T) { + const minimalYAML = `id: m1 +system: s1 +resources: + - id: r1 + type: document +` + var m api.ResourceManifest + if err := yaml.Unmarshal([]byte(minimalYAML), &m); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if m.ID == "" || m.System == "" || len(m.Resources) != 1 { + t.Fatalf("minimal manifest did not round-trip: %+v", m) + } +} diff --git a/schemas/resource_manifest.schema.json b/schemas/resource_manifest.schema.json new file mode 100644 index 0000000..964af7f --- /dev/null +++ b/schemas/resource_manifest.schema.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://flex-auth.netkingdom/schemas/resource_manifest.schema.json", + "title": "FlexAuthResourceManifest", + "description": "Manifest a protected system publishes to register its resources with flex-auth. Pinned against the Markitect-side emitter in markitect-tool/src/markitect_tool/policy/enterprise.py (MKTT-WP-0014).", + "type": "object", + "additionalProperties": false, + "required": ["id", "system", "resources"], + "properties": { + "id": { + "type": "string", + "description": "Stable identifier of this manifest (e.g. 'markitect-example-knowledge-base').", + "minLength": 1 + }, + "system": { + "type": "string", + "description": "Slug of the protected system publishing the manifest. Matches a registered protected-system manifest in flex-auth (e.g. 'markitect-tool').", + "minLength": 1 + }, + "resources": { + "type": "array", + "description": "Resources to register with flex-auth. Order is not significant; identity is by 'id'.", + "items": {"$ref": "#/$defs/resource"} + }, + "actions": { + "type": "array", + "description": "Action vocabulary the manifest's resources expect. Validated against the protected system's declared actions on registration.", + "items": {"type": "string", "minLength": 1}, + "uniqueItems": true + }, + "metadata": { + "type": "object", + "description": "Free-form provenance and contract metadata. Conventions: 'source' (origin description), 'flex_auth_contract' (contract version string, currently 'resource-registration-v0').", + "additionalProperties": true + } + }, + "$defs": { + "resource": { + "type": "object", + "additionalProperties": false, + "required": ["id", "type"], + "properties": { + "id": { + "type": "string", + "description": "Stable resource identifier, conventionally ':' (e.g. 'document:architecture/adr-001').", + "minLength": 1 + }, + "type": { + "type": "string", + "description": "Resource type within the protected system's namespace (e.g. 'knowledge_base', 'repository', 'document', 'section', 'context_package', 'workflow_artifact', 'export'). Not enumerated — flex-auth validates against the protected system's declared namespace.", + "minLength": 1 + }, + "path": { + "type": "string", + "description": "Optional source path within the protected system (e.g. a filesystem path or repo-relative path).", + "minLength": 1 + }, + "parent": { + "type": "string", + "description": "Optional resource id of the parent resource for hierarchy and inherited access.", + "minLength": 1 + }, + "labels": { + "type": "array", + "description": "Policy labels applied to the resource (e.g. 'public', 'internal', 'restricted').", + "items": {"type": "string", "minLength": 1}, + "uniqueItems": true + }, + "trust_zone": { + "type": "string", + "description": "Coarse trust classification (e.g. 'public', 'internal', 'restricted').", + "minLength": 1 + }, + "owner": { + "type": "string", + "description": "Owner identifier, conventionally 'team:' or 'user:'.", + "minLength": 1 + }, + "attributes": { + "type": "object", + "description": "Free-form attributes that policy packages may consult. Reserved keys may be defined by individual policy packages.", + "additionalProperties": true + } + } + } + } +} diff --git a/workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md b/workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md index 764ba70..8e27c73 100644 --- a/workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md +++ b/workplans/FLEX-WP-0005-foundations-and-topaz-alignment.md @@ -98,7 +98,7 @@ checklist. ```task id: FLEX-WP-0005-T003 -status: todo +status: done priority: high state_hub_task_id: "80285e1e-16ec-4f4e-b491-1e79f200219f" ```