forked from TrueCloudLab/policy-engine
Compare commits
2 commits
master
...
feature/ia
Author | SHA1 | Date | |
---|---|---|---|
0933aa7ce6 | |||
3970569602 |
14 changed files with 933 additions and 0 deletions
193
chain.go
Normal file
193
chain.go
Normal file
|
@ -0,0 +1,193 @@
|
|||
package policyengine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"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)
|
||||
}
|
||||
|
||||
type Chain struct {
|
||||
Rules []Rule
|
||||
}
|
||||
|
||||
func init() {
|
||||
// FIXME #1 (@fyrchik): Introduce more optimal serialization format.
|
||||
gob.Register(Chain{})
|
||||
}
|
||||
|
||||
func (c *Chain) Bytes() []byte {
|
||||
b := bytes.NewBuffer(nil)
|
||||
e := gob.NewEncoder(b)
|
||||
if err := e.Encode(c); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func (c *Chain) DecodeBytes(b []byte) error {
|
||||
r := bytes.NewReader(b)
|
||||
d := gob.NewDecoder(r)
|
||||
return d.Decode(c)
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Status Status
|
||||
// Actions the operation is applied to.
|
||||
Action []string
|
||||
// List of the resources the operation is applied to.
|
||||
Resource []string
|
||||
// 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 Condition struct {
|
||||
Op ConditionType
|
||||
Object ObjectType
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type ObjectType byte
|
||||
|
||||
const (
|
||||
ObjectResource ObjectType = iota
|
||||
ObjectRequest
|
||||
ObjectActor
|
||||
)
|
||||
|
||||
// TODO @fyrchik: replace string with int-like type.
|
||||
type ConditionType string
|
||||
|
||||
// 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 = "StringEquals"
|
||||
CondStringNotEquals ConditionType = "StringNotEquals"
|
||||
CondStringEqualsIgnoreCase ConditionType = "StringEqualsIgnoreCase"
|
||||
CondStringNotEqualsIgnoreCase ConditionType = "StringNotEqualsIgnoreCase"
|
||||
CondStringLike ConditionType = "StringLike"
|
||||
CondStringNotLike ConditionType = "StringNotLike"
|
||||
|
||||
// Numeric condition operators.
|
||||
CondNumericEquals ConditionType = "NumericEquals"
|
||||
CondNumericNotEquals ConditionType = "NumericNotEquals"
|
||||
CondNumericLessThan ConditionType = "NumericLessThan"
|
||||
CondNumericLessThanEquals ConditionType = "NumericLessThanEquals"
|
||||
CondNumericGreaterThan ConditionType = "NumericGreaterThan"
|
||||
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 *Condition) Match(obj Request) bool {
|
||||
var val string
|
||||
switch c.Object {
|
||||
case ObjectResource:
|
||||
val = obj.Resource().Property(c.Key)
|
||||
case ObjectRequest:
|
||||
val = obj.Property(c.Key)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
switch c.Op {
|
||||
default:
|
||||
panic(fmt.Sprintf("unimplemented: %s", 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)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Rule) Match(req Request) (status Status, matched bool) {
|
||||
found := len(r.Resource) == 0
|
||||
for i := range r.Resource {
|
||||
if globMatch(req.Resource().Name(), r.Resource[i]) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return NoRuleFound, false
|
||||
}
|
||||
for i := range r.Action {
|
||||
if globMatch(r.Action[i], req.Operation()) {
|
||||
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,
|
||||
Action: []string{
|
||||
"native::PutObject",
|
||||
},
|
||||
Resource: []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=
|
76
inmemory.go
Normal file
76
inmemory.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
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),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO параметры для actor (IP)
|
||||
// TODO
|
||||
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)
|
||||
}
|
166
inmemory_test.go
Normal file
166
inmemory_test.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
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"
|
||||
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,
|
||||
Action: []string{"native::object::delete"},
|
||||
Resource: []string{"native::object::*"},
|
||||
},
|
||||
{ // Allow to put object only from the trusted subnet AND trusted actor, deny otherwise.
|
||||
Status: AccessDenied,
|
||||
Action: []string{"native::object::put"},
|
||||
Resource: []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.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,
|
||||
Action: []string{"native::object::get"},
|
||||
Resource: []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("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,
|
||||
Action: []string{"native::object::put"},
|
||||
Resource: []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,
|
||||
Action: []string{"native::object::put"},
|
||||
Resource: []string{"native::object::abc/*"},
|
||||
}},
|
||||
})
|
||||
|
||||
status, ok = s.IsAllowed(Ingress, namespace, reqGood)
|
||||
require.Equal(t, QuotaLimitReached, status)
|
||||
require.True(t, ok)
|
||||
})
|
||||
})
|
||||
}
|
12
interface.go
Normal file
12
interface.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
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)
|
||||
}
|
139
policy.go
Normal file
139
policy.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package policyengine
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type (
|
||||
// IAMPolicy grammar https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
|
||||
IAMPolicy struct {
|
||||
Version string `json:"Version,omitempty"`
|
||||
ID string `json:"Id,omitempty"`
|
||||
Statement IAMStatements `json:"Statement"`
|
||||
}
|
||||
|
||||
IAMStatements []IAMStatement
|
||||
|
||||
IAMStatement struct {
|
||||
SID string `json:"Sid,omitempty"`
|
||||
Principal IAMPrincipal `json:"Principal,omitempty"`
|
||||
Effect IAMEffect `json:"Effect"`
|
||||
Action IAMAction `json:"Action"`
|
||||
Resource IAMResource `json:"Resource"`
|
||||
Condition IAMCondition `json:"Condition,omitempty"`
|
||||
}
|
||||
|
||||
IAMPrincipal map[string][]string
|
||||
|
||||
IAMEffect string
|
||||
|
||||
IAMAction []string
|
||||
|
||||
IAMResource []string
|
||||
|
||||
IAMCondition map[string]map[string]string
|
||||
)
|
||||
|
||||
const IAMWildcard = "*"
|
||||
|
||||
const (
|
||||
IAMAllowEffect IAMEffect = "Allow"
|
||||
IAMDenyEffect IAMEffect = "Deny"
|
||||
)
|
||||
|
||||
func (s *IAMStatements) UnmarshalJSON(data []byte) error {
|
||||
var list []IAMStatement
|
||||
if err := json.Unmarshal(data, &list); err == nil {
|
||||
*s = list
|
||||
return nil
|
||||
}
|
||||
|
||||
var elem IAMStatement
|
||||
if err := json.Unmarshal(data, &elem); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*s = []IAMStatement{elem}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *IAMPrincipal) UnmarshalJSON(data []byte) error {
|
||||
*p = make(IAMPrincipal)
|
||||
|
||||
var str string
|
||||
|
||||
if err := json.Unmarshal(data, &str); err == nil {
|
||||
if str != IAMWildcard {
|
||||
return errors.New("invalid IAM string principal")
|
||||
}
|
||||
(*p)[IAMWildcard] = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
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.([]interface{})
|
||||
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 *IAMAction) 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 *IAMResource) 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
|
||||
}
|
151
policy_test.go
Normal file
151
policy_test.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package policyengine
|
||||
|
||||
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 := IAMPolicy{
|
||||
Version: "2012-10-17",
|
||||
ID: "PutObjPolicy",
|
||||
Statement: []IAMStatement{{
|
||||
SID: "DenyObjectsThatAreNotSSEKMS",
|
||||
Principal: map[string][]string{
|
||||
"*": nil,
|
||||
},
|
||||
Effect: IAMDenyEffect,
|
||||
Action: []string{"s3:PutObject"},
|
||||
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||
Condition: map[string]map[string]string{
|
||||
"Null": {
|
||||
"s3:x-amz-server-side-encryption-aws-kms-key-id": "true",
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
var p IAMPolicy
|
||||
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 := IAMPolicy{
|
||||
Version: "2012-10-17",
|
||||
Statement: []IAMStatement{{
|
||||
Principal: map[string][]string{
|
||||
"AWS": {"arn:aws:iam::111122223333:user/JohnDoe"},
|
||||
},
|
||||
Effect: IAMAllowEffect,
|
||||
Action: []string{"s3:PutObject"},
|
||||
Resource: []string{"arn:aws:s3:::DOC-EXAMPLE-BUCKET/*"},
|
||||
Condition: map[string]map[string]string{
|
||||
"StringEquals": {
|
||||
"s3:RequestObjectTag/Department": "Finance",
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
var p IAMPolicy
|
||||
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 := IAMPolicy{
|
||||
Statement: []IAMStatement{{
|
||||
Principal: map[string][]string{
|
||||
"AWS": {"arn:aws:iam::111122223333:user/JohnDoe"},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
var p IAMPolicy
|
||||
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 IAMPolicy
|
||||
err := json.Unmarshal([]byte(policy), &p)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
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…
Reference in a new issue