diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 4035b1508a..1949796606 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -445,7 +445,7 @@ jobs: run: | if [ "${{ github.event_name }}" = "pull_request" ]; then BASE_SHA="${{ github.event.pull_request.base.sha }}" - if git diff --name-only "$BASE_SHA" HEAD | grep -q '\.proto$'; then + if git diff --name-only "$BASE_SHA" HEAD | grep -qE '\.proto$|^Makefile$|^buf\.|^protocol/codegen/|^protocol/go/internal/|^sdk/codegen/'; then echo "proto=true" >> "$GITHUB_OUTPUT" else echo "proto=false" >> "$GITHUB_OUTPUT" diff --git a/Makefile b/Makefile index 035da1d35e..ead4689a47 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # make # To run all lint checks: `LINT_OPTIONS= make lint` -.PHONY: all build clean connect-wrapper-generate docker-build fix fmt go-lint license lint proto-generate proto-lint sdk/sdk test tidy toolcheck +.PHONY: all build clean connect-wrapper-generate docker-build fix fmt go-lint license lint proto-generate proto-helper-generate proto-lint sdk/sdk test tidy toolcheck MODS=protocol/go lib/ocrypto lib/fixtures lib/flattening lib/identifier sdk service examples HAND_MODS=lib/ocrypto lib/fixtures lib/flattening lib/identifier sdk service examples @@ -74,7 +74,7 @@ govulncheck: proto-generate: toolcheck # remove all generated directories under protocol/go - find protocol/go -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + + find protocol/go -mindepth 1 -maxdepth 1 -type d ! -name internal -exec rm -rf {} + rm -rf docs/grpc docs/openapi buf generate service buf generate service --template buf.gen.grpc.docs.yaml @@ -84,11 +84,15 @@ proto-generate: toolcheck buf generate buf.build/grpc-ecosystem/grpc-gateway -o tmp-gen --template buf.gen.grpc.docs.yaml buf generate buf.build/grpc-ecosystem/grpc-gateway -o tmp-gen --template buf.gen.openapi.docs.yaml + cd protocol/codegen && go run . go run ./sdk/codegen connect-wrapper-generate: go run ./sdk/codegen +proto-helper-generate: + cd protocol/codegen && go run . + policy-sql-gen: @which sqlc > /dev/null || { echo "sqlc not found, please install it: https://docs.sqlc.dev/en/stable/overview/install.html"; exit 1; } sqlc generate -f service/policy/db/sqlc.yaml diff --git a/protocol/codegen/go.mod b/protocol/codegen/go.mod new file mode 100644 index 0000000000..1c11114085 --- /dev/null +++ b/protocol/codegen/go.mod @@ -0,0 +1,3 @@ +module github.com/opentdf/platform/protocol/codegen + +go 1.25.5 diff --git a/protocol/codegen/go.work b/protocol/codegen/go.work new file mode 100644 index 0000000000..2789a8cc01 --- /dev/null +++ b/protocol/codegen/go.work @@ -0,0 +1,5 @@ +// Isolate this module from the root workspace so `go run .` resolves locally +// without adding protocol/codegen to the root go.work. +go 1.25.5 + +use . diff --git a/protocol/codegen/main.go b/protocol/codegen/main.go new file mode 100644 index 0000000000..623ed87b57 --- /dev/null +++ b/protocol/codegen/main.go @@ -0,0 +1,157 @@ +// Command codegen copies helper source files from protocol/go/internal/ into their +// corresponding proto package directories with a "Code generated" header prepended. +// Helper source files import proto types explicitly for IDE support; the copier strips +// the self-referencing import and type qualifiers so the output compiles in-package. +// +// See https://github.com/opentdf/platform/pull/3232 for background on the source-file codegen approach. +package main + +import ( + "errors" + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" +) + +// helperMapping defines a source directory (relative to protocol/go/internal/) and its +// target directory (relative to protocol/go/) where files will be copied. +type helperMapping struct { + // Source is the subdirectory under internal/ containing the source files. + Source string + // Target is the subdirectory under protocol/go/ where files are copied. + Target string + // ProtoImportPath is the full Go import path of the target proto package. + ProtoImportPath string + // ProtoImportAlias is the import alias used in the source files for the proto package. + ProtoImportAlias string +} + +var mappings = []helperMapping{ + { + Source: "authorization/v2", + Target: "authorization/v2", + ProtoImportPath: "github.com/opentdf/platform/protocol/go/authorization/v2", + ProtoImportAlias: "authorizationv2", + }, +} + +const generatedHeader = "// Code generated by protocol/codegen. DO NOT EDIT.\n\n" + +func main() { + baseDir, err := getBaseDir() + if err != nil { + log.Fatal(err) + } + + helpersDir := filepath.Join(baseDir, "internal") + for _, m := range mappings { + srcDir := filepath.Join(helpersDir, m.Source) + dstDir := filepath.Join(baseDir, m.Target) + if err := copyHelpers(srcDir, dstDir, m); err != nil { + log.Fatalf("copying helpers from %s to %s: %v", srcDir, dstDir, err) + } + } +} + +// generatedFile holds a transformed helper ready to write. +type generatedFile struct { + dst string + content []byte + src string +} + +func copyHelpers(srcDir, dstDir string, m helperMapping) error { + entries, err := os.ReadDir(srcDir) + if err != nil { + return fmt.Errorf("reading source directory: %w", err) + } + + // Read and transform all source files before touching the target directory. + var files []generatedFile + for _, entry := range entries { + name := entry.Name() + if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { + continue + } + + src := filepath.Join(srcDir, name) + content, err := os.ReadFile(src) + if err != nil { + return fmt.Errorf("reading %s: %w", src, err) + } + + transformed := rewriteImports(string(content), m) + outName := strings.TrimSuffix(name, ".go") + ".gen.go" + + files = append(files, generatedFile{ + dst: filepath.Join(dstDir, outName), + content: []byte(generatedHeader + transformed), + src: src, + }) + } + + // Only remove stale .gen.go files once all reads succeeded. + if err := removeGenFiles(dstDir); err != nil { + return fmt.Errorf("cleaning target directory: %w", err) + } + + for _, f := range files { + if err := os.WriteFile(f.dst, f.content, 0o644); err != nil { + return fmt.Errorf("writing %s: %w", f.dst, err) + } + fmt.Printf(" %s -> %s\n", f.src, f.dst) + } + return nil +} + +func removeGenFiles(dir string) error { + entries, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("reading directory: %w", err) + } + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".gen.go") { + if err := os.Remove(filepath.Join(dir, entry.Name())); err != nil { + return fmt.Errorf("removing %s: %w", entry.Name(), err) + } + } + } + return nil +} + +// rewriteImports removes the self-referencing proto import and strips the alias qualifier +// from type references so the file compiles inside the proto package. +// +// The qualifier regex also matches inside string literals and comments. This is acceptable +// because we control the source files and don't use the alias in non-code contexts. +func rewriteImports(content string, m helperMapping) string { + // Remove the import line: `authorizationv2 "github.com/.../authorization/v2"` + importLineRe := regexp.MustCompile( + `(?m)^\s*` + regexp.QuoteMeta(m.ProtoImportAlias) + `\s+"` + regexp.QuoteMeta(m.ProtoImportPath) + `"\s*\n`, + ) + content = importLineRe.ReplaceAllString(content, "") + + // Strip the alias qualifier from type references: `authorizationv2.Foo` -> `Foo` + qualifierRe := regexp.MustCompile(regexp.QuoteMeta(m.ProtoImportAlias) + `\.`) + content = qualifierRe.ReplaceAllString(content, "") + + // Clean up empty import blocks left behind when the proto import was the only one. + emptyImportRe := regexp.MustCompile(`\nimport \(\n\)\n`) + content = emptyImportRe.ReplaceAllString(content, "") + + return content +} + +// getBaseDir returns the protocol/go/ directory by navigating from this file's location. +// From protocol/codegen/main.go, go up two levels to protocol/, then into go/. +func getBaseDir() (string, error) { + _, filename, _, ok := runtime.Caller(0) + if !ok { + return "", errors.New("could not determine current file location") + } + return filepath.Join(filepath.Dir(filepath.Dir(filename)), "go"), nil +} diff --git a/protocol/codegen/main_test.go b/protocol/codegen/main_test.go new file mode 100644 index 0000000000..0c9f54e301 --- /dev/null +++ b/protocol/codegen/main_test.go @@ -0,0 +1,251 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRewriteImports(t *testing.T) { + m := helperMapping{ + ProtoImportPath: "github.com/opentdf/platform/protocol/go/authorization/v2", + ProtoImportAlias: "authorizationv2", + } + + tests := []struct { + name string + input string + want string + }{ + { + name: "strips import line and qualifiers", + input: `package authorizationv2 + +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" +) + +func ForClientID(clientID string) *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_EntityChain{}, + } +} +`, + want: `package authorizationv2 + +import ( + "github.com/opentdf/platform/protocol/go/entity" +) + +func ForClientID(clientID string) *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_EntityChain{}, + } +} +`, + }, + { + name: "preserves other imports", + input: `package authorizationv2 + +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func WithRequestToken() *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_WithRequestToken{ + WithRequestToken: wrapperspb.Bool(true), + }, + } +} +`, + want: `package authorizationv2 + +import ( + "github.com/opentdf/platform/protocol/go/entity" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func WithRequestToken() *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_WithRequestToken{ + WithRequestToken: wrapperspb.Bool(true), + }, + } +} +`, + }, + { + name: "no-op when no matching import", + input: "package foo\n\nfunc Bar() {}\n", + want: "package foo\n\nfunc Bar() {}\n", + }, + { + name: "does not strip partial alias matches", + input: `package authorizationv2 + +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" +) + +// authorizationv2helper is not a qualifier reference +var authorizationv2helper = "should stay" +func F() *authorizationv2.EntityIdentifier { return nil } +`, + want: `package authorizationv2 + +// authorizationv2helper is not a qualifier reference +var authorizationv2helper = "should stay" +func F() *EntityIdentifier { return nil } +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := rewriteImports(tt.input, m) + if got != tt.want { + t.Errorf("rewriteImports() mismatch\n--- got ---\n%s\n--- want ---\n%s", got, tt.want) + } + }) + } +} + +func TestRemoveGenFiles(t *testing.T) { + dir := t.TempDir() + + // Create a mix of files: .gen.go (should be removed), .pb.go and .go (should survive) + files := map[string]bool{ + "entity_identifier.gen.go": false, // expect removed + "other_helper.gen.go": false, // expect removed + "authorization.pb.go": true, // expect kept + "authorization_grpc.pb.go": true, // expect kept + "regular.go": true, // expect kept + } + for name := range files { + if err := os.WriteFile(filepath.Join(dir, name), []byte("package x"), 0o644); err != nil { + t.Fatal(err) + } + } + + if err := removeGenFiles(dir); err != nil { + t.Fatal(err) + } + + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatal(err) + } + + remaining := make(map[string]bool) + for _, e := range entries { + remaining[e.Name()] = true + } + + for name, shouldExist := range files { + if shouldExist && !remaining[name] { + t.Errorf("%s was removed but should have been kept", name) + } + if !shouldExist && remaining[name] { + t.Errorf("%s was kept but should have been removed", name) + } + } +} + +func TestCopyHelpers(t *testing.T) { + m := helperMapping{ + Source: "test-pkg", + Target: "test-pkg", + ProtoImportPath: "github.com/example/proto/test", + ProtoImportAlias: "testpkg", + } + + srcDir := filepath.Join(t.TempDir(), "src") + dstDir := filepath.Join(t.TempDir(), "dst") + if err := os.MkdirAll(srcDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(dstDir, 0o755); err != nil { + t.Fatal(err) + } + + // Source .go file that should be copied and transformed. + helperContent := `package testpkg + +import ( + testpkg "github.com/example/proto/test" +) + +func NewFoo() *testpkg.Foo { + return &testpkg.Foo{} +} +` + if err := os.WriteFile(filepath.Join(srcDir, "helper.go"), []byte(helperContent), 0o644); err != nil { + t.Fatal(err) + } + + // _test.go file that should be skipped. + if err := os.WriteFile(filepath.Join(srcDir, "helper_test.go"), []byte("package testpkg\n"), 0o644); err != nil { + t.Fatal(err) + } + + // Non-Go file that should be skipped. + if err := os.WriteFile(filepath.Join(srcDir, "README.md"), []byte("# readme\n"), 0o644); err != nil { + t.Fatal(err) + } + + // Pre-existing stale .gen.go that should be cleaned up. + staleFile := filepath.Join(dstDir, "old_helper.gen.go") + if err := os.WriteFile(staleFile, []byte("package testpkg\n"), 0o644); err != nil { + t.Fatal(err) + } + + if err := copyHelpers(srcDir, dstDir, m); err != nil { + t.Fatalf("copyHelpers failed: %v", err) + } + + // Verify the transformed file was written with .gen.go suffix. + genFile := filepath.Join(dstDir, "helper.gen.go") + content, err := os.ReadFile(genFile) + if err != nil { + t.Fatalf("expected helper.gen.go to exist: %v", err) + } + + got := string(content) + + // Verify generated header is prepended. + if !strings.HasPrefix(got, generatedHeader) { + t.Errorf("missing generated header, starts with: %q", got[:min(len(got), 60)]) + } + + // Verify import rewriting happened (self-referencing import removed, qualifier stripped). + if strings.Contains(got, `"github.com/example/proto/test"`) { + t.Error("self-referencing import was not stripped") + } + if strings.Contains(got, "testpkg.Foo") { + t.Error("qualifier was not stripped from type references") + } + if !strings.Contains(got, "*Foo") { + t.Error("expected unqualified type reference *Foo") + } + + // Verify _test.go was not copied. + if _, err := os.Stat(filepath.Join(dstDir, "helper_test.gen.go")); err == nil { + t.Error("_test.go file should not be copied") + } + + // Verify non-Go file was not copied. + if _, err := os.Stat(filepath.Join(dstDir, "README.gen.go")); err == nil { + t.Error("non-Go file should not be copied") + } + + // Verify stale .gen.go was removed. + if _, err := os.Stat(staleFile); err == nil { + t.Error("stale old_helper.gen.go should have been removed") + } +} diff --git a/protocol/go/authorization/v2/entity_identifier.gen.go b/protocol/go/authorization/v2/entity_identifier.gen.go new file mode 100644 index 0000000000..c226fb5a56 --- /dev/null +++ b/protocol/go/authorization/v2/entity_identifier.gen.go @@ -0,0 +1,64 @@ +// Code generated by protocol/codegen. DO NOT EDIT. + +package authorizationv2 + +import ( + "github.com/opentdf/platform/protocol/go/entity" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// ForToken returns an EntityIdentifier that resolves the entity from the given JWT. +// The authorization service will parse the token to derive the entity chain. +func ForToken(jwt string) *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_Token{ + Token: &entity.Token{ + Jwt: jwt, + }, + }, + } +} + +// WithRequestToken returns an EntityIdentifier that instructs the authorization +// service to derive the entity from the request's Authorization header token. +func WithRequestToken() *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_WithRequestToken{ + WithRequestToken: wrapperspb.Bool(true), + }, + } +} + +// ForClientID returns an EntityIdentifier for a single subject entity identified by client ID. +func ForClientID(clientID string) *EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_ClientId{ClientId: clientID}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +// ForEmail returns an EntityIdentifier for a single subject entity identified by email address. +func ForEmail(email string) *EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_EmailAddress{EmailAddress: email}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +// ForUserName returns an EntityIdentifier for a single subject entity identified by username. +func ForUserName(username string) *EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_UserName{UserName: username}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +func entityIdentifierFromEntity(e *entity.Entity) *EntityIdentifier { + return &EntityIdentifier{ + Identifier: &EntityIdentifier_EntityChain{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{e}, + }, + }, + } +} diff --git a/protocol/go/internal/authorization/v2/entity_identifier.go b/protocol/go/internal/authorization/v2/entity_identifier.go new file mode 100644 index 0000000000..d5b6134236 --- /dev/null +++ b/protocol/go/internal/authorization/v2/entity_identifier.go @@ -0,0 +1,63 @@ +package authorizationv2 + +import ( + authorizationv2 "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +// ForToken returns an EntityIdentifier that resolves the entity from the given JWT. +// The authorization service will parse the token to derive the entity chain. +func ForToken(jwt string) *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_Token{ + Token: &entity.Token{ + Jwt: jwt, + }, + }, + } +} + +// WithRequestToken returns an EntityIdentifier that instructs the authorization +// service to derive the entity from the request's Authorization header token. +func WithRequestToken() *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_WithRequestToken{ + WithRequestToken: wrapperspb.Bool(true), + }, + } +} + +// ForClientID returns an EntityIdentifier for a single subject entity identified by client ID. +func ForClientID(clientID string) *authorizationv2.EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_ClientId{ClientId: clientID}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +// ForEmail returns an EntityIdentifier for a single subject entity identified by email address. +func ForEmail(email string) *authorizationv2.EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_EmailAddress{EmailAddress: email}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +// ForUserName returns an EntityIdentifier for a single subject entity identified by username. +func ForUserName(username string) *authorizationv2.EntityIdentifier { + return entityIdentifierFromEntity(&entity.Entity{ + EntityType: &entity.Entity_UserName{UserName: username}, + Category: entity.Entity_CATEGORY_SUBJECT, + }) +} + +func entityIdentifierFromEntity(e *entity.Entity) *authorizationv2.EntityIdentifier { + return &authorizationv2.EntityIdentifier{ + Identifier: &authorizationv2.EntityIdentifier_EntityChain{ + EntityChain: &entity.EntityChain{ + Entities: []*entity.Entity{e}, + }, + }, + } +} diff --git a/protocol/go/internal/authorization/v2/entity_identifier_test.go b/protocol/go/internal/authorization/v2/entity_identifier_test.go new file mode 100644 index 0000000000..f7d0658169 --- /dev/null +++ b/protocol/go/internal/authorization/v2/entity_identifier_test.go @@ -0,0 +1,160 @@ +package authorizationv2 + +import ( + "testing" + + authorizationv2proto "github.com/opentdf/platform/protocol/go/authorization/v2" + "github.com/opentdf/platform/protocol/go/entity" +) + +func TestForToken(t *testing.T) { + jwt := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test" + eid := ForToken(jwt) + + tok, ok := eid.GetIdentifier().(*authorizationv2proto.EntityIdentifier_Token) + if !ok { + t.Fatal("expected Token identifier") + } + if got := tok.Token.GetJwt(); got != jwt { + t.Errorf("jwt = %q, want %q", got, jwt) + } +} + +func TestForToken_EmptyString(t *testing.T) { + eid := ForToken("") + + tok, ok := eid.GetIdentifier().(*authorizationv2proto.EntityIdentifier_Token) + if !ok { + t.Fatal("expected Token identifier") + } + if got := tok.Token.GetJwt(); got != "" { + t.Errorf("jwt = %q, want empty string", got) + } +} + +func TestWithRequestToken(t *testing.T) { + eid := WithRequestToken() + + wrt, ok := eid.GetIdentifier().(*authorizationv2proto.EntityIdentifier_WithRequestToken) + if !ok { + t.Fatal("expected WithRequestToken identifier") + } + if !wrt.WithRequestToken.GetValue() { + t.Error("expected WithRequestToken value to be true") + } +} + +func TestEntityChainConstructors(t *testing.T) { + tests := []struct { + name string + constructor func(string) *authorizationv2proto.EntityIdentifier + input string + checkType func(*entity.Entity) (string, bool) + }{ + { + name: "ForClientID", + constructor: ForClientID, + input: "my-client", + checkType: func(e *entity.Entity) (string, bool) { + cid, ok := e.GetEntityType().(*entity.Entity_ClientId) + if !ok { + return "", false + } + return cid.ClientId, true + }, + }, + { + name: "ForClientID_EmptyString", + constructor: ForClientID, + input: "", + checkType: func(e *entity.Entity) (string, bool) { + cid, ok := e.GetEntityType().(*entity.Entity_ClientId) + if !ok { + return "", false + } + return cid.ClientId, true + }, + }, + { + name: "ForEmail", + constructor: ForEmail, + input: "user@example.com", + checkType: func(e *entity.Entity) (string, bool) { + em, ok := e.GetEntityType().(*entity.Entity_EmailAddress) + if !ok { + return "", false + } + return em.EmailAddress, true + }, + }, + { + name: "ForEmail_EmptyString", + constructor: ForEmail, + input: "", + checkType: func(e *entity.Entity) (string, bool) { + em, ok := e.GetEntityType().(*entity.Entity_EmailAddress) + if !ok { + return "", false + } + return em.EmailAddress, true + }, + }, + { + name: "ForUserName", + constructor: ForUserName, + input: "alice", + checkType: func(e *entity.Entity) (string, bool) { + un, ok := e.GetEntityType().(*entity.Entity_UserName) + if !ok { + return "", false + } + return un.UserName, true + }, + }, + { + name: "ForUserName_EmptyString", + constructor: ForUserName, + input: "", + checkType: func(e *entity.Entity) (string, bool) { + un, ok := e.GetEntityType().(*entity.Entity_UserName) + if !ok { + return "", false + } + return un.UserName, true + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + eid := tt.constructor(tt.input) + + chain := extractEntityChain(t, eid) + entities := chain.GetEntities() + if len(entities) != 1 { + t.Fatalf("entities len = %d, want 1", len(entities)) + } + + e := entities[0] + got, ok := tt.checkType(e) + if !ok { + t.Fatalf("unexpected entity type for %s", tt.name) + } + if got != tt.input { + t.Errorf("%s value = %q, want %q", tt.name, got, tt.input) + } + if e.GetCategory() != entity.Entity_CATEGORY_SUBJECT { + t.Errorf("category = %v, want CATEGORY_SUBJECT", e.GetCategory()) + } + }) + } +} + +func extractEntityChain(t *testing.T, eid *authorizationv2proto.EntityIdentifier) *entity.EntityChain { + t.Helper() + ec, ok := eid.GetIdentifier().(*authorizationv2proto.EntityIdentifier_EntityChain) + if !ok { + t.Fatal("expected EntityChain identifier") + } + return ec.EntityChain +}