diff --git a/extauthz/Makefile b/extauthz/Makefile index 347d6b1..698d679 100644 --- a/extauthz/Makefile +++ b/extauthz/Makefile @@ -10,10 +10,10 @@ test: .PHONY: build build: @mkdir -p ./build - make build-platform BUILD_OS="linux" BUILD_ARCH="amd64" - make build-platform BUILD_OS="linux" BUILD_ARCH="arm64" - make build-platform BUILD_OS="darwin" BUILD_ARCH="amd64" - make build-platform BUILD_OS="darwin" BUILD_ARCH="arm64" + @make build-platform BUILD_OS="linux" BUILD_ARCH="amd64" + @make build-platform BUILD_OS="linux" BUILD_ARCH="arm64" + @make build-platform BUILD_OS="darwin" BUILD_ARCH="amd64" + @make build-platform BUILD_OS="darwin" BUILD_ARCH="arm64" build-platform: @mkdir -p ./build diff --git a/extauthz/cmd/extauthz/main.go b/extauthz/cmd/extauthz/main.go index 558adfe..5de6994 100644 --- a/extauthz/cmd/extauthz/main.go +++ b/extauthz/cmd/extauthz/main.go @@ -41,10 +41,10 @@ func main() { log.Fatalf("failed to initialize OpenFGA client: %v", err) } - extractionSet := make([]extractor.ExtractorSet, 0, len(cfg.ExtractionSet)) + extractionSet := make([]extractor.ExtractorKit, 0, len(cfg.ExtractionSet)) for _, es := range cfg.ExtractionSet { var ( - eSet extractor.ExtractorSet + eSet extractor.ExtractorKit err error ) diff --git a/extauthz/cmd/extauthz/main_test.go b/extauthz/cmd/extauthz/main_test.go index 4e88876..1852e80 100644 --- a/extauthz/cmd/extauthz/main_test.go +++ b/extauthz/cmd/extauthz/main_test.go @@ -17,7 +17,7 @@ import ( "google.golang.org/grpc/test/bufconn" ) -func server(ctx context.Context, e extractor.ExtractorSet) (auth_pb.AuthorizationClient, func()) { +func server(ctx context.Context, e extractor.ExtractorKit) (auth_pb.AuthorizationClient, func()) { buffer := 101024 * 1024 lis := bufconn.Listen(buffer) @@ -28,7 +28,7 @@ func server(ctx context.Context, e extractor.ExtractorSet) (auth_pb.Authorizatio panic(err) } - filter := authz.NewExtAuthZFilter(fgaClient, []extractor.ExtractorSet{e}) + filter := authz.NewExtAuthZFilter(fgaClient, []extractor.ExtractorKit{e}) baseServer := grpc.NewServer() auth_pb.RegisterAuthorizationServer(baseServer, filter) @@ -63,7 +63,7 @@ func TestNoUserExtractedFails(t *testing.T) { expectedErr := errors.New("no user") - e := extractor.ExtractorSet{ + e := extractor.ExtractorKit{ Name: "extauthz", User: func(ctx context.Context, value *auth_pb.CheckRequest) (extractor.Extraction, bool, error) { return extractor.Extraction{}, false, expectedErr diff --git a/extauthz/e2e/config.yaml.tmpl b/extauthz/e2e/config.yaml.tmpl index cbe0937..a9d3e87 100644 --- a/extauthz/e2e/config.yaml.tmpl +++ b/extauthz/e2e/config.yaml.tmpl @@ -8,10 +8,10 @@ extraction_sets: user: type: mock config: - value: subject:user_123 + value: "subject:user_123" object: type: mock config: - value: resource:service_abc + value: "resource:service_abc" relation: - type: method + type: request_method diff --git a/extauthz/e2e/run.sh b/extauthz/e2e/run.sh index 26cbb27..717cbc5 100755 --- a/extauthz/e2e/run.sh +++ b/extauthz/e2e/run.sh @@ -11,8 +11,8 @@ STORE_FILE='e2e/store.fga.yaml' FGA_API_URL='http://localhost:18080' TARGET_URL='http://localhost:8080' -which yq || (echo "yq is not installed. Please install it using make e2e-tools." && exit 1) -which fga || (echo "fga is not installed. Please install it make e2e-tools." && exit 1) +which yq > /dev/null || (echo "yq is not installed. Please install it using make e2e-tools." && exit 1) +which fga > /dev/null || (echo "fga is not installed. Please install it make e2e-tools." && exit 1) TMPDIR=$(mktemp -d) MODEL=$TMPDIR/model.fga diff --git a/extauthz/e2e/store.fga.yaml b/extauthz/e2e/store.fga.yaml index 6bca0ff..a4621a9 100644 --- a/extauthz/e2e/store.fga.yaml +++ b/extauthz/e2e/store.fga.yaml @@ -6,22 +6,22 @@ model: | type resource relations - define can_call: [ subject with allowed_methods ] + define access: [ subject with allowed_methods ] - condition allowed_methods(allowed: list, method: string) { - allowed.exists_one(r, r == method) || allowed.exists_one(r, r == "*") + condition allowed_methods(allowed: list, request_method: string) { + allowed.exists_one(r, r == request_method) || allowed.exists_one(r, r == "*") } tuples: - user: subject:user_123 - relation: can_call + relation: access object: resource:service_abc condition: name: allowed_methods context: allowed: ["GET"] - user: subject:user_456 - relation: can_call + relation: access object: resource:service_xyz condition: name: allowed_methods @@ -33,27 +33,27 @@ tests: check: - user: subject:user_123 assertions: - can_call: true + access: true object: resource:service_abc context: - method: "GET" + request_method: "GET" - user: subject:user_123 assertions: - can_call: false + access: false object: resource:service_abc context: - method: "POST" + request_method: "POST" - name: user_456 can do only GET to service_xyz check: - user: subject:user_456 assertions: - can_call: true + access: true object: resource:service_xyz context: - method: "GET" + request_method: "GET" - user: subject:user_456 assertions: - can_call: true + access: true object: resource:service_xyz context: - method: "POST" + request_method: "POST" diff --git a/extauthz/internal/extractor/extractor.go b/extauthz/internal/extractor/extractor.go index 22c4e36..67e135e 100644 --- a/extauthz/internal/extractor/extractor.go +++ b/extauthz/internal/extractor/extractor.go @@ -3,32 +3,120 @@ package extractor import ( "context" "errors" + "fmt" authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" ) +type Check struct { + User string + Relation string + Object string + Context map[string]interface{} +} + +func (c Check) Validate() error { + if c.User == "" { + return errors.New("user is required") + } + + if c.Object == "" { + return errors.New("object is required") + } + + if c.Relation == "" { + return errors.New("relation is required") + } + + return nil +} + type Extraction struct { Value string - Context map[string]any + Context map[string]interface{} +} + +func (e *Extraction) applyExtraction(v *string, context map[string]interface{}) error { + *v = e.Value + for k, v := range e.Context { + if _, ok := context[k]; ok { + return fmt.Errorf("context key %s already exists", k) + } + context[k] = v + } + return nil } // Extractor is the interface for extracting values from a CheckRequest. type Extractor func(ctx context.Context, value *authv3.CheckRequest) (Extraction, bool, error) -type ExtractorSet struct { +type ExtractorKit struct { Name string - Object Extractor User Extractor + Object Extractor Relation Extractor } +var ErrValueNotFound = errors.New("extraction value not found") + +func (ek ExtractorKit) Extract(ctx context.Context, req *authv3.CheckRequest) (*Check, error) { + check := &Check{ + Context: make(map[string]interface{}), + } + + eUser, found, err := ek.User(ctx, req) + if err != nil { + return nil, fmt.Errorf("getting user extraction: %w", err) + } + + if !found { + return nil, fmt.Errorf("getting user extraction: %w", ErrValueNotFound) + } + + if err := eUser.applyExtraction(&check.User, check.Context); err != nil { + return nil, fmt.Errorf("extracting user: %w", err) + } + + eObject, found, err := ek.Object(ctx, req) + if err != nil { + return nil, fmt.Errorf("getting object extraction: %w", err) + } + + if !found { + return nil, fmt.Errorf("getting object extraction: %w", ErrValueNotFound) + } + + if err := eObject.applyExtraction(&check.Object, check.Context); err != nil { + return nil, fmt.Errorf("extracting object: %w", err) + } + + eRelation, found, err := ek.Relation(ctx, req) + if err != nil { + return nil, fmt.Errorf("getting relation extraction: %w", err) + } + + if !found { + return nil, fmt.Errorf("getting relation extraction: %w", ErrValueNotFound) + } + + if err := eRelation.applyExtraction(&check.Relation, check.Context); err != nil { + return nil, fmt.Errorf("extracting relation: %w", err) + } + + if err := check.Validate(); err != nil { + return nil, fmt.Errorf("validating check: %v", err) + } + + return check, nil +} + type Config interface{} func GetExtractorConfig(name string) (Config, error) { switch name { case "mock": return &MockConfig{}, nil - case "method": + case "request_method": return nil, nil default: return nil, errors.New("extractor not found") @@ -39,8 +127,8 @@ func MakeExtractor(name string, cfg Config) (Extractor, error) { switch name { case "mock": return NewMock(cfg.(*MockConfig)), nil - case "method": - return NewMethod(cfg), nil + case "request_method": + return NewRequestMethod(cfg), nil default: return nil, errors.New("extractor not found") } diff --git a/extauthz/internal/extractor/mock.go b/extauthz/internal/extractor/mock.go index 890311d..42e6143 100644 --- a/extauthz/internal/extractor/mock.go +++ b/extauthz/internal/extractor/mock.go @@ -7,13 +7,16 @@ import ( ) type MockConfig struct { - Val string `yaml:"value"` + Value string `yaml:"value"` Context map[string]interface{} `yaml:"context"` Err error `yaml:"error"` } func NewMock(cfg *MockConfig) Extractor { return func(ctx context.Context, value *authv3.CheckRequest) (Extraction, bool, error) { - return Extraction{Value: cfg.Val, Context: cfg.Context}, cfg.Val != "", cfg.Err + return Extraction{ + Value: cfg.Value, + Context: cfg.Context, + }, cfg.Value != "", cfg.Err } } diff --git a/extauthz/internal/extractor/mock_test.go b/extauthz/internal/extractor/mock_test.go new file mode 100644 index 0000000..d5f8d9a --- /dev/null +++ b/extauthz/internal/extractor/mock_test.go @@ -0,0 +1,24 @@ +package extractor + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestMockUnmarshal(t *testing.T) { + t.Run("empty", func(t *testing.T) { + c := &MockConfig{} + err := yaml.Unmarshal(nil, c) + require.NoError(t, err) + require.Empty(t, c.Value) + }) + + t.Run("success", func(t *testing.T) { + c := &MockConfig{} + err := yaml.Unmarshal([]byte(`value: "subject:user_123"`), c) + require.NoError(t, err) + require.Equal(t, "subject:user_123", c.Value) + }) +} diff --git a/extauthz/internal/extractor/method.go b/extauthz/internal/extractor/request_method.go similarity index 67% rename from extauthz/internal/extractor/method.go rename to extauthz/internal/extractor/request_method.go index b1bd8ca..cedc252 100644 --- a/extauthz/internal/extractor/method.go +++ b/extauthz/internal/extractor/request_method.go @@ -6,12 +6,12 @@ import ( authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" ) -func NewMethod(cfg any) Extractor { +func NewRequestMethod(any) Extractor { return func(ctx context.Context, value *authv3.CheckRequest) (Extraction, bool, error) { return Extraction{ - Value: "can_call", + Value: "access", Context: map[string]interface{}{ - "method": value.GetAttributes().GetRequest().GetHttp().GetMethod(), + "request_method": value.GetAttributes().GetRequest().GetHttp().GetMethod(), }, }, true, nil } diff --git a/extauthz/internal/extractor/spiffe.go b/extauthz/internal/extractor/spiffe.go new file mode 100644 index 0000000..dc10c9e --- /dev/null +++ b/extauthz/internal/extractor/spiffe.go @@ -0,0 +1,96 @@ +package extractor + +import ( + "context" + "errors" + "strings" + + authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "gopkg.in/yaml.v3" +) + +const ( + clientCertHeader = "x-forwarded-client-cert" + spiffeKey = "By=" + spiffeCurrentClientKey = "URI=" +) + +type spiffeExtractionType int8 + +func (t spiffeExtractionType) String() string { + switch t { + case spiffeTypeUser: + return "user" + case spiffeTypeObject: + return "object" + } + + return "unknown" +} + +func (t spiffeExtractionType) MarshalYAML() (interface{}, error) { + return t.String(), nil +} + +func (t *spiffeExtractionType) UnmarshalYAML(value *yaml.Node) error { + switch value.Value { + case "user": + *t = spiffeTypeUser + case "object": + *t = spiffeTypeObject + default: + return errors.New("unknown spiffe extraction type") + } + + return nil +} + +const ( + spiffeTypeUser spiffeExtractionType = iota + spiffeTypeObject +) + +type SpiffeConfig struct { + Type spiffeExtractionType `yaml:"type"` +} + +func NewSpiffe(config *SpiffeConfig) Extractor { + if config == nil { + config = &SpiffeConfig{} + } + + var prefix string + + if config.Type == spiffeTypeUser { + prefix = spiffeCurrentClientKey + } else { + prefix = spiffeKey + } + + return func(ctx context.Context, value *authv3.CheckRequest) (Extraction, bool, error) { + headers := value.GetAttributes().GetRequest().GetHttp().GetHeaders() + val, ok := headers[clientCertHeader] + if !ok { + return Extraction{}, false, nil + } + + var segments = strings.Split(val, ",") + + for _, seg := range segments { + parts := strings.Split(seg, ";") + for _, part := range parts { + if !strings.HasPrefix(part, prefix) { + continue + } + + if part[len(prefix):] == "" { + continue + } + + return Extraction{Value: part[len(prefix):]}, true, nil + } + } + + return Extraction{}, false, nil + } +} diff --git a/extauthz/internal/extractor/spiffe_test.go b/extauthz/internal/extractor/spiffe_test.go new file mode 100644 index 0000000..c29f5a0 --- /dev/null +++ b/extauthz/internal/extractor/spiffe_test.go @@ -0,0 +1,78 @@ +package extractor + +import ( + "context" + "testing" + + authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestSpiffeUnmarshal(t *testing.T) { + c := &SpiffeConfig{} + yaml.Unmarshal([]byte("type: user"), c) + require.Equal(t, spiffeTypeUser, c.Type) +} + +func TestSpiffeExtractor(t *testing.T) { + t.Run("empty", func(t *testing.T) { + extractor := NewSpiffe(nil) + + extraction, found, err := extractor(context.Background(), &authv3.CheckRequest{ + Attributes: &authv3.AttributeContext{ + Request: &authv3.AttributeContext_Request{ + Http: &authv3.AttributeContext_HttpRequest{}, + }, + }, + }) + + require.NoError(t, err) + require.False(t, found) + require.Empty(t, extraction.Value) + }) + + t.Run("success for subject", func(t *testing.T) { + extractor := NewSpiffe(&SpiffeConfig{ + Type: spiffeTypeUser, + }) + + extraction, found, err := extractor(context.Background(), &authv3.CheckRequest{ + Attributes: &authv3.AttributeContext{ + Request: &authv3.AttributeContext_Request{ + Http: &authv3.AttributeContext_HttpRequest{ + Headers: map[string]string{ + "x-forwarded-client-cert": "Hash=519dbf0d617cd943359dcf71f4d26d35e95347b616e62dc9c5ce4f3a7492ec76;Cert=\"-----BEGIN%20CERTIFICATE-----%0AMIIC3DCCAcQCAQEwDQYJKoZIhvcNAQEFBQAwLTEVMBMGA1UECgwMZXhhbXBsZSBJ%0AbmMuMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0yMTA2MDcxNDUwMDBaFw0yMjA2%0AMDcxNDUwMDBaMDsxGzAZBgNVBAMMEmNsaWVudC5leGFtcGxlLmNvbTEcMBoGA1UE%0ACgwTY2xpZW50IG9yZ2FuaXphdGlvbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC%0AAQoCggEBAMe%2FVGy3syX%2BkMpqe6MfStPuFlwwEzwqz3lAaMm9YqASOk9uv0Qc%2FZPm%0AwIMrV1dnnLtLo6nZaRfsMgz1XiYBp%2BR2O87dFw2WN5AjD98zUT3XqUzGHF63cZvH%0AmoVkqrHiwh35HCFiwh9KIS3CtIdYe1n1%2FSkJpj0tVszIY%2Bi288Hu5K5fVYyYzyk%2F%0ANYQKG7A1KEgZ39SkRCHIyK%2FWeGhNT1VCfn%2Bx3D62RUVDjK6Au9yu5pJsKAyB76eg%0AZkXdvBv92dIN2iPhe9DzJ99MfpkjG9JfZG43svc2I2BAIj1eLzh5ZUnpILIHP42S%0AQUd6IBX9X%2BvFH%2FsFVnoySKewcMRK%2BkcCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA%0AswYNs6M4UT8epcW3MOjA%2B5c8EWfMI7SjNuShpRUNaJAhzdvpfj3PFIW%2BROLNs0Tj%0AwGVwkkZW8daxBQw8yC9kEE%2Broj7eJmV9SE%2BozZwa6L4hf18pcNaJlKIyvQUS3mgB%0ApGYO9YvC%2Bsg%2B0gfbSWfbzL17jRS1UI%2BOiW%2BWS5o85SOpusSHDtrG4qcISm7jpgyb%0AudzCZQHOkknO4e%2BrWiGKLpGBE1LkS5Cl%2FJkU1qJWspa4JaFtQxNCdT2Tmo6XDRZ7%0AKfoZiH6c1lI7C07duz9iPkNATc2w%2BNP7bzQgp4BlC0zQ3MwEbcR5uVxvC3vTRsIa%0AznXIRj23jj3NmidA4DTASQ%3D%3D%0A-----END%20CERTIFICATE-----%0A\";Subject=\"O=client organization,CN=client.example.com\";URI=,By=spiffe://cluster.local/ns/default/sa/httpbin;Hash=58531cf54811dc1fd60ee4aaea52866daecb353cb23d2fa237c580cbc217b4be;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account", + }, + }, + }, + }, + }) + + require.NoError(t, err) + require.True(t, found) + require.Equal(t, "spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account", extraction.Value) + }) + + t.Run("success for object", func(t *testing.T) { + extractor := NewSpiffe(&SpiffeConfig{ + Type: spiffeTypeObject, + }) + + extraction, found, err := extractor(context.Background(), &authv3.CheckRequest{ + Attributes: &authv3.AttributeContext{ + Request: &authv3.AttributeContext_Request{ + Http: &authv3.AttributeContext_HttpRequest{ + Headers: map[string]string{ + "x-forwarded-client-cert": "Hash=519dbf0d617cd943359dcf71f4d26d35e95347b616e62dc9c5ce4f3a7492ec76;Cert=\"-----BEGIN%20CERTIFICATE-----%0AMIIC3DCCAcQCAQEwDQYJKoZIhvcNAQEFBQAwLTEVMBMGA1UECgwMZXhhbXBsZSBJ%0AbmMuMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0yMTA2MDcxNDUwMDBaFw0yMjA2%0AMDcxNDUwMDBaMDsxGzAZBgNVBAMMEmNsaWVudC5leGFtcGxlLmNvbTEcMBoGA1UE%0ACgwTY2xpZW50IG9yZ2FuaXphdGlvbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC%0AAQoCggEBAMe%2FVGy3syX%2BkMpqe6MfStPuFlwwEzwqz3lAaMm9YqASOk9uv0Qc%2FZPm%0AwIMrV1dnnLtLo6nZaRfsMgz1XiYBp%2BR2O87dFw2WN5AjD98zUT3XqUzGHF63cZvH%0AmoVkqrHiwh35HCFiwh9KIS3CtIdYe1n1%2FSkJpj0tVszIY%2Bi288Hu5K5fVYyYzyk%2F%0ANYQKG7A1KEgZ39SkRCHIyK%2FWeGhNT1VCfn%2Bx3D62RUVDjK6Au9yu5pJsKAyB76eg%0AZkXdvBv92dIN2iPhe9DzJ99MfpkjG9JfZG43svc2I2BAIj1eLzh5ZUnpILIHP42S%0AQUd6IBX9X%2BvFH%2FsFVnoySKewcMRK%2BkcCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA%0AswYNs6M4UT8epcW3MOjA%2B5c8EWfMI7SjNuShpRUNaJAhzdvpfj3PFIW%2BROLNs0Tj%0AwGVwkkZW8daxBQw8yC9kEE%2Broj7eJmV9SE%2BozZwa6L4hf18pcNaJlKIyvQUS3mgB%0ApGYO9YvC%2Bsg%2B0gfbSWfbzL17jRS1UI%2BOiW%2BWS5o85SOpusSHDtrG4qcISm7jpgyb%0AudzCZQHOkknO4e%2BrWiGKLpGBE1LkS5Cl%2FJkU1qJWspa4JaFtQxNCdT2Tmo6XDRZ7%0AKfoZiH6c1lI7C07duz9iPkNATc2w%2BNP7bzQgp4BlC0zQ3MwEbcR5uVxvC3vTRsIa%0AznXIRj23jj3NmidA4DTASQ%3D%3D%0A-----END%20CERTIFICATE-----%0A\";Subject=\"O=client organization,CN=client.example.com\";URI=,By=spiffe://cluster.local/ns/default/sa/httpbin;Hash=58531cf54811dc1fd60ee4aaea52866daecb353cb23d2fa237c580cbc217b4be;Subject=\"\";URI=spiffe://cluster.local/ns/istio-system/sa/istio-ingressgateway-service-account", + }, + }, + }, + }, + }) + + require.NoError(t, err) + require.True(t, found) + require.Equal(t, "spiffe://cluster.local/ns/default/sa/httpbin", extraction.Value) + }) +} diff --git a/extauthz/internal/server/authz/authz.go b/extauthz/internal/server/authz/authz.go index ea62f52..b9c9edc 100644 --- a/extauthz/internal/server/authz/authz.go +++ b/extauthz/internal/server/authz/authz.go @@ -2,6 +2,7 @@ package authz import ( "context" + "errors" "fmt" "log" @@ -36,15 +37,15 @@ var ( // ExtAuthZFilter is an implementation of the Envoy AuthZ filter. type ExtAuthZFilter struct { client *client.OpenFgaClient - extractionSet []extractor.ExtractorSet + extractionKit []extractor.ExtractorKit modelID string } var _ envoy.AuthorizationServer = (*ExtAuthZFilter)(nil) // NewExtAuthZFilter creates a new ExtAuthZFilter -func NewExtAuthZFilter(c *client.OpenFgaClient, es []extractor.ExtractorSet) *ExtAuthZFilter { - return &ExtAuthZFilter{client: c, extractionSet: es} +func NewExtAuthZFilter(c *client.OpenFgaClient, es []extractor.ExtractorKit) *ExtAuthZFilter { + return &ExtAuthZFilter{client: c, extractionKit: es} } func (e ExtAuthZFilter) Register(server *grpc.Server) { @@ -54,108 +55,46 @@ func (e ExtAuthZFilter) Register(server *grpc.Server) { func (e ExtAuthZFilter) Check(ctx context.Context, req *envoy.CheckRequest) (response *envoy.CheckResponse, err error) { res, err := e.check(ctx, req) if err != nil { - log.Println(err) return nil, err } - log.Println(res) return res, nil } -type extracted struct { - user extractor.Extraction - object extractor.Extraction - relation extractor.Extraction -} - -func (e ExtAuthZFilter) extract(ctx context.Context, req *envoy.CheckRequest) (*extracted, error) { - var user, object, relation extractor.Extraction - for _, es := range e.extractionSet { - var ( - found bool - err error - ) - user, found, err = es.User(ctx, req) - if err != nil { - return nil, err - } - - if !found { - continue - } - - object, found, err = es.Object(ctx, req) - if err != nil { - return nil, err - } - if !found { - continue +func (e ExtAuthZFilter) extract(ctx context.Context, req *envoy.CheckRequest) (*extractor.Check, error) { + for _, es := range e.extractionKit { + check, err := es.Extract(ctx, req) + if err == nil { + return check, nil } - relation, found, err = es.Relation(ctx, req) - if err != nil { - return nil, err - } - if !found { + if errors.Is(err, extractor.ErrValueNotFound) { continue } - return &extracted{ - user: user, - object: object, - relation: relation, - }, nil + return nil, err } return nil, nil } -func mergeMaps(map1, map2 map[string]any) map[string]any { - UniqueMap := make(map[string]any) - - // for loop for the first map - for key, val := range map1 { - UniqueMap[key] = val - } - - // for loop for the second map - for key, val := range map2 { - UniqueMap[key] = val - } - // return merged result - return UniqueMap -} - // Check implements the Check method of the Authorization interface. func (e ExtAuthZFilter) check(ctx context.Context, req *envoy.CheckRequest) (response *envoy.CheckResponse, err error) { - extracted, err := e.extract(ctx, req) + check, err := e.extract(ctx, req) if err != nil { + fmt.Printf("extracting from request: %v", err) return nil, err } - if extracted == nil { + if check == nil { return deny(codes.InvalidArgument, "No extraction set found"), nil } - context := map[string]any{} - - if extracted.user.Context != nil { - context = mergeMaps(context, extracted.user.Context) - } - - if extracted.object.Context != nil { - context = mergeMaps(context, extracted.object.Context) - } - - if extracted.relation.Context != nil { - context = mergeMaps(context, extracted.relation.Context) - } - body := client.ClientCheckRequest{ - User: extracted.user.Value, - Relation: extracted.relation.Value, - Object: extracted.object.Value, - Context: &context, + User: check.User, + Relation: check.Relation, + Object: check.Object, + Context: &check.Context, } options := client.ClientCheckOptions{ @@ -164,6 +103,7 @@ func (e ExtAuthZFilter) check(ctx context.Context, req *envoy.CheckRequest) (res data, err := e.client.Check(ctx).Body(body).Options(options).Execute() if err != nil { + log.Printf("%v for %v\n", err, body) return deny(codes.Internal, fmt.Sprintf("Error checking permissions: %v", err)), nil } @@ -171,5 +111,6 @@ func (e ExtAuthZFilter) check(ctx context.Context, req *envoy.CheckRequest) (res return allow, nil } + log.Printf("unauthorized request for %v\n", body) return deny(codes.PermissionDenied, fmt.Sprintf("Access denied: %s", data.GetResolution())), nil } diff --git a/extauthz/internal/server/config/config_test.go b/extauthz/internal/server/config/config_test.go index 1400a34..792a7a6 100644 --- a/extauthz/internal/server/config/config_test.go +++ b/extauthz/internal/server/config/config_test.go @@ -16,9 +16,9 @@ func TestConfig(t *testing.T) { require.Len(t, cfg.ExtractionSet, 1) require.Equal(t, "test", cfg.ExtractionSet[0].Name) require.Equal(t, "mock", cfg.ExtractionSet[0].User.Type) - require.Equal(t, "my_user", cfg.ExtractionSet[0].User.Config.(*extractor.MockConfig).Val) + require.Equal(t, "subject:my_user", cfg.ExtractionSet[0].User.Config.(*extractor.MockConfig).Value) require.Equal(t, "mock", cfg.ExtractionSet[0].Object.Type) - require.Equal(t, "my_object", cfg.ExtractionSet[0].Object.Config.(*extractor.MockConfig).Val) + require.Equal(t, "resource:my_object", cfg.ExtractionSet[0].Object.Config.(*extractor.MockConfig).Value) require.Equal(t, "mock", cfg.ExtractionSet[0].Relation.Type) - require.Equal(t, "my_relation", cfg.ExtractionSet[0].Relation.Config.(*extractor.MockConfig).Val) + require.Equal(t, "my_relation", cfg.ExtractionSet[0].Relation.Config.(*extractor.MockConfig).Value) } diff --git a/extauthz/internal/server/config/testdata/config.yaml b/extauthz/internal/server/config/testdata/config.yaml index b77fb93..9e68fb4 100644 --- a/extauthz/internal/server/config/testdata/config.yaml +++ b/extauthz/internal/server/config/testdata/config.yaml @@ -8,11 +8,11 @@ extraction_sets: user: type: mock config: - value: my_user + value: subject:my_user object: type: mock config: - value: my_object + value: resource:my_object relation: type: mock config: