Use access policy engine to permit PUT request #770
25 changed files with 1180 additions and 10 deletions
90
cmd/frostfs-cli/modules/control/add_rule.go
Normal file
90
cmd/frostfs-cli/modules/control/add_rule.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
ape "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
ruleFlag = "rule"
|
||||
)
|
||||
|
||||
var addRuleCmd = &cobra.Command{
|
||||
Use: "add-rule",
|
||||
Short: "Add local override",
|
||||
Long: "Add local APE rule to a node with following format:\n<action>[:action_detail] <operation> [<condition1> ...] <resource>",
|
||||
Example: `allow Object.Get *
|
||||
deny Object.Get EbxzAdz5LB4uqxuz6crWKAumBNtZyK2rKsqQP7TdZvwr/*
|
||||
deny:QuotaLimitReached Object.Put Object.Resource:Department=HR *
|
||||
`,
|
||||
Run: addRule,
|
||||
}
|
||||
|
||||
func prettyJSONFormat(cmd *cobra.Command, serializedChain []byte) string {
|
||||
wr := bytes.NewBufferString("")
|
||||
err := json.Indent(wr, serializedChain, "", " ")
|
||||
commonCmd.ExitOnErr(cmd, "%w", err)
|
||||
return wr.String()
|
||||
}
|
||||
|
||||
func addRule(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
var cnr cid.ID
|
||||
cidStr, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
commonCmd.ExitOnErr(cmd, "can't decode container ID: %w", cnr.DecodeString(cidStr))
|
||||
|
||||
rawCID := make([]byte, sha256.Size)
|
||||
cnr.Encode(rawCID)
|
||||
|
||||
rule, _ := cmd.Flags().GetString(ruleFlag)
|
||||
|
||||
chain := new(ape.Chain)
|
||||
commonCmd.ExitOnErr(cmd, "parser error: %w", util.ParseAPEChain(chain, []string{rule}))
|
||||
serializedChain := chain.Bytes()
|
||||
|
||||
cmd.Println("Container ID: " + cidStr)
|
||||
cmd.Println("Parsed chain:\n" + prettyJSONFormat(cmd, serializedChain))
|
||||
|
||||
req := &control.AddChainLocalOverrideRequest{
|
||||
Body: &control.AddChainLocalOverrideRequest_Body{
|
||||
ContainerId: rawCID,
|
||||
Chain: serializedChain,
|
||||
},
|
||||
}
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.AddChainLocalOverrideResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *client.Client) error {
|
||||
resp, err = control.AddChainLocalOverride(client, req)
|
||||
return err
|
||||
})
|
||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
cmd.Println("Rule has been added. Chain id: ", resp.GetBody().GetChainId())
|
||||
}
|
||||
|
||||
func initControlAddRuleCmd() {
|
||||
initControlFlags(addRuleCmd)
|
||||
|
||||
ff := addRuleCmd.Flags()
|
||||
ff.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
ff.String(ruleFlag, "", "Rule statement")
|
||||
}
|
69
cmd/frostfs-cli/modules/control/get_rule.go
Normal file
69
cmd/frostfs-cli/modules/control/get_rule.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getRuleCmd = &cobra.Command{
|
||||
Use: "get-rule",
|
||||
Short: "Get local override",
|
||||
Long: "Get local APE override of the node",
|
||||
Run: getRule,
|
||||
}
|
||||
|
||||
func getRule(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
var cnr cid.ID
|
||||
cidStr, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
commonCmd.ExitOnErr(cmd, "can't decode container ID: %w", cnr.DecodeString(cidStr))
|
||||
|
||||
rawCID := make([]byte, sha256.Size)
|
||||
cnr.Encode(rawCID)
|
||||
|
||||
chainID, _ := cmd.Flags().GetString(chainIDFlag)
|
||||
|
||||
req := &control.GetChainLocalOverrideRequest{
|
||||
Body: &control.GetChainLocalOverrideRequest_Body{
|
||||
ContainerId: rawCID,
|
||||
ChainId: chainID,
|
||||
},
|
||||
}
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.GetChainLocalOverrideResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *client.Client) error {
|
||||
resp, err = control.GetChainLocalOverride(client, req)
|
||||
return err
|
||||
})
|
||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
var chain policyengine.Chain
|
||||
commonCmd.ExitOnErr(cmd, "decode error: %w", chain.DecodeBytes(resp.GetBody().GetChain()))
|
||||
|
||||
// TODO (aarifullin): make pretty-formatted output for chains.
|
||||
cmd.Println("Parsed chain:\n" + prettyJSONFormat(cmd, chain.Bytes()))
|
||||
}
|
||||
|
||||
func initControGetRuleCmd() {
|
||||
initControlFlags(getRuleCmd)
|
||||
|
||||
ff := getRuleCmd.Flags()
|
||||
ff.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
ff.String(chainIDFlag, "", "Chain id")
|
||||
}
|
72
cmd/frostfs-cli/modules/control/list_rules.go
Normal file
72
cmd/frostfs-cli/modules/control/list_rules.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var listRulesCmd = &cobra.Command{
|
||||
Use: "list-rules",
|
||||
Short: "List local overrides",
|
||||
Long: "List local APE overrides of the node",
|
||||
Run: listRules,
|
||||
}
|
||||
|
||||
func listRules(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
var cnr cid.ID
|
||||
cidStr, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
commonCmd.ExitOnErr(cmd, "can't decode container ID: %w", cnr.DecodeString(cidStr))
|
||||
|
||||
rawCID := make([]byte, sha256.Size)
|
||||
cnr.Encode(rawCID)
|
||||
|
||||
req := &control.ListChainLocalOverridesRequest{
|
||||
Body: &control.ListChainLocalOverridesRequest_Body{
|
||||
ContainerId: rawCID,
|
||||
},
|
||||
}
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.ListChainLocalOverridesResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *client.Client) error {
|
||||
resp, err = control.ListChainLocalOverrides(client, req)
|
||||
return err
|
||||
})
|
||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
chains := resp.GetBody().GetChains()
|
||||
if len(chains) == 0 {
|
||||
cmd.Println("Local overrides are not defined for the container.")
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range chains {
|
||||
// TODO (aarifullin): make pretty-formatted output for chains.
|
||||
var chain policyengine.Chain
|
||||
commonCmd.ExitOnErr(cmd, "decode error: %w", chain.DecodeBytes(c))
|
||||
cmd.Println("Parsed chain:\n" + prettyJSONFormat(cmd, chain.Bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
func initControlListRulesCmd() {
|
||||
initControlFlags(listRulesCmd)
|
||||
|
||||
ff := listRulesCmd.Flags()
|
||||
ff.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
}
|
72
cmd/frostfs-cli/modules/control/remove_rule.go
Normal file
72
cmd/frostfs-cli/modules/control/remove_rule.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
chainIDFlag = "chain-id"
|
||||
)
|
||||
|
||||
var removeRuleCmd = &cobra.Command{
|
||||
Use: "remove-rule",
|
||||
Short: "Remove local override",
|
||||
Long: "Remove local APE override of the node",
|
||||
Run: removeRule,
|
||||
}
|
||||
|
||||
func removeRule(cmd *cobra.Command, _ []string) {
|
||||
pk := key.Get(cmd)
|
||||
|
||||
var cnr cid.ID
|
||||
cidStr, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||
commonCmd.ExitOnErr(cmd, "can't decode container ID: %w", cnr.DecodeString(cidStr))
|
||||
|
||||
rawCID := make([]byte, sha256.Size)
|
||||
cnr.Encode(rawCID)
|
||||
|
||||
chainID, _ := cmd.Flags().GetString(chainIDFlag)
|
||||
|
||||
req := &control.RemoveChainLocalOverrideRequest{
|
||||
Body: &control.RemoveChainLocalOverrideRequest_Body{
|
||||
ContainerId: rawCID,
|
||||
ChainId: chainID,
|
||||
},
|
||||
}
|
||||
|
||||
signRequest(cmd, pk, req)
|
||||
|
||||
cli := getClient(cmd, pk)
|
||||
|
||||
var resp *control.RemoveChainLocalOverrideResponse
|
||||
var err error
|
||||
err = cli.ExecRaw(func(client *client.Client) error {
|
||||
resp, err = control.RemoveChainLocalOverride(client, req)
|
||||
return err
|
||||
})
|
||||
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
|
||||
|
||||
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
|
||||
|
||||
if resp.GetBody().Removed {
|
||||
cmd.Println("Rule has been removed.")
|
||||
} else {
|
||||
cmd.Println("Rule has not been removed.")
|
||||
}
|
||||
}
|
||||
|
||||
func initControlRemoveRuleCmd() {
|
||||
initControlFlags(removeRuleCmd)
|
||||
|
||||
ff := removeRuleCmd.Flags()
|
||||
ff.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
|
||||
ff.String(chainIDFlag, "", "Chain id")
|
||||
}
|
|
@ -34,6 +34,10 @@ func init() {
|
|||
shardsCmd,
|
||||
synchronizeTreeCmd,
|
||||
irCmd,
|
||||
addRuleCmd,
|
||||
removeRuleCmd,
|
||||
listRulesCmd,
|
||||
getRuleCmd,
|
||||
)
|
||||
|
||||
initControlHealthCheckCmd()
|
||||
|
@ -42,4 +46,8 @@ func init() {
|
|||
initControlShardsCmd()
|
||||
initControlSynchronizeTreeCmd()
|
||||
initControlIRCmd()
|
||||
initControlAddRuleCmd()
|
||||
initControlRemoveRuleCmd()
|
||||
initControlListRulesCmd()
|
||||
initControGetRuleCmd()
|
||||
}
|
||||
|
|
179
cmd/frostfs-cli/modules/util/ape.go
Normal file
179
cmd/frostfs-cli/modules/util/ape.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
"github.com/flynn-archive/go-shlex"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidStatementFormat = errors.New("invalid statement format")
|
||||
errInvalidConditionFormat = errors.New("invalid condition format")
|
||||
errUnknownAction = errors.New("action is not recognized")
|
||||
errUnknownOperation = errors.New("operation is not recognized")
|
||||
errUnknownActionDetail = errors.New("action detail is not recognized")
|
||||
errUnknownBinaryOperator = errors.New("binary operator is not recognized")
|
||||
errUnknownCondObjectType = errors.New("condition object type is not recognized")
|
||||
)
|
||||
|
||||
// ParseAPEChain parses APE chain rules.
|
||||
func ParseAPEChain(chain *policyengine.Chain, rules []string) error {
|
||||
if len(rules) == 0 {
|
||||
return errors.New("no APE rules provided")
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
r := new(policyengine.Rule)
|
||||
if err := ParseAPERule(r, rule); err != nil {
|
||||
return err
|
||||
}
|
||||
chain.Rules = append(chain.Rules, *r)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseAPERule parses access-policy-engine statement from the following form:
|
||||
// <action>[:action_detail] <operation> [<condition1> ...] <resource>
|
||||
//
|
||||
// Examples:
|
||||
// deny Object.Put *
|
||||
// deny:QuotaLimitReached Object.Put *
|
||||
// allow Object.Put *
|
||||
// allow Object.Get Object.Resource:Department=HR Object.Request:Actor=ownerA *
|
||||
//
|
||||
//nolint:godot
|
||||
func ParseAPERule(r *policyengine.Rule, rule string) error {
|
||||
lexemes, err := shlex.Split(rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't parse rule '%s': %v", rule, err)
|
||||
}
|
||||
return parseRuleLexemes(r, lexemes)
|
||||
}
|
||||
|
||||
func parseRuleLexemes(r *policyengine.Rule, lexemes []string) error {
|
||||
if len(lexemes) < 2 {
|
||||
return errInvalidStatementFormat
|
||||
}
|
||||
|
||||
var err error
|
||||
r.Status, err = parseStatus(lexemes[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Action, err = parseAction(lexemes[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Condition, err = parseConditions(lexemes[2 : len(lexemes)-1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Resource, err = parseResource(lexemes[len(lexemes)-1])
|
||||
return err
|
||||
}
|
||||
|
||||
func parseStatus(lexeme string) (policyengine.Status, error) {
|
||||
action, expression, found := strings.Cut(lexeme, ":")
|
||||
switch action = strings.ToLower(action); action {
|
||||
case "deny":
|
||||
if !found {
|
||||
return policyengine.AccessDenied, nil
|
||||
} else if strings.EqualFold(expression, "QuotaLimitReached") {
|
||||
return policyengine.QuotaLimitReached, nil
|
||||
} else {
|
||||
return 0, fmt.Errorf("%w: %s", errUnknownActionDetail, expression)
|
||||
}
|
||||
case "allow":
|
||||
if found {
|
||||
return 0, errUnknownActionDetail
|
||||
}
|
||||
return policyengine.Allow, nil
|
||||
default:
|
||||
return 0, errUnknownAction
|
||||
}
|
||||
}
|
||||
|
||||
func parseAction(lexeme string) ([]string, error) {
|
||||
switch strings.ToLower(lexeme) {
|
||||
case "object.put":
|
||||
return []string{"native:PutObject"}, nil
|
||||
case "object.get":
|
||||
return []string{"native:GetObject"}, nil
|
||||
case "object.head":
|
||||
return []string{"native:HeadObject"}, nil
|
||||
case "object.delete":
|
||||
return []string{"native:DeleteObject"}, nil
|
||||
case "object.search":
|
||||
return []string{"native:SearchObject"}, nil
|
||||
case "object.range":
|
||||
return []string{"native:RangeObject"}, nil
|
||||
case "object.hash":
|
||||
return []string{"native:HashObject"}, nil
|
||||
default:
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", errUnknownOperation, lexeme)
|
||||
}
|
||||
|
||||
func parseResource(lexeme string) ([]string, error) {
|
||||
return []string{fmt.Sprintf("native:::object/%s", lexeme)}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
ObjectResource = "object.resource"
|
||||
ObjectRequest = "object.request"
|
||||
ObjectActor = "object.actor"
|
||||
)
|
||||
|
||||
var typeToCondObject = map[string]policyengine.ObjectType{
|
||||
ObjectResource: policyengine.ObjectResource,
|
||||
ObjectRequest: policyengine.ObjectRequest,
|
||||
ObjectActor: policyengine.ObjectActor,
|
||||
}
|
||||
|
||||
func parseConditions(lexemes []string) ([]policyengine.Condition, error) {
|
||||
conds := make([]policyengine.Condition, 0)
|
||||
|
||||
for _, lexeme := range lexemes {
|
||||
typ, expression, found := strings.Cut(lexeme, ":")
|
||||
typ = strings.ToLower(typ)
|
||||
|
||||
objType, ok := typeToCondObject[typ]
|
||||
if ok {
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%w: %s", errInvalidConditionFormat, lexeme)
|
||||
}
|
||||
|
||||
var lhs, rhs string
|
||||
var binExpFound bool
|
||||
|
||||
var cond policyengine.Condition
|
||||
cond.Object = objType
|
||||
|
||||
lhs, rhs, binExpFound = strings.Cut(expression, "!=")
|
||||
if !binExpFound {
|
||||
lhs, rhs, binExpFound = strings.Cut(expression, "=")
|
||||
if !binExpFound {
|
||||
return nil, fmt.Errorf("%w: %s", errUnknownBinaryOperator, expression)
|
||||
}
|
||||
cond.Op = policyengine.CondStringEquals
|
||||
} else {
|
||||
cond.Op = policyengine.CondStringNotEquals
|
||||
}
|
||||
|
||||
cond.Key, cond.Value = lhs, rhs
|
||||
|
||||
conds = append(conds, cond)
|
||||
} else {
|
||||
return nil, fmt.Errorf("%w: %s", errUnknownCondObjectType, typ)
|
||||
}
|
||||
}
|
||||
|
||||
return conds, nil
|
||||
}
|
129
cmd/frostfs-cli/modules/util/ape_test.go
Normal file
129
cmd/frostfs-cli/modules/util/ape_test.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseAPERule(t *testing.T) {
|
||||
tests := [...]struct {
|
||||
name string
|
||||
rule string
|
||||
expectErr error
|
||||
expectRule policyengine.Rule
|
||||
}{
|
||||
{
|
||||
name: "Valid allow rule",
|
||||
rule: "allow Object.Put *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.Allow,
|
||||
Action: []string{"native:PutObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid deny rule",
|
||||
rule: "deny Object.Put *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.AccessDenied,
|
||||
Action: []string{"native:PutObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid deny rule with action detail",
|
||||
rule: "deny:QuotaLimitReached Object.Put *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.QuotaLimitReached,
|
||||
Action: []string{"native:PutObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid allow rule with conditions",
|
||||
rule: "allow Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.Allow,
|
||||
Action: []string{"native:GetObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{
|
||||
{
|
||||
Op: policyengine.CondStringEquals,
|
||||
Object: policyengine.ObjectResource,
|
||||
Key: "Department",
|
||||
Value: "HR",
|
||||
},
|
||||
{
|
||||
Op: policyengine.CondStringNotEquals,
|
||||
Object: policyengine.ObjectRequest,
|
||||
Key: "Actor",
|
||||
Value: "ownerA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Valid rule with conditions with action detail",
|
||||
rule: "deny:QuotaLimitReached Object.Get Object.Resource:Department=HR Object.Request:Actor!=ownerA *",
|
||||
expectRule: policyengine.Rule{
|
||||
Status: policyengine.QuotaLimitReached,
|
||||
Action: []string{"native:GetObject"},
|
||||
Resource: []string{"native:::object/*"},
|
||||
Condition: []policyengine.Condition{
|
||||
{
|
||||
Op: policyengine.CondStringEquals,
|
||||
Object: policyengine.ObjectResource,
|
||||
Key: "Department",
|
||||
Value: "HR",
|
||||
},
|
||||
{
|
||||
Op: policyengine.CondStringNotEquals,
|
||||
Object: policyengine.ObjectRequest,
|
||||
Key: "Actor",
|
||||
Value: "ownerA",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown action",
|
||||
rule: "permit Object.Put *",
|
||||
expectErr: errUnknownAction,
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown operation",
|
||||
rule: "allow Object.PutOut *",
|
||||
expectErr: errUnknownOperation,
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown action detail",
|
||||
rule: "deny:UnknownActionDetail Object.Put *",
|
||||
expectErr: errUnknownActionDetail,
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown condition binary operator",
|
||||
rule: "deny Object.Put Object.Resource:Department<HR *",
|
||||
expectErr: errUnknownBinaryOperator,
|
||||
},
|
||||
{
|
||||
name: "Invalid rule with unknown condition object type",
|
||||
rule: "deny Object.Put Object.ResourZe:Department=HR *",
|
||||
expectErr: errUnknownCondObjectType,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
r := new(policyengine.Rule)
|
||||
err := ParseAPERule(r, test.rule)
|
||||
require.ErrorIs(t, err, test.expectErr)
|
||||
if test.expectErr == nil {
|
||||
require.Equal(t, test.expectRule, *r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -510,6 +510,11 @@ type cfgObject struct {
|
|||
|
||||
eaclSource container.EACLSource
|
||||
|
||||
// Access policy chain source is used by object service to
|
||||
// check for operation permissions but this source is also shared with
|
||||
// control service that dispatches local overrides.
|
||||
apeChainSource container.AccessPolicyEngineChainSource
|
||||
|
||||
pool cfgObjectRoutines
|
||||
|
||||
cfgLocalStorage cfgLocalStorage
|
||||
|
|
|
@ -51,6 +51,7 @@ func initControlService(c *cfg) {
|
|||
controlSvc.WithTreeService(treeSynchronizer{
|
||||
c.treeService,
|
||||
}),
|
||||
controlSvc.WithAPEChainSource(c.cfgObject.apeChainSource),
|
||||
)
|
||||
|
||||
lis, err := net.Listen("tcp", endpoint)
|
||||
|
|
|
@ -157,6 +157,8 @@ func initObjectService(c *cfg) {
|
|||
|
||||
c.replicator = createReplicator(c, keyStorage, c.bgClientCache)
|
||||
|
||||
c.cfgObject.apeChainSource = NewAPESource()
|
||||
|
||||
addPolicer(c, keyStorage, c.bgClientCache)
|
||||
|
||||
traverseGen := util.NewTraverserGenerator(c.netMapSource, c.cfgObject.cnrSource, c)
|
||||
|
@ -424,6 +426,7 @@ func createACLServiceV2(c *cfg, splitSvc *objectService.TransportSplitter, irFet
|
|||
c.cfgObject.eaclSource,
|
||||
eaclSDK.NewValidator(),
|
||||
ls),
|
||||
acl.NewAPEChecker(c.log, c.cfgObject.apeChainSource),
|
||||
c.cfgObject.cnrSource,
|
||||
v2.WithLogger(c.log),
|
||||
)
|
||||
|
|
34
cmd/frostfs-node/policy_engine.go
Normal file
34
cmd/frostfs-node/policy_engine.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
)
|
||||
|
||||
type apeChainSourceImpl struct {
|
||||
mtx sync.Mutex
|
||||
localChainStorage map[cid.ID]policyengine.CachedChainStorage
|
||||
}
|
||||
|
||||
func NewAPESource() container.AccessPolicyEngineChainSource {
|
||||
return &apeChainSourceImpl{
|
||||
localChainStorage: make(map[cid.ID]policyengine.CachedChainStorage),
|
||||
}
|
||||
}
|
||||
|
||||
var _ container.AccessPolicyEngineChainSource = (*apeChainSourceImpl)(nil)
|
||||
|
||||
func (c *apeChainSourceImpl) GetChainSource(cid cid.ID) (policyengine.CachedChainStorage, error) {
|
||||
c.mtx.Lock()
|
||||
defer c.mtx.Unlock()
|
||||
|
||||
s, ok := c.localChainStorage[cid]
|
||||
if ok {
|
||||
return s, nil
|
||||
}
|
||||
c.localChainStorage[cid] = policyengine.NewInMemory()
|
||||
return c.localChainStorage[cid], nil
|
||||
}
|
1
go.mod
1
go.mod
|
@ -8,6 +8,7 @@ require (
|
|||
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20230531082742-c97d21411eb6
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231101144515-6fbe1595cb3d
|
||||
git.frostfs.info/TrueCloudLab/hrw v1.2.1
|
||||
git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20231101082425-5eee1a733432
|
||||
git.frostfs.info/TrueCloudLab/tzhash v1.8.0
|
||||
github.com/cheggaaa/pb v1.0.29
|
||||
github.com/chzyer/readline v1.5.1
|
||||
|
|
BIN
go.sum
BIN
go.sum
Binary file not shown.
|
@ -6,6 +6,7 @@ import (
|
|||
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
)
|
||||
|
||||
// Container groups information about the FrostFS container stored in the FrostFS network.
|
||||
|
@ -70,3 +71,10 @@ type EACLSource interface {
|
|||
// eACL table is not in source.
|
||||
GetEACL(cid.ID) (*EACL, error)
|
||||
}
|
||||
|
||||
// AccessPolicyEngineChainSource interface provides methods to access and manipulate
|
||||
// policy engine chain storage.
|
||||
type AccessPolicyEngineChainSource interface {
|
||||
// TODO (aarifullin): Better to use simpler interface instead CachedChainStorage.
|
||||
GetChainSource(cid cid.ID) (policyengine.CachedChainStorage, error)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@ const (
|
|||
rpcStopShardEvacuation = "StopShardEvacuation"
|
||||
rpcFlushCache = "FlushCache"
|
||||
rpcDoctor = "Doctor"
|
||||
rpcAddChainLocalOverride = "AddChainLocalOverride"
|
||||
rpcGetChainLocalOverride = "GetChainLocalOverride"
|
||||
rpcListChainLocalOverrides = "ListChainLocalOverrides"
|
||||
rpcRemoveChainLocalOverride = "RemoveChainLocalOverride"
|
||||
)
|
||||
|
||||
// HealthCheck executes ControlService.HealthCheck RPC.
|
||||
|
@ -208,3 +212,55 @@ func Doctor(cli *client.Client, req *DoctorRequest, opts ...client.CallOption) (
|
|||
|
||||
return wResp.message, nil
|
||||
}
|
||||
|
||||
// AddChainLocalOverride executes ControlService.AddChainLocalOverride RPC.
|
||||
func AddChainLocalOverride(cli *client.Client, req *AddChainLocalOverrideRequest, opts ...client.CallOption) (*AddChainLocalOverrideResponse, error) {
|
||||
wResp := newResponseWrapper[AddChainLocalOverrideResponse]()
|
||||
wReq := &requestWrapper{m: req}
|
||||
|
||||
err := client.SendUnary(cli, common.CallMethodInfoUnary(serviceName, rpcAddChainLocalOverride), wReq, wResp, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wResp.message, nil
|
||||
}
|
||||
|
||||
// ListChainLocalOverrides executes ControlService.ListChainLocalOverrides RPC.
|
||||
func ListChainLocalOverrides(cli *client.Client, req *ListChainLocalOverridesRequest, opts ...client.CallOption) (*ListChainLocalOverridesResponse, error) {
|
||||
wResp := newResponseWrapper[ListChainLocalOverridesResponse]()
|
||||
wReq := &requestWrapper{m: req}
|
||||
|
||||
err := client.SendUnary(cli, common.CallMethodInfoUnary(serviceName, rpcListChainLocalOverrides), wReq, wResp, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wResp.message, nil
|
||||
}
|
||||
|
||||
// RemoveChainLocalOverride executes ControlService.RemoveChainLocalOverride RPC.
|
||||
func GetChainLocalOverride(cli *client.Client, req *GetChainLocalOverrideRequest, opts ...client.CallOption) (*GetChainLocalOverrideResponse, error) {
|
||||
wResp := newResponseWrapper[GetChainLocalOverrideResponse]()
|
||||
wReq := &requestWrapper{m: req}
|
||||
|
||||
err := client.SendUnary(cli, common.CallMethodInfoUnary(serviceName, rpcGetChainLocalOverride), wReq, wResp, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wResp.message, nil
|
||||
}
|
||||
|
||||
// RemoveChainLocalOverride executes ControlService.RemoveChainLocalOverride RPC.
|
||||
func RemoveChainLocalOverride(cli *client.Client, req *RemoveChainLocalOverrideRequest, opts ...client.CallOption) (*RemoveChainLocalOverrideResponse, error) {
|
||||
wResp := newResponseWrapper[RemoveChainLocalOverrideResponse]()
|
||||
wReq := &requestWrapper{m: req}
|
||||
|
||||
err := client.SendUnary(cli, common.CallMethodInfoUnary(serviceName, rpcRemoveChainLocalOverride), wReq, wResp, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return wResp.message, nil
|
||||
}
|
||||
|
|
148
pkg/services/control/server/policy_engine.go
Normal file
148
pkg/services/control/server/policy_engine.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package control
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (s *Server) AddChainLocalOverride(_ context.Context, req *control.AddChainLocalOverrideRequest) (*control.AddChainLocalOverrideResponse, error) {
|
||||
if err := s.isValidRequest(req); err != nil {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
|
||||
var cid cid.ID
|
||||
err := cid.Decode(req.GetBody().GetContainerId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
var chain policyengine.Chain
|
||||
if err = chain.DecodeBytes(req.GetBody().GetChain()); err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
src, err := s.apeChainSrc.GetChainSource(cid)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
s.apeChainCounter.Add(1)
|
||||
// TODO (aarifullin): the such chain id is not well-designed yet.
|
||||
chain.ID = policyengine.ChainID(fmt.Sprintf("%s:%d", policyengine.Ingress, s.apeChainCounter.Load()))
|
||||
|
||||
src.AddOverride(policyengine.Ingress, &chain)
|
||||
|
||||
resp := &control.AddChainLocalOverrideResponse{
|
||||
Body: &control.AddChainLocalOverrideResponse_Body{
|
||||
ChainId: string(chain.ID),
|
||||
},
|
||||
}
|
||||
err = SignMessage(s.key, resp)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) GetChainLocalOverride(_ context.Context, req *control.GetChainLocalOverrideRequest) (*control.GetChainLocalOverrideResponse, error) {
|
||||
if err := s.isValidRequest(req); err != nil {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
|
||||
var cid cid.ID
|
||||
err := cid.Decode(req.GetBody().GetContainerId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
src, err := s.apeChainSrc.GetChainSource(cid)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
chain, found := src.GetOverride(policyengine.Ingress, policyengine.ChainID(req.GetBody().GetChainId()))
|
||||
if !found {
|
||||
err = fmt.Errorf("local override has not been found")
|
||||
return nil, status.Error(codes.NotFound, err.Error())
|
||||
}
|
||||
|
||||
resp := &control.GetChainLocalOverrideResponse{
|
||||
Body: &control.GetChainLocalOverrideResponse_Body{
|
||||
Chain: chain.Bytes(),
|
||||
},
|
||||
}
|
||||
err = SignMessage(s.key, resp)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) ListChainLocalOverrides(_ context.Context, req *control.ListChainLocalOverridesRequest) (*control.ListChainLocalOverridesResponse, error) {
|
||||
if err := s.isValidRequest(req); err != nil {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
|
||||
var cid cid.ID
|
||||
err := cid.Decode(req.GetBody().GetContainerId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
src, err := s.apeChainSrc.GetChainSource(cid)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
chains := src.ListOverrides(policyengine.Ingress)
|
||||
serializedChains := make([][]byte, 0, len(chains))
|
||||
for _, chain := range chains {
|
||||
serializedChains = append(serializedChains, chain.Bytes())
|
||||
}
|
||||
|
||||
resp := &control.ListChainLocalOverridesResponse{
|
||||
Body: &control.ListChainLocalOverridesResponse_Body{
|
||||
Chains: serializedChains,
|
||||
},
|
||||
}
|
||||
err = SignMessage(s.key, resp)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) RemoveChainLocalOverride(_ context.Context, req *control.RemoveChainLocalOverrideRequest) (*control.RemoveChainLocalOverrideResponse, error) {
|
||||
if err := s.isValidRequest(req); err != nil {
|
||||
return nil, status.Error(codes.PermissionDenied, err.Error())
|
||||
}
|
||||
|
||||
var cid cid.ID
|
||||
err := cid.Decode(req.GetBody().GetContainerId())
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
|
||||
src, err := s.apeChainSrc.GetChainSource(cid)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
|
||||
removed := src.RemoveOverride(policyengine.Ingress, policyengine.ChainID(req.GetBody().GetChainId()))
|
||||
resp := &control.RemoveChainLocalOverrideResponse{
|
||||
Body: &control.RemoveChainLocalOverrideResponse_Body{
|
||||
Removed: removed,
|
||||
},
|
||||
}
|
||||
err = SignMessage(s.key, resp)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, err.Error())
|
||||
}
|
||||
return resp, nil
|
||||
}
|
|
@ -2,6 +2,7 @@ package control
|
|||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"sync/atomic"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
|
||||
|
@ -14,6 +15,11 @@ import (
|
|||
// Control service on storage node.
|
||||
type Server struct {
|
||||
*cfg
|
||||
|
||||
// TODO (aarifullin): this counter is used to assign id for rule chains
|
||||
// added as local overrides and will be removed as soon as in-memory
|
||||
// implementation will be replaced.
|
||||
apeChainCounter atomic.Uint32
|
||||
}
|
||||
|
||||
// HealthChecker is component interface for calculating
|
||||
|
@ -59,6 +65,8 @@ type cfg struct {
|
|||
|
||||
cnrSrc container.Source
|
||||
|
||||
apeChainSrc container.AccessPolicyEngineChainSource
|
||||
|
||||
replicator *replicator.Replicator
|
||||
|
||||
nodeState NodeState
|
||||
|
@ -151,3 +159,11 @@ func WithTreeService(s TreeService) Option {
|
|||
c.treeService = s
|
||||
}
|
||||
}
|
||||
|
||||
// WithAPEChainSource returns the option to set access policy engine
|
||||
// chain source.
|
||||
func WithAPEChainSource(apeChainSrc container.AccessPolicyEngineChainSource) Option {
|
||||
return func(c *cfg) {
|
||||
c.apeChainSrc = apeChainSrc
|
||||
}
|
||||
}
|
||||
|
|
BIN
pkg/services/control/service.pb.go
generated
BIN
pkg/services/control/service.pb.go
generated
Binary file not shown.
|
@ -44,6 +44,18 @@ service ControlService {
|
|||
|
||||
// Doctor performs storage restructuring operations on engine.
|
||||
rpc Doctor (DoctorRequest) returns (DoctorResponse);
|
||||
|
||||
// Add local access policy engine overrides to a node.
|
||||
rpc AddChainLocalOverride (AddChainLocalOverrideRequest) returns (AddChainLocalOverrideResponse);
|
||||
|
||||
// Get local access policy engine overrides stored in the node by chain id.
|
||||
rpc GetChainLocalOverride (GetChainLocalOverrideRequest) returns (GetChainLocalOverrideResponse);
|
||||
|
||||
// List local access policy engine overrides stored in the node by container id.
|
||||
rpc ListChainLocalOverrides (ListChainLocalOverridesRequest) returns (ListChainLocalOverridesResponse);
|
||||
|
||||
// Remove local access policy engine overrides stored in the node by chaind id.
|
||||
rpc RemoveChainLocalOverride (RemoveChainLocalOverrideRequest) returns (RemoveChainLocalOverrideResponse);
|
||||
}
|
||||
|
||||
// Health check request.
|
||||
|
@ -405,3 +417,105 @@ message StopShardEvacuationResponse {
|
|||
Body body = 1;
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// AddChainLocalOverride request.
|
||||
message AddChainLocalOverrideRequest {
|
||||
message Body {
|
||||
// Container id for which the overrides are applied.
|
||||
bytes container_id = 1;
|
||||
|
||||
// Serialized rule chain.
|
||||
bytes chain = 2;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// AddChainLocalOverride response.
|
||||
message AddChainLocalOverrideResponse {
|
||||
message Body {
|
||||
// Chain ID assigned for the added rule chain.
|
||||
string chain_id = 1;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// GetChainLocalOverride request.
|
||||
message GetChainLocalOverrideRequest {
|
||||
message Body {
|
||||
// Container id for which the overrides are defined.
|
||||
bytes container_id = 1;
|
||||
|
||||
// Chain ID assigned for the added rule chain.
|
||||
string chain_id = 2;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// GetChainLocalOverride response.
|
||||
message GetChainLocalOverrideResponse {
|
||||
message Body {
|
||||
// Serialized rule chain.
|
||||
bytes chain = 1;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// ListChainLocalOverrides request.
|
||||
message ListChainLocalOverridesRequest {
|
||||
message Body {
|
||||
// Container id for which the overrides are defined.
|
||||
bytes container_id = 1;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
// ListChainLocalOverrides response.
|
||||
message ListChainLocalOverridesResponse {
|
||||
message Body {
|
||||
// The list of serialized rule chain.
|
||||
repeated bytes chains = 1;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
message RemoveChainLocalOverrideRequest {
|
||||
message Body {
|
||||
// Container id for which the overrides are defined.
|
||||
bytes container_id = 1;
|
||||
|
||||
// Chain ID assigned for the added rule chain.
|
||||
string chain_id = 2;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
||||
message RemoveChainLocalOverrideResponse {
|
||||
message Body {
|
||||
bool removed = 1;
|
||||
}
|
||||
|
||||
Body body = 1;
|
||||
|
||||
Signature signature = 2;
|
||||
}
|
||||
|
|
BIN
pkg/services/control/service_frostfs.pb.go
generated
BIN
pkg/services/control/service_frostfs.pb.go
generated
Binary file not shown.
BIN
pkg/services/control/service_grpc.pb.go
generated
BIN
pkg/services/control/service_grpc.pb.go
generated
Binary file not shown.
56
pkg/services/object/acl/ape.go
Normal file
56
pkg/services/object/acl/ape.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
||||
v2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/v2"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
)
|
||||
|
||||
var (
|
||||
errAPEChainNoSource = errors.New("could not get ape chain source for the container")
|
||||
)
|
||||
|
||||
type apeCheckerImpl struct {
|
||||
log *logger.Logger
|
||||
apeSrc container.AccessPolicyEngineChainSource
|
||||
}
|
||||
|
||||
func NewAPEChecker(log *logger.Logger, apeSrc container.AccessPolicyEngineChainSource) v2.APEChainChecker {
|
||||
return &apeCheckerImpl{
|
||||
log: log,
|
||||
apeSrc: apeSrc,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *apeCheckerImpl) CheckIfRequestPermitted(reqInfo v2.RequestInfo) error {
|
||||
cnr := reqInfo.ContainerID()
|
||||
|
||||
chainCache, err := c.apeSrc.GetChainSource(cnr)
|
||||
if err != nil {
|
||||
return errAPEChainNoSource
|
||||
}
|
||||
|
||||
request := new(Request)
|
||||
request.FromRequestInfo(reqInfo)
|
||||
|
||||
status, ruleFound := chainCache.IsAllowed(policyengine.Ingress, "", request)
|
||||
|
||||
if !ruleFound || status == policyengine.Allow {
|
||||
return nil
|
||||
}
|
||||
|
||||
return apeErr(reqInfo, status)
|
||||
}
|
||||
|
||||
const accessDeniedAPEReasonFmt = "access to operation %s is denied by access policy engine: %s"
|
||||
|
||||
func apeErr(req v2.RequestInfo, status policyengine.Status) error {
|
||||
errAccessDenied := &apistatus.ObjectAccessDenied{}
|
||||
errAccessDenied.WriteReason(fmt.Sprintf(accessDeniedAPEReasonFmt, req.Operation(), status.String()))
|
||||
return errAccessDenied
|
||||
}
|
105
pkg/services/object/acl/ape_request.go
Normal file
105
pkg/services/object/acl/ape_request.go
Normal file
|
@ -0,0 +1,105 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/v2"
|
||||
aclSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
operation string
|
||||
resource *resource
|
||||
properties map[string]string
|
||||
}
|
||||
|
||||
var _ policyengine.Request = (*Request)(nil)
|
||||
|
||||
type resource struct {
|
||||
name string
|
||||
properties map[string]string
|
||||
}
|
||||
|
||||
var _ policyengine.Resource = (*resource)(nil)
|
||||
|
||||
func (r *resource) Name() string {
|
||||
return r.name
|
||||
}
|
||||
|
||||
func (r *resource) Property(key string) string {
|
||||
return r.properties[key]
|
||||
}
|
||||
|
||||
// TODO (aarifullin): these stringified verbs, properties and namespaces
|
||||
// should be non-implementation-specific.
|
||||
func getResource(reqInfo v2.RequestInfo) *resource {
|
||||
cid := reqInfo.ContainerID()
|
||||
oid := "*"
|
||||
if reqOID := reqInfo.ObjectID(); reqOID != nil {
|
||||
oid = reqOID.EncodeToString()
|
||||
}
|
||||
name := fmt.Sprintf("native:::object/%s/%s",
|
||||
cid,
|
||||
oid)
|
||||
|
||||
return &resource{
|
||||
name: name,
|
||||
properties: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func getProperties(_ v2.RequestInfo) map[string]string {
|
||||
return map[string]string{
|
||||
"Actor": "",
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (aarifullin): these stringified verbs, properties and namespaces
|
||||
// should be non-implementation-specific.
|
||||
func getOperation(reqInfo v2.RequestInfo) string {
|
||||
var verb string
|
||||
switch op := reqInfo.Operation(); op {
|
||||
case aclSDK.OpObjectGet:
|
||||
verb = "GetObject"
|
||||
case aclSDK.OpObjectHead:
|
||||
verb = "HeadObject"
|
||||
case aclSDK.OpObjectPut:
|
||||
verb = "PutObject"
|
||||
case aclSDK.OpObjectDelete:
|
||||
verb = "DeleteObject"
|
||||
case aclSDK.OpObjectSearch:
|
||||
verb = "SearchObject"
|
||||
case aclSDK.OpObjectRange:
|
||||
verb = "RangeObject"
|
||||
case aclSDK.OpObjectHash:
|
||||
verb = "HashObject"
|
||||
}
|
||||
|
||||
return "native:" + verb
|
||||
}
|
||||
|
||||
func NewRequest() *Request {
|
||||
return &Request{
|
||||
resource: new(resource),
|
||||
properties: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Request) FromRequestInfo(ri v2.RequestInfo) {
|
||||
r.operation = getOperation(ri)
|
||||
r.resource = getResource(ri)
|
||||
r.properties = getProperties(ri)
|
||||
}
|
||||
|
||||
func (r *Request) Operation() string {
|
||||
return r.operation
|
||||
}
|
||||
|
||||
func (r *Request) Property(key string) string {
|
||||
return r.properties[key]
|
||||
}
|
||||
|
||||
func (r *Request) Resource() policyengine.Resource {
|
||||
return r.resource
|
||||
}
|
|
@ -67,6 +67,10 @@ type cfg struct {
|
|||
|
||||
checker ACLChecker
|
||||
|
||||
// TODO(aarifullin): apeCheck is temporarily the part of
|
||||
// acl service and must be standalone.
|
||||
apeChecker APEChainChecker
|
||||
|
||||
irFetcher InnerRingFetcher
|
||||
|
||||
nm netmap.Source
|
||||
|
@ -79,6 +83,7 @@ func New(next object.ServiceServer,
|
|||
nm netmap.Source,
|
||||
irf InnerRingFetcher,
|
||||
acl ACLChecker,
|
||||
apeChecker APEChainChecker,
|
||||
cs container.Source,
|
||||
opts ...Option,
|
||||
) Service {
|
||||
|
@ -88,6 +93,7 @@ func New(next object.ServiceServer,
|
|||
nm: nm,
|
||||
irFetcher: irf,
|
||||
checker: acl,
|
||||
apeChecker: apeChecker,
|
||||
containers: cs,
|
||||
}
|
||||
|
||||
|
@ -212,12 +218,6 @@ func (b Service) Head(
|
|||
|
||||
reqInfo.obj = obj
|
||||
|
||||
if !b.checker.CheckBasicACL(reqInfo) {
|
||||
return nil, basicACLErr(reqInfo)
|
||||
} else if err := b.checker.CheckEACL(request, reqInfo); err != nil {
|
||||
return nil, eACLErr(reqInfo, err)
|
||||
}
|
||||
|
||||
resp, err := b.next.Head(ctx, request)
|
||||
if err == nil {
|
||||
if err = b.checker.CheckEACL(resp, reqInfo); err != nil {
|
||||
|
@ -560,10 +560,8 @@ func (p putStreamBasicChecker) Send(ctx context.Context, request *objectV2.PutRe
|
|||
|
||||
reqInfo.obj = obj
|
||||
|
||||
if !p.source.checker.CheckBasicACL(reqInfo) || !p.source.checker.StickyBitCheck(reqInfo, idOwner) {
|
||||
return basicACLErr(reqInfo)
|
||||
} else if err := p.source.checker.CheckEACL(request, reqInfo); err != nil {
|
||||
return eACLErr(reqInfo, err)
|
||||
if err := p.source.apeChecker.CheckIfRequestPermitted(reqInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,3 +26,9 @@ type InnerRingFetcher interface {
|
|||
// the actual inner ring.
|
||||
InnerRingKeys() ([][]byte, error)
|
||||
}
|
||||
|
||||
// APEChainChecker is the interface that provides methods to
|
||||
// check if the access policy engine permits to perform the request.
|
||||
type APEChainChecker interface {
|
||||
CheckIfRequestPermitted(RequestInfo) error
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue