Files
flex-auth/internal/adapters/keycloak/adapter_test.go
tegwick 360025e38b
Some checks failed
CI / Build and Test (push) Has been cancelled
CI / Lint (push) Has been cancelled
Add Keycloak authorization adapter path
2026-05-17 07:18:45 +02:00

193 lines
6.4 KiB
Go

package keycloak_test
import (
"context"
"errors"
"testing"
"github.com/netkingdom/flex-auth/internal/adapters/keycloak"
"github.com/netkingdom/flex-auth/pkg/api"
)
func TestBuildAuthorizationRequestUsesUMAPermissionShape(t *testing.T) {
adapter := newAdapter(t, &fakeClient{})
got, err := adapter.BuildAuthorizationRequest(api.CheckRequest{
ID: "check:keycloak",
Subject: api.SubjectRef{ID: "user:alice"},
Action: "read",
Resource: api.ResourceRef{ID: "document:internal-note", Type: "document", System: "markitect-tool"},
Context: map[string]any{"purpose": "support"},
CaringContext: caringDescriptor(),
})
if err != nil {
t.Fatalf("BuildAuthorizationRequest: %v", err)
}
if got.Realm != "platform" || got.Audience != "markitect-tool" {
t.Fatalf("realm/audience = %s/%s", got.Realm, got.Audience)
}
if got.Permission.ResourceID != "document:internal-note" || got.Permission.Scope != "read" {
t.Fatalf("permission = %+v", got.Permission)
}
if got.ClaimToken["caring_context"] == nil {
t.Fatal("claim token missing CARING context")
}
}
func TestResourceRegistrationsFromManifest(t *testing.T) {
got := keycloak.ResourceRegistrationsFromManifest(api.ResourceManifest{
ID: "manifest:markitect",
System: "markitect-tool",
Actions: []string{"read", "export"},
Resources: []api.Resource{
{
ID: "document:internal-note",
Type: "document",
Path: "/docs/internal-note",
Labels: []string{"internal"},
TrustZone: "internal",
Owner: "team:platform",
},
},
})
if len(got) != 1 {
t.Fatalf("len = %d", len(got))
}
if got[0].ID != "document:internal-note" || got[0].Type != "document" {
t.Fatalf("registration = %+v", got[0])
}
if len(got[0].Scopes) != 2 || got[0].Attributes["trust_zone"][0] != "internal" {
t.Fatalf("registration = %+v", got[0])
}
}
func TestAdapterCheckWrapsKeycloakAllow(t *testing.T) {
client := &fakeClient{
result: keycloak.AuthorizationResult{
Allowed: true,
Reason: "uma_permission_granted",
RPTTokenID: "rpt:123",
PolicyVersion: "kc-v2",
Diagnostics: map[string]any{"policy": "document-reader"},
},
}
adapter := newAdapter(t, client)
got, err := adapter.Check(context.Background(), api.CheckRequest{
ID: "check:allow",
Subject: api.SubjectRef{ID: "user:alice"},
Action: "read",
Resource: api.ResourceRef{ID: "document:internal-note", Type: "document", System: "markitect-tool"},
CaringContext: caringDescriptor(),
})
if err != nil {
t.Fatalf("Check: %v", err)
}
if got.Effect != api.DecisionEffectAllow || got.Reason != "uma_permission_granted" {
t.Fatalf("decision = %s/%s", got.Effect, got.Reason)
}
if got.Provenance.Evaluator != keycloak.EvaluatorName || got.MatchedPolicyVersion != "kc-v2" {
t.Fatalf("provenance = %+v matched=%s", got.Provenance, got.MatchedPolicyVersion)
}
if got.Diagnostics["permission"] != "document:internal-note#read" || got.Diagnostics["rpt_token_id"] != "rpt:123" {
t.Fatalf("diagnostics = %+v", got.Diagnostics)
}
if got.Caring == nil || got.Caring.Descriptor == nil {
t.Fatal("missing CARING descriptor")
}
}
func TestAdapterFailsClosedOnUnavailableKeycloak(t *testing.T) {
client := &fakeClient{
err: keycloak.NewBackendError(keycloak.FailureUnavailable, "authorize", errors.New("connect refused")),
}
adapter := newAdapter(t, client)
got, err := adapter.Check(context.Background(), api.CheckRequest{
ID: "check:down",
Subject: api.SubjectRef{ID: "user:alice"},
Action: "read",
Resource: api.ResourceRef{ID: "document:internal-note", Type: "document", System: "markitect-tool"},
CaringContext: caringDescriptor(),
})
if err != nil {
t.Fatalf("Check: %v", err)
}
if got.Effect != api.DecisionEffectDeny || got.Reason != "keycloak_unavailable" {
t.Fatalf("decision = %s/%s; want fail closed", got.Effect, got.Reason)
}
if got.Diagnostics["keycloak_failure"] != "unavailable" {
t.Fatalf("diagnostics = %+v", got.Diagnostics)
}
if got.Caring.ConformanceFindings[0].Code != "KEYCLOAK-UNAVAILABLE" {
t.Fatalf("finding = %+v", got.Caring.ConformanceFindings[0])
}
}
func TestRegisterResourceManifestDelegatesToClient(t *testing.T) {
client := &fakeClient{}
adapter := newAdapter(t, client)
report, err := adapter.RegisterResourceManifest(context.Background(), api.ResourceManifest{
System: "markitect-tool",
Actions: []string{"read"},
Resources: []api.Resource{
{ID: "document:internal-note", Type: "document"},
},
})
if err != nil {
t.Fatalf("RegisterResourceManifest: %v", err)
}
if report.ResourcesWritten != 1 || len(client.registered) != 1 {
t.Fatalf("report = %+v registered = %+v", report, client.registered)
}
}
func newAdapter(t *testing.T, client *fakeClient) *keycloak.Adapter {
t.Helper()
adapter, err := keycloak.New(client, keycloak.Options{
Realm: "platform",
Audience: "markitect-tool",
PolicyPackage: "keycloak.authz",
PolicyVersion: "v1",
})
if err != nil {
t.Fatalf("New: %v", err)
}
return adapter
}
func caringDescriptor() *api.CaringAccessDescriptor {
return &api.CaringAccessDescriptor{
ID: "descriptor:keycloak-reader",
Profile: api.CaringProfileCaring040RC2,
SubjectType: api.SubjectTypeHuman,
OrganizationRelation: api.OrganizationRelationCustomer,
CanonicalRole: api.CanonicalRoleDoer,
Scope: api.CaringScope{Level: api.ScopeLevelResource, ID: "document:internal-note"},
Planes: []api.Plane{api.PlaneData},
Capabilities: []api.Capability{api.CapabilityView},
ExposureModes: []api.ExposureMode{api.ExposureModeMasked},
Restrictions: []api.Restriction{api.RestrictionExportBlocked},
}
}
type fakeClient struct {
result keycloak.AuthorizationResult
err error
registered []keycloak.ResourceRegistration
}
func (c *fakeClient) Authorize(context.Context, keycloak.AuthorizationRequest) (keycloak.AuthorizationResult, error) {
return c.result, c.err
}
func (c *fakeClient) RegisterResources(_ context.Context, resources []keycloak.ResourceRegistration) (keycloak.ResourceImportReport, error) {
c.registered = append([]keycloak.ResourceRegistration(nil), resources...)
return keycloak.ResourceImportReport{ResourcesWritten: len(resources), ResourceServerID: "rs:markitect"}, nil
}
func (c *fakeClient) Health(context.Context) error {
return nil
}