Initial implementation #2
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)
|
||||||
dstepanov-yadro marked this conversation as resolved
Outdated
|
|||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
dstepanov-yadro marked this conversation as resolved
Outdated
dstepanov-yadro
commented
obj Request -> req Request obj Request -> req Request
fyrchik
commented
fixed fixed
|
|||||||
|
|
||||||
|
switch c.Op {
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unimplemented: %s", c.Op))
|
||||||
|
case CondStringEquals:
|
||||||
|
return val == c.Value
|
||||||
|
case CondStringNotEquals:
|
||||||
|
return val != c.Value
|
||||||
dstepanov-yadro marked this conversation as resolved
Outdated
dstepanov-yadro
commented
Explain please why unknown Object leads to false, but unknown Op leads to panic? Explain please why unknown Object leads to false, but unknown Op leads to panic?
fyrchik
commented
Made panic for both. Panic is for the development stage, to avoid long debugging sessions after silent misbehaviour. Made panic for both. Panic is for the development stage, to avoid long debugging sessions after silent misbehaviour.
|
|||||||
|
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)
|
||||||
dkirillov marked this conversation as resolved
Outdated
dkirillov
commented
Should the args be in reverse order? Should the args be in reverse order?
`if globMatch(req.Operation(), r.Action[i]) {`
fyrchik
commented
Fixed Fixed
dkirillov
commented
Did you push the fix into and not to the branch from which this PR is opened https://git.frostfs.info/fyrchik/policy-engine/src/commit/3970569602d100fc47b9b0e51f55586820652f8b/chain.go#L154 ? Did you push the fix into `init` branch in the main repo https://git.frostfs.info/TrueCloudLab/policy-engine/src/commit/c1ac4ad957262ac4da2dc3bbfcb3b31028d71128/chain.go#L154 and not to the branch from which this PR is opened https://git.frostfs.info/fyrchik/policy-engine/src/commit/3970569602d100fc47b9b0e51f55586820652f8b/chain.go#L154 ?
fyrchik
commented
Yes, probably, fixed Yes, probably, fixed
|
|||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
dstepanov-yadro marked this conversation as resolved
Outdated
dstepanov-yadro
commented
NoRuleFound -> AccessDenied NoRuleFound -> AccessDenied
fyrchik
commented
Agree here, I have postponed having default DENY values for future, to make the result more explicit. Agree here, I have postponed having default DENY values for future, to make the result more explicit.
|
|||||||
|
|
||||||
|
func (c *Chain) Match(req Request) (status Status, matched bool) {
|
||||||
|
for i := range c.Rules {
|
||||||
|
status, matched := c.Rules[i].Match(req)
|
||||||
|
if matched {
|
||||||
dstepanov-yadro marked this conversation as resolved
Outdated
dstepanov-yadro
commented
NoRuleFound -> AccessDenied: NoRuleFound -> AccessDenied:
fyrchik
commented
There are multiple rules in the chain, There are multiple rules in the chain, `AccessDenied` implies one of the conditions has matched.
dstepanov-yadro
commented
Ah, ok Ah, ok
|
|||||||
|
return status, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NoRuleFound, false
|
||||||
|
}
|
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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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 {
|
||||||
dstepanov-yadro marked this conversation as resolved
Outdated
dstepanov-yadro
commented
I think interface can be replaced with struct, because Request looks like data-holder, not behavior. I think interface can be replaced with struct, because Request looks like data-holder, not behavior.
But it is up to you.
fyrchik
commented
It is an interfarce because of 2 reasons:
Maybe I don't understand, what is the exact struct you have in mind? It is an interfarce because of 2 reasons:
1. Multiple implementations are possible (S3 and gRPC protocol).
2. We do not want to depend on frostfs-api here (object) and we do not want to copy all objects attributes just to create this struct.
Maybe I don't understand, what is the exact struct you have in mind?
dstepanov-yadro
commented
Something like that:
I belive that both of s3-gw and node will go to the same implementation of this interface.
It is not required to copy everything Something like that:
```
type Request struct {
Operation string
Properties map[string]string
Resource Resource
}
```
> Multiple implementations are possible (S3 and gRPC protocol).
I belive that both of s3-gw and node will go to the same implementation of this interface.
> we do not want to copy all objects attributes
It is not required to copy everything
fyrchik
commented
I wanted to avoid allocating I wanted to avoid allocating `Properties` map as much as possible. Let's see the prototypes and figure out whether is is better.
fyrchik
commented
This is not a final implementation. This is not a final implementation.
|
|||||||
|
// 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
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
As far as i remember
gob
requires to register every type, but Chain contains interfaces. Is simple json not good enough?true, replaced