Compare commits

..

1 commit
master ... init

Author SHA1 Message Date
c1ac4ad957 [#xx] Initial implementation
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
2023-10-20 13:17:48 +03:00
16 changed files with 124 additions and 1638 deletions

View file

@ -1,21 +0,0 @@
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 }}'

View file

@ -1,73 +0,0 @@
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

View file

@ -47,7 +47,7 @@ linters:
- durationcheck - durationcheck
- exhaustive - exhaustive
- exportloopref - exportloopref
- gofumpt - gofmt
- goimports - goimports
- misspell - misspell
- whitespace - whitespace

View file

@ -26,17 +26,14 @@ 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.6 rev: v0.9.0.2
hooks: hooks:
- id: shellcheck - id: shellcheck
- repo: local - repo: https://github.com/golangci/golangci-lint
rev: v1.51.2
hooks: hooks:
- id: make-lint - id: golangci-lint
name: Run Make Lint
entry: make lint
language: system
pass_filenames: false
- repo: local - repo: local
hooks: hooks:
@ -46,9 +43,3 @@ 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

View file

@ -1,62 +0,0 @@
#!/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 ./...

160
chain.go
View file

@ -1,7 +1,8 @@
package policyengine package policyengine
import ( import (
"encoding/json" "bytes"
"encoding/gob"
"fmt" "fmt"
"strings" "strings"
) )
@ -13,49 +14,42 @@ type Engine interface {
IsAllowed(name Name, namespace string, r Request) (Status, bool) IsAllowed(name Name, namespace string, r Request) (Status, bool)
} }
// ChainID is the ID of rule chain.
type ChainID string
type Chain struct { type Chain struct {
ID ChainID
Rules []Rule Rules []Rule
} }
func init() {
// FIXME #1 (@fyrchik): Introduce more optimal serialization format.
gob.Register(Chain{})
}
func (c *Chain) Bytes() []byte { func (c *Chain) Bytes() []byte {
data, err := json.Marshal(c) b := bytes.NewBuffer(nil)
if err != nil { e := gob.NewEncoder(b)
if err := e.Encode(c); err != nil {
panic(err) panic(err)
} }
return data return b.Bytes()
} }
func (c *Chain) DecodeBytes(b []byte) error { func (c *Chain) DecodeBytes(b []byte) error {
return json.Unmarshal(b, c) r := bytes.NewReader(b)
d := gob.NewDecoder(r)
return d.Decode(c)
} }
type Rule struct { type Rule struct {
Status Status Status Status
// Actions the operation is applied to. // Actions the operation is applied to.
Actions Actions Action []string
// List of the resources the operation is applied to. // List of the resources the operation is applied to.
Resources Resources Resource []string
// True iff individual conditions must be combined with the logical OR. // True iff individual conditions must be combined with the logical OR.
// By default AND is used, so _each_ condition must pass. // By default AND is used, so _each_ condition must pass.
Any bool Any bool
Condition []Condition Condition []Condition
} }
type Actions struct {
Inverted bool
Names []string
}
type Resources struct {
Inverted bool
Names []string
}
type Condition struct { type Condition struct {
Op ConditionType Op ConditionType
Object ObjectType Object ObjectType
@ -71,86 +65,65 @@ const (
ObjectActor ObjectActor
) )
type ConditionType byte // TODO @fyrchik: replace string with int-like type.
type ConditionType string
// TODO @fyrchik: reduce the number of conditions. // TODO @fyrchik: reduce the number of conditions.
// Everything from here should be expressable, but we do not need them all. // 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 // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
const ( const (
// String condition operators. // String condition operators.
CondStringEquals ConditionType = iota CondStringEquals ConditionType = "StringEquals"
CondStringNotEquals CondStringNotEquals ConditionType = "StringNotEquals"
CondStringEqualsIgnoreCase CondStringEqualsIgnoreCase ConditionType = "StringEqualsIgnoreCase"
CondStringNotEqualsIgnoreCase CondStringNotEqualsIgnoreCase ConditionType = "StringNotEqualsIgnoreCase"
CondStringLike CondStringLike ConditionType = "StringLike"
CondStringNotLike CondStringNotLike ConditionType = "StringNotLike"
CondStringLessThan
CondStringLessThanEquals
CondStringGreaterThan
CondStringGreaterThanEquals
// Numeric condition operators. // Numeric condition operators.
CondNumericEquals CondNumericEquals ConditionType = "NumericEquals"
CondNumericNotEquals CondNumericNotEquals ConditionType = "NumericNotEquals"
CondNumericLessThan CondNumericLessThan ConditionType = "NumericLessThan"
CondNumericLessThanEquals CondNumericLessThanEquals ConditionType = "NumericLessThanEquals"
CondNumericGreaterThan CondNumericGreaterThan ConditionType = "NumericGreaterThan"
CondNumericGreaterThanEquals CondNumericGreaterThanEquals ConditionType = "NumericGreaterThanEquals"
// Date condition operators.
CondDateEquals ConditionType = "DateEquals"
CondDateNotEquals ConditionType = "DateNotEquals"
CondDateLessThan ConditionType = "DateLessThan"
CondDateLessThanEquals ConditionType = "DateLessThanEquals"
CondDateGreaterThan ConditionType = "DateGreaterThan"
CondDateGreaterThanEquals ConditionType = "DateGreaterThanEquals"
// Bolean condition operators.
CondBool ConditionType = "Bool"
// IP address condition operators.
CondIPAddress ConditionType = "IpAddress"
CondNotIPAddress ConditionType = "NotIpAddress"
// ARN condition operators.
CondArnEquals ConditionType = "ArnEquals"
CondArnLike ConditionType = "ArnLike"
CondArnNotEquals ConditionType = "ArnNotEquals"
CondArnNotLike ConditionType = "ArnNotLike"
) )
func (c ConditionType) String() string { func (c *Condition) Match(obj Request) bool {
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 var val string
switch c.Object { switch c.Object {
case ObjectResource: case ObjectResource:
val = req.Resource().Property(c.Key) val = obj.Resource().Property(c.Key)
case ObjectRequest: case ObjectRequest:
val = req.Property(c.Key) val = obj.Property(c.Key)
default: default:
panic(fmt.Sprintf("unknown condition type: %d", c.Object)) return false
} }
switch c.Op { switch c.Op {
default: default:
panic(fmt.Sprintf("unimplemented: %d", c.Op)) panic(fmt.Sprintf("unimplemented: %s", c.Op))
case CondStringEquals: case CondStringEquals:
return val == c.Value return val == c.Value
case CondStringNotEquals: case CondStringNotEquals:
@ -163,21 +136,13 @@ func (c *Condition) Match(req Request) bool {
return globMatch(val, c.Value) return globMatch(val, c.Value)
case CondStringNotLike: case CondStringNotLike:
return !globMatch(val, c.Value) 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) { func (r *Rule) Match(req Request) (status Status, matched bool) {
found := len(r.Resources.Names) == 0 found := len(r.Resource) == 0
for i := range r.Resources.Names { for i := range r.Resource {
if globMatch(req.Resource().Name(), r.Resources.Names[i]) != r.Resources.Inverted { if globMatch(req.Resource().Name(), r.Resource[i]) {
found = true found = true
break break
} }
@ -185,8 +150,8 @@ func (r *Rule) Match(req Request) (status Status, matched bool) {
if !found { if !found {
return NoRuleFound, false return NoRuleFound, false
} }
for i := range r.Actions.Names { for i := range r.Action {
if globMatch(req.Operation(), r.Actions.Names[i]) != r.Actions.Inverted { if globMatch(req.Operation(), r.Action[i]) {
return r.matchCondition(req) return r.matchCondition(req)
} }
} }
@ -199,16 +164,15 @@ func (r *Rule) matchCondition(obj Request) (status Status, matched bool) {
} }
return r.matchAll(obj) return r.matchAll(obj)
} }
func (r *Rule) matchAny(obj Request) (status Status, matched bool) { func (r *Rule) matchAny(obj Request) (status Status, matched bool) {
for i := range r.Condition { for i := range r.Condition {
if r.Condition[i].Match(obj) { if r.Condition[i].Match(obj) {
return r.Status, true return r.Status, true
} }
} }
return NoRuleFound, false return NoRuleFound, false
} }
func (r *Rule) matchAll(obj Request) (status Status, matched bool) { func (r *Rule) matchAll(obj Request) (status Status, matched bool) {
for i := range r.Condition { for i := range r.Condition {
if !r.Condition[i].Match(obj) { if !r.Condition[i].Match(obj) {

View file

@ -11,10 +11,10 @@ func TestEncodeDecode(t *testing.T) {
Rules: []Rule{ Rules: []Rule{
{ {
Status: Allow, Status: Allow,
Actions: Actions{Names: []string{ Action: []string{
"native::PutObject", "native::PutObject",
}}, },
Resources: Resources{Names: []string{"*"}}, Resource: []string{"*"},
Condition: []Condition{ Condition: []Condition{
{ {
Op: CondStringEquals, Op: CondStringEquals,

View file

@ -2,7 +2,7 @@ package policyengine
import "fmt" import "fmt"
// Status is the status for policy application. // Status is the status for policy application
type Status byte type Status byte
const ( const (

View file

@ -1,236 +0,0 @@
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
}

View file

@ -1,306 +0,0 @@
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)
}
})
}

View file

@ -1,314 +0,0 @@
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
}

View file

@ -1,424 +0,0 @@
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)
}
})
}
}

View file

@ -20,7 +20,8 @@ func NewInMemory() CachedChainStorage {
} }
} }
// IsAllowed implements the Engine interface. // TODO параметры для actor (IP)
// TODO
func (s *inmemory) IsAllowed(name Name, namespace string, r Request) (Status, bool) { func (s *inmemory) IsAllowed(name Name, namespace string, r Request) (Status, bool) {
var ruleFound bool var ruleFound bool
if local, ok := s.local[name]; ok { if local, ok := s.local[name]; ok {
@ -73,34 +74,3 @@ func (s *inmemory) AddNameSpaceChain(name Name, namespace string, c *Chain) {
func (s *inmemory) AddOverride(name Name, c *Chain) { func (s *inmemory) AddOverride(name Name, c *Chain) {
s.local[name] = append(s.local[name], c) 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]
}

View file

@ -11,7 +11,6 @@ func TestInmemory(t *testing.T) {
object = "native::object::abc/xyz" object = "native::object::abc/xyz"
container = "native::object::abc/*" container = "native::object::abc/*"
namespace = "Tenant1" namespace = "Tenant1"
namespace2 = "Tenant2"
actor1 = "owner1" actor1 = "owner1"
actor2 = "owner2" actor2 = "owner2"
) )
@ -34,13 +33,13 @@ func TestInmemory(t *testing.T) {
Rules: []Rule{ Rules: []Rule{
{ // Restrict to remove ANY object from the namespace. { // Restrict to remove ANY object from the namespace.
Status: AccessDenied, Status: AccessDenied,
Actions: Actions{Names: []string{"native::object::delete"}}, Action: []string{"native::object::delete"},
Resources: Resources{Names: []string{"native::object::*"}}, Resource: []string{"native::object::*"},
}, },
{ // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise. { // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise.
Status: AccessDenied, Status: AccessDenied,
Actions: Actions{Names: []string{"native::object::put"}}, Action: []string{"native::object::put"},
Resources: Resources{Names: []string{"native::object::*"}}, Resource: []string{"native::object::*"},
Any: true, Any: true,
Condition: []Condition{ Condition: []Condition{
{ {
@ -60,22 +59,12 @@ func TestInmemory(t *testing.T) {
}, },
}) })
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{ s.AddResourceChain(Ingress, container, &Chain{
Rules: []Rule{ Rules: []Rule{
{ // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute. { // Allow to actor2 to get objects from the specific container only if they have `Department=HR` attribute.
Status: Allow, Status: Allow,
Actions: Actions{Names: []string{"native::object::get"}}, Action: []string{"native::object::get"},
Resources: Resources{Names: []string{"native::object::abc/*"}}, Resource: []string{"native::object::abc/*"},
Condition: []Condition{ Condition: []Condition{
{ {
Op: CondStringEquals, Op: CondStringEquals,
@ -142,22 +131,6 @@ func TestInmemory(t *testing.T) {
require.Equal(t, AccessDenied, status) require.Equal(t, AccessDenied, status)
require.True(t, ok) 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) { t.Run("good", func(t *testing.T) {
status, ok = s.IsAllowed(Ingress, namespace, reqGood) status, ok = s.IsAllowed(Ingress, namespace, reqGood)
require.Equal(t, NoRuleFound, status) require.Equal(t, NoRuleFound, status)
@ -167,8 +140,8 @@ func TestInmemory(t *testing.T) {
s.AddOverride(Ingress, &Chain{ s.AddOverride(Ingress, &Chain{
Rules: []Rule{{ Rules: []Rule{{
Status: QuotaLimitReached, Status: QuotaLimitReached,
Actions: Actions{Names: []string{"native::object::put"}}, Action: []string{"native::object::put"},
Resources: Resources{Names: []string{"native::object::cba/*"}}, Resource: []string{"native::object::cba/*"},
}}, }},
}) })
@ -180,8 +153,8 @@ func TestInmemory(t *testing.T) {
s.AddOverride(Ingress, &Chain{ s.AddOverride(Ingress, &Chain{
Rules: []Rule{{ Rules: []Rule{{
Status: QuotaLimitReached, Status: QuotaLimitReached,
Actions: Actions{Names: []string{"native::object::put"}}, Action: []string{"native::object::put"},
Resources: Resources{Names: []string{"native::object::abc/*"}}, Resource: []string{"native::object::abc/*"},
}}, }},
}) })

View file

@ -9,10 +9,4 @@ type CachedChainStorage interface {
AddNameSpaceChain(name Name, namespace string, c *Chain) AddNameSpaceChain(name Name, namespace string, c *Chain)
// Adds a local policy chain used for all operations with this service. // Adds a local policy chain used for all operations with this service.
AddOverride(name Name, c *Chain) 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
} }

30
policy.go Normal file
View file

@ -0,0 +1,30 @@
package policyengine
//{
// "Version": "xyz",
// "Policy": [
// {
// "Effect": "Allow",
// "Action": [
// "native:*",
// "s3:PutObject",
// "s3:GetObject"
// ],
// "Resource": ["*"],
// "Principal": ["did:frostfs:039e3ee771a223361fe7862f532e9511b57baaae3c3e2622682e99d0e660f7671"],
// "Condition": [ {"StringEquals": {"native::object::attribute", "iamuser-admin"}]
// }
// ]
//}
// type Policy struct {
// Rules []Rule `json:"Policy"`
// }
// type AWSRule struct {
// Effect string `json:"Effect"`
// Action []string `json:"Action"`
// Resource []string `json:"Resource"`
// Principal []string `json:"Principal"`
// Condition []Condition `json:"Condition"`
// }