Skip to content
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: 8 additions & 0 deletions internal/server/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ package authz
import "context"

type Verifier interface {
// IsAllowed returns whether the user is allowed to access the resource
IsAllowed(ctx context.Context, input map[string]any) (bool, error)
// Namespaces returns the list of namespaces the user has access to
Namespaces(ctx context.Context, input map[string]any) ([]string, error)
// Shutdown is called when the server is shutting down
Shutdown(ctx context.Context) error
}

type contextKey string

const NamespacesKey contextKey = "namespaces"
21 changes: 20 additions & 1 deletion internal/server/authz/engine/bundle/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"context"
"fmt"
"os"
"strings"

Expand Down Expand Up @@ -75,7 +76,6 @@
Path: "flipt/authz/v1/allow",
Input: input,
})

if err != nil {
return false, err
}
Expand All @@ -84,6 +84,25 @@
return allow, nil
}

func (e *Engine) Namespaces(ctx context.Context, input map[string]interface{}) ([]string, error) {
dec, err := e.opa.Decision(ctx, sdk.DecisionOptions{
Path: "flipt/authz/v1/viewable_namespaces",
Input: input,
})
if err != nil {
return nil, err
}
values, ok := dec.Result.([]any)
if !ok {
return nil, fmt.Errorf("unexpected result type: %T", values)
}

Check warning on line 98 in internal/server/authz/engine/bundle/engine.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/engine/bundle/engine.go#L97-L98

Added lines #L97 - L98 were not covered by tests
namespaces := make([]string, len(values))
for i, ns := range values {
namespaces[i] = fmt.Sprintf("%s", ns)
}
return namespaces, nil
}

func (e *Engine) Shutdown(ctx context.Context) error {
e.opa.Stop(ctx)
for _, cleanup := range e.cleanupFuncs {
Expand Down
75 changes: 74 additions & 1 deletion internal/server/authz/engine/bundle/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func TestEngine_IsAllowed(t *testing.T) {
logger: zaptest.NewLogger(t),
}

var tests = []struct {
tests := []struct {
name string
input string
expected bool
Expand Down Expand Up @@ -246,5 +246,78 @@ func TestEngine_IsAllowed(t *testing.T) {
})
}

t.Run("viewable namespaces without definition", func(t *testing.T) {
namespaces, err := engine.Namespaces(ctx, map[string]any{})
require.Error(t, err)
require.Nil(t, namespaces)
})

assert.NoError(t, engine.Shutdown(ctx))
}

func TestViewableNamespaces(t *testing.T) {
ctx := context.Background()

policy, err := os.ReadFile("../testdata/viewable_namespaces.rego")
require.NoError(t, err)

data, err := os.ReadFile("../testdata/viewable_namespaces.json")
require.NoError(t, err)

var (
server = sdktest.MustNewServer(
sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{
"main.rego": string(policy),
"data.json": string(data),
}),
)
config = fmt.Sprintf(`{
"services": {
"test": {
"url": %q
}
},
"bundles": {
"test": {
"resource": "/bundles/bundle.tar.gz"
}
},
}`, server.URL())
)

t.Cleanup(server.Stop)

opa, err := sdk.New(ctx, sdk.Options{
Config: strings.NewReader(config),
Store: inmem.New(),
Logger: ozap.Wrap(zaptest.NewLogger(t), &zap.AtomicLevel{}),
})

require.NoError(t, err)
assert.NotNil(t, opa)

engine := &Engine{
opa: opa,
logger: zaptest.NewLogger(t),
}
t.Cleanup(func() {
assert.NoError(t, engine.Shutdown(ctx))
})

tt := []struct {
name string
roles []string
namespaces []string
}{
{"empty", []string{}, []string{}},
{"devs", []string{"devs"}, []string{"local", "staging"}},
{"devsops", []string{"devs", "ops"}, []string{"local", "production", "staging"}},
}
for _, tt := range tt {
t.Run(tt.name, func(t *testing.T) {
namespaces, err := engine.Namespaces(ctx, map[string]any{"roles": tt.roles})
require.NoError(t, err)
require.Equal(t, tt.namespaces, namespaces)
})
}
}
53 changes: 44 additions & 9 deletions internal/server/authz/engine/rego/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,10 @@
type Engine struct {
logger *zap.Logger

mu sync.RWMutex
query rego.PreparedEvalQuery
store storage.Store
mu sync.RWMutex
queryAllow rego.PreparedEvalQuery
queryNamespaces rego.PreparedEvalQuery
store storage.Store

policySource PolicySource
policyHash source.Hash
Expand Down Expand Up @@ -144,7 +145,7 @@
defer e.mu.RUnlock()

e.logger.Debug("evaluating policy", zap.Any("input", input))
results, err := e.query.Eval(ctx, rego.EvalInput(input))
results, err := e.queryAllow.Eval(ctx, rego.EvalInput(input))
if err != nil {
return false, err
}
Expand All @@ -160,6 +161,25 @@
return nil
}

func (e *Engine) Namespaces(ctx context.Context, input map[string]any) ([]string, error) {
results, err := e.queryNamespaces.Eval(ctx, rego.EvalInput(input))
if err != nil {
return nil, err
}

Check warning on line 168 in internal/server/authz/engine/rego/engine.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/engine/rego/engine.go#L167-L168

Added lines #L167 - L168 were not covered by tests
if len(results) == 0 {
return nil, errors.New("no results found")
}
values, ok := results[0].Expressions[0].Value.([]any)
if !ok {
return nil, fmt.Errorf("unexpected result type: %T", results[0].Expressions[0].Value)
}

Check warning on line 175 in internal/server/authz/engine/rego/engine.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/engine/rego/engine.go#L174-L175

Added lines #L174 - L175 were not covered by tests
namespaces := make([]string, len(values))
for i, ns := range values {
namespaces[i] = fmt.Sprintf("%s", ns)
}
return namespaces, nil
}

func poll(ctx context.Context, d time.Duration, fn func()) {
ticker := time.NewTicker(d)
for {
Expand All @@ -186,15 +206,29 @@
return fmt.Errorf("getting policy definition: %w", err)
}

m := rego.Module("policy.rego", string(policy))
s := rego.Store(e.store)

r := rego.New(
rego.Query("data.flipt.authz.v1.allow"),
rego.Module("policy.rego", string(policy)),
rego.Store(e.store),
m,
s,
)

queryAllow, err := r.PrepareForEval(ctx)
if err != nil {
return fmt.Errorf("preparing policy allow: %w", err)
}

Check warning on line 221 in internal/server/authz/engine/rego/engine.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/engine/rego/engine.go#L220-L221

Added lines #L220 - L221 were not covered by tests

r = rego.New(
rego.Query("data.flipt.authz.v1.viewable_namespaces"),
m,
s,
)

query, err := r.PrepareForEval(ctx)
queryNamespaces, err := r.PrepareForEval(ctx)
if err != nil {
return fmt.Errorf("preparing policy: %w", err)
return fmt.Errorf("preparing policy namespaces: %w", err)

Check warning on line 231 in internal/server/authz/engine/rego/engine.go

View check run for this annotation

Codecov / codecov/patch

internal/server/authz/engine/rego/engine.go#L231

Added line #L231 was not covered by tests
}

e.mu.Lock()
Expand All @@ -204,7 +238,8 @@
return nil
}
e.policyHash = hash
e.query = query
e.queryAllow = queryAllow
e.queryNamespaces = queryNamespaces

return nil
}
Expand Down
69 changes: 56 additions & 13 deletions internal/server/authz/engine/rego/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.flipt.io/flipt/internal/server/authz/engine/rego/source"
authrpc "go.flipt.io/flipt/rpc/flipt/auth"
Expand All @@ -29,7 +30,18 @@ func TestEngine_NewEngine(t *testing.T) {
}

func TestEngine_IsAllowed(t *testing.T) {
var tests = []struct {
policy, err := os.ReadFile("../testdata/rbac.rego")
require.NoError(t, err)

data, err := os.ReadFile("../testdata/rbac.json")
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
engine, err := newEngine(ctx, zaptest.NewLogger(t), withPolicySource(policySource(string(policy))), withDataSource(dataSource(string(data)), 5*time.Second))
require.NoError(t, err)

tests := []struct {
name string
input string
expected bool
Expand Down Expand Up @@ -200,17 +212,6 @@ func TestEngine_IsAllowed(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
policy, err := os.ReadFile("../testdata/rbac.rego")
require.NoError(t, err)

data, err := os.ReadFile("../testdata/rbac.json")
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
engine, err := newEngine(ctx, zaptest.NewLogger(t), withPolicySource(policySource(string(policy))), withDataSource(dataSource(string(data)), 5*time.Second))
require.NoError(t, err)

var input map[string]interface{}

err = json.Unmarshal([]byte(tt.input), &input)
Expand All @@ -221,10 +222,18 @@ func TestEngine_IsAllowed(t *testing.T) {
require.Equal(t, tt.expected, allowed)
})
}

t.Run("viewable namespaces without definition", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
namespaces, err := engine.Namespaces(ctx, map[string]any{})
require.Error(t, err)
require.Nil(t, namespaces)
})
}

func TestEngine_IsAuthMethod(t *testing.T) {
var tests = []struct {
tests := []struct {
name string
input authrpc.Method
expected bool
Expand Down Expand Up @@ -269,6 +278,40 @@ func TestEngine_IsAuthMethod(t *testing.T) {
}
}

func TestViewableNamespaces(t *testing.T) {
policy, err := os.ReadFile("../testdata/viewable_namespaces.rego")
require.NoError(t, err)

data, err := os.ReadFile("../testdata/viewable_namespaces.json")
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
engine, err := newEngine(ctx, zaptest.NewLogger(t), withPolicySource(policySource(string(policy))), withDataSource(dataSource(string(data)), 5*time.Second))
require.NoError(t, err)

t.Cleanup(func() {
assert.NoError(t, engine.Shutdown(ctx))
})

tt := []struct {
name string
roles []string
namespaces []string
}{
{"empty", []string{}, []string{}},
{"devs", []string{"devs"}, []string{"local", "staging"}},
{"devsops", []string{"devs", "ops"}, []string{"local", "production", "staging"}},
}
for _, tt := range tt {
t.Run(tt.name, func(t *testing.T) {
namespaces, err := engine.Namespaces(ctx, map[string]any{"roles": tt.roles})
require.NoError(t, err)
require.Equal(t, tt.namespaces, namespaces)
})
}
}

type policySource string

func (p policySource) Get(context.Context, source.Hash) ([]byte, source.Hash, error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"roles_to_namespaces": {
"devs": ["local", "staging"],
"ops": ["staging", "production"]
}
}
16 changes: 16 additions & 0 deletions internal/server/authz/engine/testdata/viewable_namespaces.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

package flipt.authz.v1

import rego.v1
import data

viewable_namespaces contains namespace if {
some role in input.roles
some namespace in data.roles_to_namespaces[role]
}

default allow := false

allow if {
input.request.namespace in viewable_namespaces
}
Loading
Loading