Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds spiffe extractor #16

Merged
merged 5 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions extauthz/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions extauthz/cmd/extauthz/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
6 changes: 3 additions & 3 deletions extauthz/cmd/extauthz/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions extauthz/e2e/config.yaml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions extauthz/e2e/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 13 additions & 13 deletions extauthz/e2e/store.fga.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>, method: string) {
allowed.exists_one(r, r == method) || allowed.exists_one(r, r == "*")
condition allowed_methods(allowed: list<string>, 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
Expand All @@ -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"
100 changes: 94 additions & 6 deletions extauthz/internal/extractor/extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
}
Expand Down
7 changes: 5 additions & 2 deletions extauthz/internal/extractor/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
24 changes: 24 additions & 0 deletions extauthz/internal/extractor/mock_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading
Loading