forked from TrueCloudLab/frostfs-node
[#770] cli: Add methods to work with APE rules via control svc
* Add methods to frostfs-cli * Implement rpc in control service Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
This commit is contained in:
parent
d04a00800b
commit
624f9deb87
11 changed files with 813 additions and 12 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -5,20 +5,144 @@ import (
|
|||
"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(ctx context.Context, req *control.AddChainLocalOverrideRequest) (*control.AddChainLocalOverrideResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented yet")
|
||||
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(ctx context.Context, req *control.GetChainLocalOverrideRequest) (*control.GetChainLocalOverrideResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented yet")
|
||||
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(ctx context.Context, req *control.ListChainLocalOverridesRequest) (*control.ListChainLocalOverridesResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented yet")
|
||||
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(ctx context.Context, req *control.RemoveChainLocalOverrideRequest) (*control.RemoveChainLocalOverrideResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented yet")
|
||||
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
|
||||
|
|
|
@ -218,10 +218,6 @@ func (b Service) Head(
|
|||
|
||||
reqInfo.obj = obj
|
||||
|
||||
if err := b.apeChecker.CheckIfRequestPermitted(reqInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := b.next.Head(ctx, request)
|
||||
if err == nil {
|
||||
if err = b.checker.CheckEACL(resp, reqInfo); err != nil {
|
||||
|
|
Loading…
Reference in a new issue