forked from TrueCloudLab/policy-engine
Compare commits
10 commits
Author | SHA1 | Date | |
---|---|---|---|
63ecf63a08 | |||
8d291039d8 | |||
5eee1a7334 | |||
8dc9d9fa58 | |||
7f6ee39cb8 | |||
|
76372aac04 | ||
|
35f24627f0 | ||
31a308ea61 | |||
88cf807951 | |||
5ebb2e694c |
21 changed files with 2192 additions and 5 deletions
21
.forgejo/workflows/dco.yml
Normal file
21
.forgejo/workflows/dco.yml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
name: DCO action
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dco:
|
||||||
|
name: DCO
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Run commit format checker
|
||||||
|
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v2
|
||||||
|
with:
|
||||||
|
from: 'origin/${{ github.event.pull_request.base.ref }}'
|
73
.forgejo/workflows/tests.yml
Normal file
73
.forgejo/workflows/tests.yml
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
name: Tests and linters
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install linters
|
||||||
|
run: make lint-install
|
||||||
|
|
||||||
|
- name: Run linters
|
||||||
|
run: make lint
|
||||||
|
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go_versions: [ '1.20', '1.21' ]
|
||||||
|
fail-fast: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '${{ matrix.go_versions }}'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: make test
|
||||||
|
|
||||||
|
tests-race:
|
||||||
|
name: Tests with -race
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: go test ./... -count=1 -race
|
||||||
|
|
||||||
|
staticcheck:
|
||||||
|
name: Staticcheck
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Install staticcheck
|
||||||
|
run: make staticcheck-install
|
||||||
|
|
||||||
|
- name: Run staticcheck
|
||||||
|
run: make staticcheck-run
|
|
@ -47,7 +47,7 @@ linters:
|
||||||
- durationcheck
|
- durationcheck
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exportloopref
|
- exportloopref
|
||||||
- gofmt
|
- gofumpt
|
||||||
- goimports
|
- goimports
|
||||||
- misspell
|
- misspell
|
||||||
- whitespace
|
- whitespace
|
||||||
|
|
|
@ -26,14 +26,17 @@ repos:
|
||||||
exclude: ".key$"
|
exclude: ".key$"
|
||||||
|
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||||
rev: v0.9.0.2
|
rev: v0.9.0.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
|
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: local
|
||||||
rev: v1.51.2
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint
|
- id: make-lint
|
||||||
|
name: Run Make Lint
|
||||||
|
entry: make lint
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -43,3 +46,9 @@ repos:
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
types: [go]
|
types: [go]
|
||||||
language: system
|
language: system
|
||||||
|
|
||||||
|
- repo: https://github.com/TekWizely/pre-commit-golang
|
||||||
|
rev: v1.0.0-rc.1
|
||||||
|
hooks:
|
||||||
|
- id: go-staticcheck-repo-mod
|
||||||
|
- id: go-mod-tidy
|
||||||
|
|
62
Makefile
Executable file
62
Makefile
Executable file
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
TRUECLOUDLAB_LINT_VERSION ?= 0.0.2
|
||||||
|
TMP_DIR := .cache
|
||||||
|
OUTPUT_LINT_DIR ?= $(shell pwd)/bin
|
||||||
|
LINT_VERSION ?= 1.55.1
|
||||||
|
LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
|
||||||
|
|
||||||
|
# Run all code formatters
|
||||||
|
fmts: fmt imports
|
||||||
|
|
||||||
|
# Reformat code
|
||||||
|
fmt:
|
||||||
|
@echo "⇒ Processing gofmt check"
|
||||||
|
@gofumpt -s -w .
|
||||||
|
|
||||||
|
# Reformat imports
|
||||||
|
imports:
|
||||||
|
@echo "⇒ Processing goimports check"
|
||||||
|
@goimports -w .
|
||||||
|
|
||||||
|
# Run Unit Test with go test
|
||||||
|
test:
|
||||||
|
@echo "⇒ Running go test"
|
||||||
|
@go test ./... -count=1
|
||||||
|
|
||||||
|
# Activate pre-commit hooks
|
||||||
|
pre-commit:
|
||||||
|
pre-commit install -t pre-commit -t commit-msg
|
||||||
|
|
||||||
|
# Deactivate pre-commit hooks
|
||||||
|
unpre-commit:
|
||||||
|
pre-commit uninstall -t pre-commit -t commit-msg
|
||||||
|
|
||||||
|
pre-commit-run:
|
||||||
|
@pre-commit run -a --hook-stage manual
|
||||||
|
|
||||||
|
# Install linters
|
||||||
|
lint-install:
|
||||||
|
@mkdir -p $(TMP_DIR)
|
||||||
|
@rm -rf $(TMP_DIR)/linters
|
||||||
|
@git -c advice.detachedHead=false clone --branch v$(TRUECLOUDLAB_LINT_VERSION) https://git.frostfs.info/TrueCloudLab/linters.git $(TMP_DIR)/linters
|
||||||
|
@make -C $(TMP_DIR)/linters lib CGO_ENABLED=1 OUT_DIR=$(OUTPUT_LINT_DIR)
|
||||||
|
@rm -rf $(TMP_DIR)/linters
|
||||||
|
@rmdir $(TMP_DIR) 2>/dev/null || true
|
||||||
|
@CGO_ENABLED=1 GOBIN=$(LINT_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@v$(LINT_VERSION)
|
||||||
|
|
||||||
|
# Run linters
|
||||||
|
lint:
|
||||||
|
@if [ ! -d "$(LINT_DIR)" ]; then \
|
||||||
|
echo "Run make lint-install"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@$(LINT_DIR)/golangci-lint run
|
||||||
|
|
||||||
|
# Install staticcheck
|
||||||
|
staticcheck-install:
|
||||||
|
@go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
|
||||||
|
# Run staticcheck
|
||||||
|
staticcheck-run:
|
||||||
|
@staticcheck ./...
|
229
chain.go
Normal file
229
chain.go
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Engine ...
|
||||||
|
type Engine interface {
|
||||||
|
// IsAllowed returns status for the operation after all checks.
|
||||||
|
// The second return value signifies whether a matching rule was found.
|
||||||
|
IsAllowed(name Name, namespace string, r Request) (Status, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainID is the ID of rule chain.
|
||||||
|
type ChainID string
|
||||||
|
|
||||||
|
type Chain struct {
|
||||||
|
ID ChainID
|
||||||
|
|
||||||
|
Rules []Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chain) Bytes() []byte {
|
||||||
|
data, err := json.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chain) DecodeBytes(b []byte) error {
|
||||||
|
return json.Unmarshal(b, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
Status Status
|
||||||
|
// Actions the operation is applied to.
|
||||||
|
Actions Actions
|
||||||
|
// List of the resources the operation is applied to.
|
||||||
|
Resources Resources
|
||||||
|
// True iff individual conditions must be combined with the logical OR.
|
||||||
|
// By default AND is used, so _each_ condition must pass.
|
||||||
|
Any bool
|
||||||
|
Condition []Condition
|
||||||
|
}
|
||||||
|
|
||||||
|
type Actions struct {
|
||||||
|
Inverted bool
|
||||||
|
Names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Resources struct {
|
||||||
|
Inverted bool
|
||||||
|
Names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Condition struct {
|
||||||
|
Op ConditionType
|
||||||
|
Object ObjectType
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ObjectType byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
ObjectResource ObjectType = iota
|
||||||
|
ObjectRequest
|
||||||
|
ObjectActor
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConditionType byte
|
||||||
|
|
||||||
|
// TODO @fyrchik: reduce the number of conditions.
|
||||||
|
// Everything from here should be expressable, but we do not need them all.
|
||||||
|
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
||||||
|
const (
|
||||||
|
// String condition operators.
|
||||||
|
CondStringEquals ConditionType = iota
|
||||||
|
CondStringNotEquals
|
||||||
|
CondStringEqualsIgnoreCase
|
||||||
|
CondStringNotEqualsIgnoreCase
|
||||||
|
CondStringLike
|
||||||
|
CondStringNotLike
|
||||||
|
CondStringLessThan
|
||||||
|
CondStringLessThanEquals
|
||||||
|
CondStringGreaterThan
|
||||||
|
CondStringGreaterThanEquals
|
||||||
|
|
||||||
|
// Numeric condition operators.
|
||||||
|
CondNumericEquals
|
||||||
|
CondNumericNotEquals
|
||||||
|
CondNumericLessThan
|
||||||
|
CondNumericLessThanEquals
|
||||||
|
CondNumericGreaterThan
|
||||||
|
CondNumericGreaterThanEquals
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c ConditionType) String() string {
|
||||||
|
switch c {
|
||||||
|
case CondStringEquals:
|
||||||
|
return "StringEquals"
|
||||||
|
case CondStringNotEquals:
|
||||||
|
return "StringNotEquals"
|
||||||
|
case CondStringEqualsIgnoreCase:
|
||||||
|
return "StringEqualsIgnoreCase"
|
||||||
|
case CondStringNotEqualsIgnoreCase:
|
||||||
|
return "StringNotEqualsIgnoreCase"
|
||||||
|
case CondStringLike:
|
||||||
|
return "StringLike"
|
||||||
|
case CondStringNotLike:
|
||||||
|
return "StringNotLike"
|
||||||
|
case CondStringLessThan:
|
||||||
|
return "StringLessThan"
|
||||||
|
case CondStringLessThanEquals:
|
||||||
|
return "StringLessThanEquals"
|
||||||
|
case CondStringGreaterThan:
|
||||||
|
return "StringGreaterThan"
|
||||||
|
case CondStringGreaterThanEquals:
|
||||||
|
return "StringGreaterThanEquals"
|
||||||
|
case CondNumericEquals:
|
||||||
|
return "NumericEquals"
|
||||||
|
case CondNumericNotEquals:
|
||||||
|
return "NumericNotEquals"
|
||||||
|
case CondNumericLessThan:
|
||||||
|
return "NumericLessThan"
|
||||||
|
case CondNumericLessThanEquals:
|
||||||
|
return "NumericLessThanEquals"
|
||||||
|
case CondNumericGreaterThan:
|
||||||
|
return "NumericGreaterThan"
|
||||||
|
case CondNumericGreaterThanEquals:
|
||||||
|
return "NumericGreaterThanEquals"
|
||||||
|
default:
|
||||||
|
return "unknown condition type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Condition) Match(req Request) bool {
|
||||||
|
var val string
|
||||||
|
switch c.Object {
|
||||||
|
case ObjectResource:
|
||||||
|
val = req.Resource().Property(c.Key)
|
||||||
|
case ObjectRequest:
|
||||||
|
val = req.Property(c.Key)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unknown condition type: %d", c.Object))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.Op {
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unimplemented: %d", c.Op))
|
||||||
|
case CondStringEquals:
|
||||||
|
return val == c.Value
|
||||||
|
case CondStringNotEquals:
|
||||||
|
return val != c.Value
|
||||||
|
case CondStringEqualsIgnoreCase:
|
||||||
|
return strings.EqualFold(val, c.Value)
|
||||||
|
case CondStringNotEqualsIgnoreCase:
|
||||||
|
return !strings.EqualFold(val, c.Value)
|
||||||
|
case CondStringLike:
|
||||||
|
return globMatch(val, c.Value)
|
||||||
|
case CondStringNotLike:
|
||||||
|
return !globMatch(val, c.Value)
|
||||||
|
case CondStringLessThan:
|
||||||
|
return val < c.Value
|
||||||
|
case CondStringLessThanEquals:
|
||||||
|
return val <= c.Value
|
||||||
|
case CondStringGreaterThan:
|
||||||
|
return val > c.Value
|
||||||
|
case CondStringGreaterThanEquals:
|
||||||
|
return val >= c.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rule) Match(req Request) (status Status, matched bool) {
|
||||||
|
found := len(r.Resources.Names) == 0
|
||||||
|
for i := range r.Resources.Names {
|
||||||
|
if globMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return NoRuleFound, false
|
||||||
|
}
|
||||||
|
for i := range r.Actions.Names {
|
||||||
|
if globMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted {
|
||||||
|
return r.matchCondition(req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NoRuleFound, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rule) matchCondition(obj Request) (status Status, matched bool) {
|
||||||
|
if r.Any {
|
||||||
|
return r.matchAny(obj)
|
||||||
|
}
|
||||||
|
return r.matchAll(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rule) matchAny(obj Request) (status Status, matched bool) {
|
||||||
|
for i := range r.Condition {
|
||||||
|
if r.Condition[i].Match(obj) {
|
||||||
|
return r.Status, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NoRuleFound, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rule) matchAll(obj Request) (status Status, matched bool) {
|
||||||
|
for i := range r.Condition {
|
||||||
|
if !r.Condition[i].Match(obj) {
|
||||||
|
return NoRuleFound, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.Status, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Chain) Match(req Request) (status Status, matched bool) {
|
||||||
|
for i := range c.Rules {
|
||||||
|
status, matched := c.Rules[i].Match(req)
|
||||||
|
if matched {
|
||||||
|
return status, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NoRuleFound, false
|
||||||
|
}
|
10
chain_names.go
Normal file
10
chain_names.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
// Name represents the place in the request lifecycle where policy is applied.
|
||||||
|
type Name string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Ingress represents chains applied when crossing user/storage network boundary.
|
||||||
|
// It is not applied when talking between nodes.
|
||||||
|
Ingress Name = "ingress"
|
||||||
|
)
|
33
chain_test.go
Normal file
33
chain_test.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeDecode(t *testing.T) {
|
||||||
|
expected := Chain{
|
||||||
|
Rules: []Rule{
|
||||||
|
{
|
||||||
|
Status: Allow,
|
||||||
|
Actions: Actions{Names: []string{
|
||||||
|
"native::PutObject",
|
||||||
|
}},
|
||||||
|
Resources: Resources{Names: []string{"*"}},
|
||||||
|
Condition: []Condition{
|
||||||
|
{
|
||||||
|
Op: CondStringEquals,
|
||||||
|
Key: "Name",
|
||||||
|
Value: "NNS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data := expected.Bytes()
|
||||||
|
|
||||||
|
var actual Chain
|
||||||
|
require.NoError(t, actual.DecodeBytes(data))
|
||||||
|
require.Equal(t, expected, actual)
|
||||||
|
}
|
35
error.go
Normal file
35
error.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Status is the status for policy application.
|
||||||
|
type Status byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
Allow Status = iota
|
||||||
|
NoRuleFound
|
||||||
|
AccessDenied
|
||||||
|
QuotaLimitReached
|
||||||
|
last
|
||||||
|
)
|
||||||
|
|
||||||
|
// Valid returns true if the status is valid.
|
||||||
|
func (s Status) Valid() bool {
|
||||||
|
return s < last
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the fmt.Stringer interface.
|
||||||
|
func (s Status) String() string {
|
||||||
|
switch s {
|
||||||
|
case Allow:
|
||||||
|
return "Allowed"
|
||||||
|
case NoRuleFound:
|
||||||
|
return "NoRuleFound"
|
||||||
|
case AccessDenied:
|
||||||
|
return "Access denied"
|
||||||
|
case QuotaLimitReached:
|
||||||
|
return "Quota limit reached"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Denied with status: %d", s)
|
||||||
|
}
|
||||||
|
}
|
22
glob.go
Normal file
22
glob.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Matches s against the pattern.
|
||||||
|
// ? in pattern correspond to any symbol.
|
||||||
|
// * in pattern correspond to any sequence of symbols.
|
||||||
|
// Currently only '*' in the suffix is supported.
|
||||||
|
func globMatch(s, pattern string) bool {
|
||||||
|
index := strings.IndexByte(pattern, '*')
|
||||||
|
switch index {
|
||||||
|
default:
|
||||||
|
panic("unimplemented")
|
||||||
|
case -1:
|
||||||
|
return pattern == s
|
||||||
|
case utf8.RuneCountInString(pattern) - 1:
|
||||||
|
return strings.HasPrefix(s, pattern[:len(pattern)-1])
|
||||||
|
}
|
||||||
|
}
|
11
go.mod
Normal file
11
go.mod
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module git.frostfs.info/TrueCloudLab/policy-engine
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.8.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
17
go.sum
Normal file
17
go.sum
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
236
iam/converter.go
Normal file
236
iam/converter.go
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
package iam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
RequestOwnerProperty = "Owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// String condition operators.
|
||||||
|
CondStringEquals string = "StringEquals"
|
||||||
|
CondStringNotEquals string = "StringNotEquals"
|
||||||
|
CondStringEqualsIgnoreCase string = "StringEqualsIgnoreCase"
|
||||||
|
CondStringNotEqualsIgnoreCase string = "StringNotEqualsIgnoreCase"
|
||||||
|
CondStringLike string = "StringLike"
|
||||||
|
CondStringNotLike string = "StringNotLike"
|
||||||
|
|
||||||
|
// Numeric condition operators.
|
||||||
|
CondNumericEquals string = "NumericEquals"
|
||||||
|
CondNumericNotEquals string = "NumericNotEquals"
|
||||||
|
CondNumericLessThan string = "NumericLessThan"
|
||||||
|
CondNumericLessThanEquals string = "NumericLessThanEquals"
|
||||||
|
CondNumericGreaterThan string = "NumericGreaterThan"
|
||||||
|
CondNumericGreaterThanEquals string = "NumericGreaterThanEquals"
|
||||||
|
|
||||||
|
// Date condition operators.
|
||||||
|
CondDateEquals string = "DateEquals"
|
||||||
|
CondDateNotEquals string = "DateNotEquals"
|
||||||
|
CondDateLessThan string = "DateLessThan"
|
||||||
|
CondDateLessThanEquals string = "DateLessThanEquals"
|
||||||
|
CondDateGreaterThan string = "DateGreaterThan"
|
||||||
|
CondDateGreaterThanEquals string = "DateGreaterThanEquals"
|
||||||
|
|
||||||
|
// Bolean condition operators.
|
||||||
|
CondBool string = "Bool"
|
||||||
|
|
||||||
|
// IP address condition operators.
|
||||||
|
CondIPAddress string = "IpAddress"
|
||||||
|
CondNotIPAddress string = "NotIpAddress"
|
||||||
|
|
||||||
|
// ARN condition operators.
|
||||||
|
CondArnEquals string = "ArnEquals"
|
||||||
|
CondArnLike string = "ArnLike"
|
||||||
|
CondArnNotEquals string = "ArnNotEquals"
|
||||||
|
CondArnNotLike string = "ArnNotLike"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p Policy) ToChain() (*policyengine.Chain, error) {
|
||||||
|
if err := p.Validate(GeneralPolicyType); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var chain policyengine.Chain
|
||||||
|
|
||||||
|
for _, statement := range p.Statement {
|
||||||
|
status := policyengine.AccessDenied
|
||||||
|
if statement.Effect == AllowEffect {
|
||||||
|
status = policyengine.Allow
|
||||||
|
}
|
||||||
|
|
||||||
|
var principals []string
|
||||||
|
var op policyengine.ConditionType
|
||||||
|
statementPrincipal, inverted := statement.principal()
|
||||||
|
if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false
|
||||||
|
principals = []string{Wildcard}
|
||||||
|
op = policyengine.CondStringLike
|
||||||
|
} else {
|
||||||
|
for _, principal := range statementPrincipal {
|
||||||
|
principals = append(principals, principal...)
|
||||||
|
}
|
||||||
|
|
||||||
|
op = policyengine.CondStringEquals
|
||||||
|
if inverted {
|
||||||
|
op = policyengine.CondStringNotEquals
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var conditions []policyengine.Condition
|
||||||
|
for _, principal := range principals {
|
||||||
|
conditions = append(conditions, policyengine.Condition{
|
||||||
|
Op: op,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: RequestOwnerProperty,
|
||||||
|
Value: principal,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
conds, err := statement.Conditions.ToChainCondition()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conditions = append(conditions, conds...)
|
||||||
|
|
||||||
|
action, actionInverted := statement.action()
|
||||||
|
ruleAction := policyengine.Actions{Inverted: actionInverted, Names: action}
|
||||||
|
|
||||||
|
resource, resourceInverted := statement.resource()
|
||||||
|
ruleResource := policyengine.Resources{Inverted: resourceInverted, Names: resource}
|
||||||
|
|
||||||
|
r := policyengine.Rule{
|
||||||
|
Status: status,
|
||||||
|
Actions: ruleAction,
|
||||||
|
Resources: ruleResource,
|
||||||
|
Any: true,
|
||||||
|
Condition: conditions,
|
||||||
|
}
|
||||||
|
chain.Rules = append(chain.Rules, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chain, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:funlen
|
||||||
|
func (c Conditions) ToChainCondition() ([]policyengine.Condition, error) {
|
||||||
|
var conditions []policyengine.Condition
|
||||||
|
|
||||||
|
var convertValue convertFunction
|
||||||
|
|
||||||
|
for op, KVs := range c {
|
||||||
|
var condType policyengine.ConditionType
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(op, "String"):
|
||||||
|
convertValue = noConvertFunction
|
||||||
|
switch op {
|
||||||
|
case CondStringEquals:
|
||||||
|
condType = policyengine.CondStringEquals
|
||||||
|
case CondStringNotEquals:
|
||||||
|
condType = policyengine.CondStringNotEquals
|
||||||
|
case CondStringEqualsIgnoreCase:
|
||||||
|
condType = policyengine.CondStringEqualsIgnoreCase
|
||||||
|
case CondStringNotEqualsIgnoreCase:
|
||||||
|
condType = policyengine.CondStringNotEqualsIgnoreCase
|
||||||
|
case CondStringLike:
|
||||||
|
condType = policyengine.CondStringLike
|
||||||
|
case CondStringNotLike:
|
||||||
|
condType = policyengine.CondStringNotLike
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(op, "Arn"):
|
||||||
|
convertValue = noConvertFunction
|
||||||
|
switch op {
|
||||||
|
case CondArnEquals:
|
||||||
|
condType = policyengine.CondStringEquals
|
||||||
|
case CondArnNotEquals:
|
||||||
|
condType = policyengine.CondStringNotEquals
|
||||||
|
case CondArnLike:
|
||||||
|
condType = policyengine.CondStringLike
|
||||||
|
case CondArnNotLike:
|
||||||
|
condType = policyengine.CondStringNotLike
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(op, "Numeric"):
|
||||||
|
// TODO
|
||||||
|
case strings.HasPrefix(op, "Date"):
|
||||||
|
convertValue = dateConvertFunction
|
||||||
|
switch op {
|
||||||
|
case CondDateEquals:
|
||||||
|
condType = policyengine.CondStringEquals
|
||||||
|
case CondDateNotEquals:
|
||||||
|
condType = policyengine.CondStringNotEquals
|
||||||
|
case CondDateLessThan:
|
||||||
|
condType = policyengine.CondStringLessThan
|
||||||
|
case CondDateLessThanEquals:
|
||||||
|
condType = policyengine.CondStringLessThanEquals
|
||||||
|
case CondDateGreaterThan:
|
||||||
|
condType = policyengine.CondStringGreaterThan
|
||||||
|
case CondDateGreaterThanEquals:
|
||||||
|
condType = policyengine.CondStringGreaterThanEquals
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
|
||||||
|
}
|
||||||
|
case op == CondBool:
|
||||||
|
convertValue = noConvertFunction
|
||||||
|
condType = policyengine.CondStringEqualsIgnoreCase
|
||||||
|
case op == CondIPAddress:
|
||||||
|
// todo consider using converters
|
||||||
|
// "203.0.113.0/24" -> "203.0.113.*",
|
||||||
|
// "2001:DB8:1234:5678::/64" -> "2001:DB8:1234:5678:*"
|
||||||
|
// or having specific condition type for IP
|
||||||
|
convertValue = noConvertFunction
|
||||||
|
condType = policyengine.CondStringLike
|
||||||
|
case op == CondNotIPAddress:
|
||||||
|
convertValue = noConvertFunction
|
||||||
|
condType = policyengine.CondStringNotLike
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported condition operator: '%s'", op)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, values := range KVs {
|
||||||
|
for _, val := range values {
|
||||||
|
converted, err := convertValue(val)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conditions = append(conditions, policyengine.Condition{
|
||||||
|
Op: condType,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: key,
|
||||||
|
Value: converted,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return conditions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type convertFunction func(string) (string, error)
|
||||||
|
|
||||||
|
func noConvertFunction(val string) (string, error) {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dateConvertFunction(val string) (string, error) {
|
||||||
|
if _, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := time.Parse(time.RFC3339, val)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.FormatInt(parsed.UTC().Unix(), 10), nil
|
||||||
|
}
|
306
iam/converter_test.go
Normal file
306
iam/converter_test.go
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
package iam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConverters(t *testing.T) {
|
||||||
|
t.Run("valid policy", func(t *testing.T) {
|
||||||
|
p := Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []Statement{{
|
||||||
|
Principal: map[PrincipalType][]string{
|
||||||
|
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
|
||||||
|
},
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||||
|
Conditions: map[string]Condition{
|
||||||
|
CondStringEquals: {
|
||||||
|
"s3:RequestObjectTag/Department": {"Finance"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := &policyengine.Chain{Rules: []policyengine.Rule{
|
||||||
|
{
|
||||||
|
Status: policyengine.Allow,
|
||||||
|
Actions: policyengine.Actions{Names: p.Statement[0].Action},
|
||||||
|
Resources: policyengine.Resources{Names: p.Statement[0].Resource},
|
||||||
|
Any: true,
|
||||||
|
Condition: []policyengine.Condition{
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: RequestOwnerProperty,
|
||||||
|
Value: "arn:aws:iam::111122223333:user/JohnDoe",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "s3:RequestObjectTag/Department",
|
||||||
|
Value: "Finance",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
chain, err := p.ToChain()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, chain)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid inverted policy", func(t *testing.T) {
|
||||||
|
p := Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []Statement{{
|
||||||
|
NotPrincipal: map[PrincipalType][]string{
|
||||||
|
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
|
||||||
|
},
|
||||||
|
Effect: DenyEffect,
|
||||||
|
NotAction: []string{"s3:PutObject"},
|
||||||
|
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := &policyengine.Chain{Rules: []policyengine.Rule{
|
||||||
|
{
|
||||||
|
Status: policyengine.AccessDenied,
|
||||||
|
Actions: policyengine.Actions{Inverted: true, Names: p.Statement[0].NotAction},
|
||||||
|
Resources: policyengine.Resources{Inverted: true, Names: p.Statement[0].NotResource},
|
||||||
|
Any: true,
|
||||||
|
Condition: []policyengine.Condition{
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringNotEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: RequestOwnerProperty,
|
||||||
|
Value: "arn:aws:iam::111122223333:user/JohnDoe",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
chain, err := p.ToChain()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, chain)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid policy (unsupported principal type)", func(t *testing.T) {
|
||||||
|
p := Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []Statement{{
|
||||||
|
Principal: map[PrincipalType][]string{
|
||||||
|
"dummy": {"arn:aws:iam::111122223333:user/JohnDoe"},
|
||||||
|
},
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.ToChain()
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid policy (missing resource)", func(t *testing.T) {
|
||||||
|
p := Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []Statement{{
|
||||||
|
Principal: map[PrincipalType][]string{
|
||||||
|
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
|
||||||
|
},
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := p.ToChain()
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check policy conditions", func(t *testing.T) {
|
||||||
|
p := Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []Statement{{
|
||||||
|
Principal: map[PrincipalType][]string{Wildcard: nil},
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||||
|
Conditions: Conditions{
|
||||||
|
CondStringEquals: {"key1": {"val0", "val1"}},
|
||||||
|
CondStringNotEquals: {"key2": {"val2"}},
|
||||||
|
CondStringEqualsIgnoreCase: {"key3": {"val3"}},
|
||||||
|
CondStringNotEqualsIgnoreCase: {"key4": {"val4"}},
|
||||||
|
CondStringLike: {"key5": {"val5"}},
|
||||||
|
CondStringNotLike: {"key6": {"val6"}},
|
||||||
|
CondDateEquals: {"key7": {"2006-01-02T15:04:05+07:00"}},
|
||||||
|
CondDateNotEquals: {"key8": {"2006-01-02T15:04:05Z"}},
|
||||||
|
CondDateLessThan: {"key9": {"2006-01-02T15:04:05+06:00"}},
|
||||||
|
CondDateLessThanEquals: {"key10": {"2006-01-02T15:04:05+03:00"}},
|
||||||
|
CondDateGreaterThan: {"key11": {"2006-01-02T15:04:05-01:00"}},
|
||||||
|
CondDateGreaterThanEquals: {"key12": {"2006-01-02T15:04:05-03:00"}},
|
||||||
|
CondBool: {"key13": {"True"}},
|
||||||
|
CondIPAddress: {"key14": {"val14"}},
|
||||||
|
CondNotIPAddress: {"key15": {"val15"}},
|
||||||
|
CondArnEquals: {"key16": {"val16"}},
|
||||||
|
CondArnLike: {"key17": {"val17"}},
|
||||||
|
CondArnNotEquals: {"key18": {"val18"}},
|
||||||
|
CondArnNotLike: {"key19": {"val19"}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := &policyengine.Chain{Rules: []policyengine.Rule{
|
||||||
|
{
|
||||||
|
Status: policyengine.Allow,
|
||||||
|
Actions: policyengine.Actions{Names: p.Statement[0].Action},
|
||||||
|
Resources: policyengine.Resources{Names: p.Statement[0].Resource},
|
||||||
|
Any: true,
|
||||||
|
Condition: []policyengine.Condition{
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringLike,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: RequestOwnerProperty,
|
||||||
|
Value: "*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key1",
|
||||||
|
Value: "val0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key1",
|
||||||
|
Value: "val1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringNotEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key2",
|
||||||
|
Value: "val2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringEqualsIgnoreCase,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key3",
|
||||||
|
Value: "val3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringNotEqualsIgnoreCase,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key4",
|
||||||
|
Value: "val4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringLike,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key5",
|
||||||
|
Value: "val5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringNotLike,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key6",
|
||||||
|
Value: "val6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key7",
|
||||||
|
Value: "1136189045",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringNotEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key8",
|
||||||
|
Value: "1136214245",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringLessThan,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key9",
|
||||||
|
Value: "1136192645",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringLessThanEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key10",
|
||||||
|
Value: "1136203445",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringGreaterThan,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key11",
|
||||||
|
Value: "1136217845",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringGreaterThanEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key12",
|
||||||
|
Value: "1136225045",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringEqualsIgnoreCase,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key13",
|
||||||
|
Value: "True",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringLike,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key14",
|
||||||
|
Value: "val14",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringNotLike,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key15",
|
||||||
|
Value: "val15",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key16",
|
||||||
|
Value: "val16",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringLike,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key17",
|
||||||
|
Value: "val17",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringNotEquals,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key18",
|
||||||
|
Value: "val18",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: policyengine.CondStringNotLike,
|
||||||
|
Object: policyengine.ObjectRequest,
|
||||||
|
Key: "key19",
|
||||||
|
Value: "val19",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
chain, err := p.ToChain()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for i, rule := range chain.Rules {
|
||||||
|
expectedRule := expected.Rules[i]
|
||||||
|
require.Equal(t, expectedRule.Actions, rule.Actions)
|
||||||
|
require.Equal(t, expectedRule.Any, rule.Any)
|
||||||
|
require.Equal(t, expectedRule.Resources, rule.Resources)
|
||||||
|
require.Equal(t, expectedRule.Status, rule.Status)
|
||||||
|
require.ElementsMatch(t, expectedRule.Condition, rule.Condition)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
314
iam/policy.go
Normal file
314
iam/policy.go
Normal file
|
@ -0,0 +1,314 @@
|
||||||
|
package iam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Policy grammar https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
|
||||||
|
Policy struct {
|
||||||
|
Version string `json:"Version,omitempty"`
|
||||||
|
ID string `json:"Id,omitempty"`
|
||||||
|
Statement Statements `json:"Statement"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Statements []Statement
|
||||||
|
|
||||||
|
Statement struct {
|
||||||
|
ID string `json:"Id,omitempty"`
|
||||||
|
SID string `json:"Sid,omitempty"`
|
||||||
|
Principal Principal `json:"Principal,omitempty"`
|
||||||
|
NotPrincipal Principal `json:"NotPrincipal,omitempty"`
|
||||||
|
Effect Effect `json:"Effect"`
|
||||||
|
Action Action `json:"Action,omitempty"`
|
||||||
|
NotAction Action `json:"NotAction,omitempty"`
|
||||||
|
Resource Resource `json:"Resource,omitempty"`
|
||||||
|
NotResource Resource `json:"NotResource,omitempty"`
|
||||||
|
Conditions Conditions `json:"Condition,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Principal map[PrincipalType][]string
|
||||||
|
|
||||||
|
Effect string
|
||||||
|
|
||||||
|
Action []string
|
||||||
|
|
||||||
|
Resource []string
|
||||||
|
|
||||||
|
Conditions map[string]Condition
|
||||||
|
|
||||||
|
Condition map[string][]string
|
||||||
|
|
||||||
|
PolicyType int
|
||||||
|
|
||||||
|
PrincipalType string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GeneralPolicyType PolicyType = iota
|
||||||
|
IdentityBasedPolicyType
|
||||||
|
ResourceBasedPolicyType
|
||||||
|
)
|
||||||
|
|
||||||
|
const Wildcard = "*"
|
||||||
|
|
||||||
|
const (
|
||||||
|
AllowEffect Effect = "Allow"
|
||||||
|
DenyEffect Effect = "Deny"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e Effect) IsValid() bool {
|
||||||
|
return e == AllowEffect || e == DenyEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
AWSPrincipalType PrincipalType = "AWS"
|
||||||
|
FederatedPrincipalType PrincipalType = "Federated"
|
||||||
|
ServicePrincipalType PrincipalType = "Service"
|
||||||
|
CanonicalUserPrincipalType PrincipalType = "CanonicalUser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p PrincipalType) IsValid() bool {
|
||||||
|
return p == AWSPrincipalType || p == FederatedPrincipalType ||
|
||||||
|
p == ServicePrincipalType || p == CanonicalUserPrincipalType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Statements) UnmarshalJSON(data []byte) error {
|
||||||
|
var list []Statement
|
||||||
|
if err := json.Unmarshal(data, &list); err == nil {
|
||||||
|
*s = list
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var elem Statement
|
||||||
|
if err := json.Unmarshal(data, &elem); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = []Statement{elem}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Principal) UnmarshalJSON(data []byte) error {
|
||||||
|
*p = make(Principal)
|
||||||
|
|
||||||
|
var str string
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &str); err == nil {
|
||||||
|
if str != Wildcard {
|
||||||
|
return errors.New("invalid IAM string principal")
|
||||||
|
}
|
||||||
|
(*p)[Wildcard] = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make(map[PrincipalType]any)
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val := range m {
|
||||||
|
element, ok := val.(string)
|
||||||
|
if ok {
|
||||||
|
(*p)[key] = []string{element}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
list, ok := val.([]any)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid principal format")
|
||||||
|
}
|
||||||
|
|
||||||
|
resList := make([]string, len(list))
|
||||||
|
for i := range list {
|
||||||
|
val, ok := list[i].(string)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid principal format")
|
||||||
|
}
|
||||||
|
resList[i] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
(*p)[key] = resList
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Action) UnmarshalJSON(data []byte) error {
|
||||||
|
var list []string
|
||||||
|
if err := json.Unmarshal(data, &list); err == nil {
|
||||||
|
*a = list
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var elem string
|
||||||
|
if err := json.Unmarshal(data, &elem); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*a = []string{elem}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Resource) UnmarshalJSON(data []byte) error {
|
||||||
|
var list []string
|
||||||
|
if err := json.Unmarshal(data, &list); err == nil {
|
||||||
|
*r = list
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var elem string
|
||||||
|
if err := json.Unmarshal(data, &elem); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*r = []string{elem}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Condition) UnmarshalJSON(data []byte) error {
|
||||||
|
*c = make(Condition)
|
||||||
|
|
||||||
|
m := make(map[string]any)
|
||||||
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, val := range m {
|
||||||
|
element, ok := val.(string)
|
||||||
|
if ok {
|
||||||
|
(*c)[key] = []string{element}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
list, ok := val.([]any)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid principal format")
|
||||||
|
}
|
||||||
|
|
||||||
|
resList := make([]string, len(list))
|
||||||
|
for i := range list {
|
||||||
|
val, ok := list[i].(string)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("invalid principal format")
|
||||||
|
}
|
||||||
|
resList[i] = val
|
||||||
|
}
|
||||||
|
|
||||||
|
(*c)[key] = resList
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Policy) Validate(typ PolicyType) error {
|
||||||
|
if err := p.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typ {
|
||||||
|
case IdentityBasedPolicyType:
|
||||||
|
return p.validateIdentityBased()
|
||||||
|
case ResourceBasedPolicyType:
|
||||||
|
return p.validateResourceBased()
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Policy) validate() error {
|
||||||
|
for _, statement := range p.Statement {
|
||||||
|
if !statement.Effect.IsValid() {
|
||||||
|
return fmt.Errorf("unknown effect: '%s'", statement.Effect)
|
||||||
|
}
|
||||||
|
if len(statement.Action) != 0 && len(statement.NotAction) != 0 {
|
||||||
|
return errors.New("'Actions' and 'NotAction' are mutually exclusive")
|
||||||
|
}
|
||||||
|
if statement.Resource != nil && statement.NotResource != nil {
|
||||||
|
return errors.New("'Resources' and 'NotResource' are mutually exclusive")
|
||||||
|
}
|
||||||
|
if len(statement.Resource) == 0 && len(statement.NotResource) == 0 {
|
||||||
|
return errors.New("one of 'Resources'/'NotResource' must be provided")
|
||||||
|
}
|
||||||
|
if len(statement.Principal) != 0 && len(statement.NotPrincipal) != 0 {
|
||||||
|
return errors.New("'Principal' and 'NotPrincipal' are mutually exclusive")
|
||||||
|
}
|
||||||
|
if len(statement.NotPrincipal) != 0 && statement.Effect != DenyEffect {
|
||||||
|
return errors.New("using 'NotPrincipal' with effect 'Allow' is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
principal, _ := statement.principal()
|
||||||
|
if err := principal.validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Policy) validateIdentityBased() error {
|
||||||
|
if len(p.ID) != 0 {
|
||||||
|
return errors.New("'Id' is not allowed for identity-based policy")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, statement := range p.Statement {
|
||||||
|
if len(statement.Principal) != 0 || len(statement.NotPrincipal) != 0 {
|
||||||
|
return errors.New("'Principal' and 'NotPrincipal' are not allowed for identity-based policy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Policy) validateResourceBased() error {
|
||||||
|
for _, statement := range p.Statement {
|
||||||
|
if len(statement.Principal) == 0 && len(statement.NotPrincipal) == 0 {
|
||||||
|
return errors.New("'Principal' or 'NotPrincipal' must be provided for resource-based policy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Statement) principal() (Principal, bool) {
|
||||||
|
if len(s.NotPrincipal) != 0 {
|
||||||
|
return s.NotPrincipal, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Principal, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Statement) action() (Action, bool) {
|
||||||
|
if len(s.NotAction) != 0 {
|
||||||
|
return s.NotAction, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Action, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Statement) resource() (Resource, bool) {
|
||||||
|
if len(s.NotResource) != 0 {
|
||||||
|
return s.NotResource, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Resource, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Principal) validate() error {
|
||||||
|
if _, ok := p[Wildcard]; ok && len(p) == 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range p {
|
||||||
|
if !key.IsValid() {
|
||||||
|
return fmt.Errorf("unknown principal type: '%s'", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
424
iam/policy_test.go
Normal file
424
iam/policy_test.go
Normal file
|
@ -0,0 +1,424 @@
|
||||||
|
package iam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUnmarshalIAMPolicy(t *testing.T) {
|
||||||
|
t.Run("simple fields", func(t *testing.T) {
|
||||||
|
policy := `{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Id": "PutObjPolicy",
|
||||||
|
"Statement": {
|
||||||
|
"Sid": "DenyObjectsThatAreNotSSEKMS",
|
||||||
|
"Principal": "*",
|
||||||
|
"Effect": "Deny",
|
||||||
|
"Action": "s3:PutObject",
|
||||||
|
"Resource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*",
|
||||||
|
"Condition": {
|
||||||
|
"Null": {
|
||||||
|
"s3:x-amz-server-side-encryption-aws-kms-key-id": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
ID: "PutObjPolicy",
|
||||||
|
Statement: []Statement{{
|
||||||
|
SID: "DenyObjectsThatAreNotSSEKMS",
|
||||||
|
Principal: map[PrincipalType][]string{
|
||||||
|
"*": nil,
|
||||||
|
},
|
||||||
|
Effect: DenyEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||||
|
Conditions: map[string]Condition{
|
||||||
|
"Null": {
|
||||||
|
"s3:x-amz-server-side-encryption-aws-kms-key-id": {"true"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var p Policy
|
||||||
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, p)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("complex fields", func(t *testing.T) {
|
||||||
|
policy := `{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [{
|
||||||
|
"Principal":{
|
||||||
|
"AWS":[
|
||||||
|
"arn:aws:iam::111122223333:user/JohnDoe"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:PutObject"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
|
||||||
|
],
|
||||||
|
"Condition": {
|
||||||
|
"StringEquals": {
|
||||||
|
"s3:RequestObjectTag/Department": ["Finance"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := Policy{
|
||||||
|
Version: "2012-10-17",
|
||||||
|
Statement: []Statement{{
|
||||||
|
Principal: map[PrincipalType][]string{
|
||||||
|
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
|
||||||
|
},
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||||
|
Conditions: map[string]Condition{
|
||||||
|
"StringEquals": {
|
||||||
|
"s3:RequestObjectTag/Department": {"Finance"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var p Policy
|
||||||
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, p)
|
||||||
|
|
||||||
|
raw, err := json.Marshal(expected)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t, policy, string(raw))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check principal AWS", func(t *testing.T) {
|
||||||
|
policy := `{
|
||||||
|
"Statement": [{
|
||||||
|
"Principal":{
|
||||||
|
"AWS":"arn:aws:iam::111122223333:user/JohnDoe"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Principal: map[PrincipalType][]string{
|
||||||
|
AWSPrincipalType: {"arn:aws:iam::111122223333:user/JohnDoe"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var p Policy
|
||||||
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, p)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("native example", func(t *testing.T) {
|
||||||
|
policy := `
|
||||||
|
{
|
||||||
|
"Version": "xyz",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"native:*",
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:GetObject"
|
||||||
|
],
|
||||||
|
"Resource": ["*"],
|
||||||
|
"Principal": {"FrostFS": ["did:frostfs:039e3ee771a223361fe7862f532e9511b57baaae3c3e2622682e99d0e660f7671"]},
|
||||||
|
"Condition": {"StringEquals": {"native::object::attribute": "iamuser-admin"}}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
var p Policy
|
||||||
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("condition array", func(t *testing.T) {
|
||||||
|
policy := `
|
||||||
|
{
|
||||||
|
"Statement": [{
|
||||||
|
"Condition": {"StringLike": {"ec2:InstanceType": ["t1.*", "t2.*", "m3.*"]}}
|
||||||
|
}]
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Conditions: map[string]Condition{
|
||||||
|
"StringLike": {"ec2:InstanceType": {"t1.*", "t2.*", "m3.*"}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var p Policy
|
||||||
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, p)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("'Not*' fields", func(t *testing.T) {
|
||||||
|
policy := `
|
||||||
|
{
|
||||||
|
"Id": "PutObjPolicy",
|
||||||
|
"Statement": [{
|
||||||
|
"NotPrincipal": {"AWS":["arn:aws:iam::111122223333:user/Alice"]},
|
||||||
|
"Effect": "Deny",
|
||||||
|
"NotAction": "s3:PutObject",
|
||||||
|
"NotResource": "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"
|
||||||
|
}]
|
||||||
|
}`
|
||||||
|
|
||||||
|
expected := Policy{
|
||||||
|
ID: "PutObjPolicy",
|
||||||
|
Statement: []Statement{{
|
||||||
|
NotPrincipal: map[PrincipalType][]string{
|
||||||
|
AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"},
|
||||||
|
},
|
||||||
|
Effect: DenyEffect,
|
||||||
|
NotAction: []string{"s3:PutObject"},
|
||||||
|
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var p Policy
|
||||||
|
err := json.Unmarshal([]byte(policy), &p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expected, p)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidatePolicies(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
policy Policy
|
||||||
|
typ PolicyType
|
||||||
|
isValid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid permission boundaries",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: GeneralPolicyType,
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "general invalid effect",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: "dummy",
|
||||||
|
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: GeneralPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "general invalid principal block",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
Principal: map[PrincipalType][]string{Wildcard: nil},
|
||||||
|
NotPrincipal: map[PrincipalType][]string{Wildcard: nil},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: GeneralPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "general invalid not principal",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: GeneralPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "general invalid principal type",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
NotPrincipal: map[PrincipalType][]string{"dummy": {"arn:aws:iam::111122223333:user/Alice"}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: GeneralPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "general invalid action block",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:*", "cloudwatch:*", "ec2:*"},
|
||||||
|
NotAction: []string{"iam:*"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: GeneralPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "general invalid resource block",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: GeneralPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid resource block",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Resource: []string{},
|
||||||
|
NotResource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: GeneralPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing resource block",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: GeneralPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "identity based valid",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: IdentityBasedPolicyType,
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "identity based invalid because of id presence",
|
||||||
|
policy: Policy{
|
||||||
|
ID: "some-id",
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: IdentityBasedPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "identity based invalid because of principal presence",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: IdentityBasedPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "identity based invalid because of not principal presence",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: IdentityBasedPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource based valid principal",
|
||||||
|
policy: Policy{
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: DenyEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
Principal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: ResourceBasedPolicyType,
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource based valid not principal",
|
||||||
|
policy: Policy{
|
||||||
|
ID: "some-id",
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: DenyEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
NotPrincipal: map[PrincipalType][]string{AWSPrincipalType: {"arn:aws:iam::111122223333:user/Alice"}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: ResourceBasedPolicyType,
|
||||||
|
isValid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource based invalid missing principal",
|
||||||
|
policy: Policy{
|
||||||
|
ID: "some-id",
|
||||||
|
Statement: []Statement{{
|
||||||
|
Effect: AllowEffect,
|
||||||
|
Action: []string{"s3:PutObject"},
|
||||||
|
Resource: []string{Wildcard},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
typ: ResourceBasedPolicyType,
|
||||||
|
isValid: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.policy.Validate(tc.typ)
|
||||||
|
if tc.isValid {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
106
inmemory.go
Normal file
106
inmemory.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
type inmemory struct {
|
||||||
|
namespace map[Name][]chain
|
||||||
|
resource map[Name][]chain
|
||||||
|
local map[Name][]*Chain
|
||||||
|
}
|
||||||
|
|
||||||
|
type chain struct {
|
||||||
|
object string
|
||||||
|
chain *Chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInMemory returns new inmemory instance of chain storage.
|
||||||
|
func NewInMemory() CachedChainStorage {
|
||||||
|
return &inmemory{
|
||||||
|
namespace: make(map[Name][]chain),
|
||||||
|
resource: make(map[Name][]chain),
|
||||||
|
local: make(map[Name][]*Chain),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAllowed implements the Engine interface.
|
||||||
|
func (s *inmemory) IsAllowed(name Name, namespace string, r Request) (Status, bool) {
|
||||||
|
var ruleFound bool
|
||||||
|
if local, ok := s.local[name]; ok {
|
||||||
|
for _, c := range local {
|
||||||
|
if status, matched := c.Match(r); matched && status != Allow {
|
||||||
|
return status, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cs, ok := s.namespace[name]; ok {
|
||||||
|
status, ok := matchArray(cs, namespace, r)
|
||||||
|
if ok && status != Allow {
|
||||||
|
return status, true
|
||||||
|
}
|
||||||
|
ruleFound = ruleFound || ok
|
||||||
|
}
|
||||||
|
if cs, ok := s.resource[name]; ok {
|
||||||
|
status, ok := matchArray(cs, r.Resource().Name(), r)
|
||||||
|
if ok {
|
||||||
|
return status, true
|
||||||
|
}
|
||||||
|
ruleFound = ruleFound || ok
|
||||||
|
}
|
||||||
|
if ruleFound {
|
||||||
|
return Allow, true
|
||||||
|
}
|
||||||
|
return NoRuleFound, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchArray(cs []chain, object string, r Request) (Status, bool) {
|
||||||
|
for _, c := range cs {
|
||||||
|
if !globMatch(object, c.object) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if status, matched := c.chain.Match(r); matched {
|
||||||
|
return status, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NoRuleFound, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inmemory) AddResourceChain(name Name, resource string, c *Chain) {
|
||||||
|
s.resource[name] = append(s.resource[name], chain{resource, c})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inmemory) AddNameSpaceChain(name Name, namespace string, c *Chain) {
|
||||||
|
s.namespace[name] = append(s.namespace[name], chain{namespace, c})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inmemory) AddOverride(name Name, c *Chain) {
|
||||||
|
s.local[name] = append(s.local[name], c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inmemory) GetOverride(name Name, chainID ChainID) (chain *Chain, found bool) {
|
||||||
|
chains := s.local[name]
|
||||||
|
|
||||||
|
for _, chain = range chains {
|
||||||
|
if chain.ID == chainID {
|
||||||
|
found = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inmemory) RemoveOverride(name Name, chainID ChainID) (found bool) {
|
||||||
|
chains := s.local[name]
|
||||||
|
|
||||||
|
for i, chain := range chains {
|
||||||
|
if chain.ID == chainID {
|
||||||
|
s.local[name] = append(chains[:i], chains[i+1:]...)
|
||||||
|
found = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *inmemory) ListOverrides(name Name) []*Chain {
|
||||||
|
return s.local[name]
|
||||||
|
}
|
193
inmemory_test.go
Normal file
193
inmemory_test.go
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInmemory(t *testing.T) {
|
||||||
|
const (
|
||||||
|
object = "native::object::abc/xyz"
|
||||||
|
container = "native::object::abc/*"
|
||||||
|
namespace = "Tenant1"
|
||||||
|
namespace2 = "Tenant2"
|
||||||
|
actor1 = "owner1"
|
||||||
|
actor2 = "owner2"
|
||||||
|
)
|
||||||
|
|
||||||
|
s := NewInMemory()
|
||||||
|
|
||||||
|
// Object which was put via S3.
|
||||||
|
res := newResource(object, map[string]string{"FromS3": "true"})
|
||||||
|
// Request initiating from the trusted subnet and actor.
|
||||||
|
reqGood := newRequest("native::object::put", res, map[string]string{
|
||||||
|
"SourceIP": "10.1.1.12",
|
||||||
|
"Actor": actor1,
|
||||||
|
})
|
||||||
|
|
||||||
|
status, ok := s.IsAllowed(Ingress, namespace, reqGood)
|
||||||
|
require.Equal(t, NoRuleFound, status)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
s.AddNameSpaceChain(Ingress, namespace, &Chain{
|
||||||
|
Rules: []Rule{
|
||||||
|
{ // Restrict to remove ANY object from the namespace.
|
||||||
|
Status: AccessDenied,
|
||||||
|
Actions: Actions{Names: []string{"native::object::delete"}},
|
||||||
|
Resources: Resources{Names: []string{"native::object::*"}},
|
||||||
|
},
|
||||||
|
{ // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise.
|
||||||
|
Status: AccessDenied,
|
||||||
|
Actions: Actions{Names: []string{"native::object::put"}},
|
||||||
|
Resources: Resources{Names: []string{"native::object::*"}},
|
||||||
|
Any: true,
|
||||||
|
Condition: []Condition{
|
||||||
|
{
|
||||||
|
Op: CondStringNotLike,
|
||||||
|
Object: ObjectRequest,
|
||||||
|
Key: "SourceIP",
|
||||||
|
Value: "10.1.1.*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: CondStringNotEquals,
|
||||||
|
Object: ObjectRequest,
|
||||||
|
Key: "Actor",
|
||||||
|
Value: actor1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddNameSpaceChain(Ingress, namespace2, &Chain{
|
||||||
|
Rules: []Rule{
|
||||||
|
{ // Deny all expect "native::object::get" for all objects expect "native::object::abc/xyz".
|
||||||
|
Status: AccessDenied,
|
||||||
|
Actions: Actions{Inverted: true, Names: []string{"native::object::get"}},
|
||||||
|
Resources: Resources{Inverted: true, Names: []string{object}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
s.AddResourceChain(Ingress, container, &Chain{
|
||||||
|
Rules: []Rule{
|
||||||
|
{ // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute.
|
||||||
|
Status: Allow,
|
||||||
|
Actions: Actions{Names: []string{"native::object::get"}},
|
||||||
|
Resources: Resources{Names: []string{"native::object::abc/*"}},
|
||||||
|
Condition: []Condition{
|
||||||
|
{
|
||||||
|
Op: CondStringEquals,
|
||||||
|
Object: ObjectResource,
|
||||||
|
Key: "Department",
|
||||||
|
Value: "HR",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Op: CondStringEquals,
|
||||||
|
Object: ObjectRequest,
|
||||||
|
Key: "Actor",
|
||||||
|
Value: actor2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad subnet, namespace deny", func(t *testing.T) {
|
||||||
|
// Request initiating from the untrusted subnet.
|
||||||
|
reqBadIP := newRequest("native::object::put", res, map[string]string{
|
||||||
|
"SourceIP": "10.122.1.20",
|
||||||
|
"Actor": actor1,
|
||||||
|
})
|
||||||
|
status, ok := s.IsAllowed(Ingress, namespace, reqBadIP)
|
||||||
|
require.Equal(t, AccessDenied, status)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
t.Run("bad actor, namespace deny", func(t *testing.T) {
|
||||||
|
// Request initiating from the untrusted actor.
|
||||||
|
reqBadActor := newRequest("native::object::put", res, map[string]string{
|
||||||
|
"SourceIP": "10.1.1.13",
|
||||||
|
"Actor": actor2,
|
||||||
|
})
|
||||||
|
status, ok := s.IsAllowed(Ingress, namespace, reqBadActor)
|
||||||
|
require.Equal(t, AccessDenied, status)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
t.Run("bad object, container deny", func(t *testing.T) {
|
||||||
|
objGood := newResource("native::object::abc/id1", map[string]string{"Department": "HR"})
|
||||||
|
objBadAttr := newResource("native::object::abc/id2", map[string]string{"Department": "Support"})
|
||||||
|
|
||||||
|
status, ok := s.IsAllowed(Ingress, namespace, newRequest("native::object::get", objGood, map[string]string{
|
||||||
|
"SourceIP": "10.1.1.14",
|
||||||
|
"Actor": actor2,
|
||||||
|
}))
|
||||||
|
require.Equal(t, Allow, status)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
status, ok = s.IsAllowed(Ingress, namespace, newRequest("native::object::get", objBadAttr, map[string]string{
|
||||||
|
"SourceIP": "10.1.1.14",
|
||||||
|
"Actor": actor2,
|
||||||
|
}))
|
||||||
|
require.Equal(t, NoRuleFound, status)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
t.Run("bad operation, namespace deny", func(t *testing.T) {
|
||||||
|
// Request with the forbidden operation.
|
||||||
|
reqBadOperation := newRequest("native::object::delete", res, map[string]string{
|
||||||
|
"SourceIP": "10.1.1.12",
|
||||||
|
"Actor": actor1,
|
||||||
|
})
|
||||||
|
status, ok := s.IsAllowed(Ingress, namespace, reqBadOperation)
|
||||||
|
require.Equal(t, AccessDenied, status)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
t.Run("inverted rules", func(t *testing.T) {
|
||||||
|
req := newRequest("native::object::put", newResource(object, nil), nil)
|
||||||
|
status, ok = s.IsAllowed(Ingress, namespace2, req)
|
||||||
|
require.Equal(t, NoRuleFound, status)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
req = newRequest("native::object::put", newResource("native::object::cba/def", nil), nil)
|
||||||
|
status, ok = s.IsAllowed(Ingress, namespace2, req)
|
||||||
|
require.Equal(t, AccessDenied, status)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
req = newRequest("native::object::get", newResource("native::object::cba/def", nil), nil)
|
||||||
|
status, ok = s.IsAllowed(Ingress, namespace2, req)
|
||||||
|
require.Equal(t, NoRuleFound, status)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
t.Run("good", func(t *testing.T) {
|
||||||
|
status, ok = s.IsAllowed(Ingress, namespace, reqGood)
|
||||||
|
require.Equal(t, NoRuleFound, status)
|
||||||
|
require.False(t, ok)
|
||||||
|
|
||||||
|
t.Run("quota on a different container", func(t *testing.T) {
|
||||||
|
s.AddOverride(Ingress, &Chain{
|
||||||
|
Rules: []Rule{{
|
||||||
|
Status: QuotaLimitReached,
|
||||||
|
Actions: Actions{Names: []string{"native::object::put"}},
|
||||||
|
Resources: Resources{Names: []string{"native::object::cba/*"}},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
status, ok = s.IsAllowed(Ingress, namespace, reqGood)
|
||||||
|
require.Equal(t, NoRuleFound, status)
|
||||||
|
require.False(t, ok)
|
||||||
|
})
|
||||||
|
t.Run("quota on the request container", func(t *testing.T) {
|
||||||
|
s.AddOverride(Ingress, &Chain{
|
||||||
|
Rules: []Rule{{
|
||||||
|
Status: QuotaLimitReached,
|
||||||
|
Actions: Actions{Names: []string{"native::object::put"}},
|
||||||
|
Resources: Resources{Names: []string{"native::object::abc/*"}},
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
|
||||||
|
status, ok = s.IsAllowed(Ingress, namespace, reqGood)
|
||||||
|
require.Equal(t, QuotaLimitReached, status)
|
||||||
|
require.True(t, ok)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
18
interface.go
Normal file
18
interface.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
// CachedChainStorage ...
|
||||||
|
type CachedChainStorage interface {
|
||||||
|
Engine
|
||||||
|
// Adds a policy chain used for all operations with a specific resource.
|
||||||
|
AddResourceChain(name Name, resource string, c *Chain)
|
||||||
|
// Adds a policy chain used for all operations in the namespace.
|
||||||
|
AddNameSpaceChain(name Name, namespace string, c *Chain)
|
||||||
|
// Adds a local policy chain used for all operations with this service.
|
||||||
|
AddOverride(name Name, c *Chain)
|
||||||
|
// Gets the local override with given chain id.
|
||||||
|
GetOverride(name Name, chainID ChainID) (chain *Chain, found bool)
|
||||||
|
// Remove the local override with given chain id.
|
||||||
|
RemoveOverride(name Name, chainID ChainID) (removed bool)
|
||||||
|
// ListOverrides returns the list of local overrides.
|
||||||
|
ListOverrides(name Name) []*Chain
|
||||||
|
}
|
19
resource.go
Normal file
19
resource.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
// Request represents generic named resource (bucket, container etc.).
|
||||||
|
// Name is resource depenent but should be globally unique for any given
|
||||||
|
// type of resource.
|
||||||
|
type Request interface {
|
||||||
|
// Name is the operation name, such as Object.Put. Must not include wildcards.
|
||||||
|
Operation() string
|
||||||
|
// Property returns request properties, such as IP address of the origin.
|
||||||
|
Property(string) string
|
||||||
|
// Resource returns resource the operation is applied to.
|
||||||
|
Resource() Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource represents the resource operation is applied to.
|
||||||
|
type Resource interface {
|
||||||
|
Name() string
|
||||||
|
Property(string) string
|
||||||
|
}
|
49
resource_test.go
Normal file
49
resource_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package policyengine
|
||||||
|
|
||||||
|
type resource struct {
|
||||||
|
name string
|
||||||
|
properties map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) Name() string {
|
||||||
|
return r.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resource) Property(name string) string {
|
||||||
|
return r.properties[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResource(name string, properties map[string]string) *resource {
|
||||||
|
if properties == nil {
|
||||||
|
properties = make(map[string]string)
|
||||||
|
}
|
||||||
|
return &resource{name: name, properties: properties}
|
||||||
|
}
|
||||||
|
|
||||||
|
type request struct {
|
||||||
|
operation string
|
||||||
|
properties map[string]string
|
||||||
|
resource *resource
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Request = (*request)(nil)
|
||||||
|
|
||||||
|
func (r *request) Operation() string {
|
||||||
|
return r.operation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *request) Resource() Resource {
|
||||||
|
return r.resource
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *request) Property(name string) string {
|
||||||
|
return r.properties[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequest(op string, r *resource, properties map[string]string) *request {
|
||||||
|
return &request{
|
||||||
|
operation: op,
|
||||||
|
properties: properties,
|
||||||
|
resource: r,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue