forked from TrueCloudLab/policy-engine
[#2] Initial implementation
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
This commit is contained in:
parent
af4da29b1b
commit
5ebb2e694c
13 changed files with 664 additions and 0 deletions
184
chain.go
Normal file
184
chain.go
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Chain struct {
|
||||||
|
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.
|
||||||
|
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(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: %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(req.Operation(), r.Action[i]) {
|
||||||
|
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)
|
||||||
|
}
|
30
policy.go
Normal file
30
policy.go
Normal 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"`
|
||||||
|
// }
|
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