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,
|
shardsCmd,
|
||||||
synchronizeTreeCmd,
|
synchronizeTreeCmd,
|
||||||
irCmd,
|
irCmd,
|
||||||
|
addRuleCmd,
|
||||||
|
removeRuleCmd,
|
||||||
|
listRulesCmd,
|
||||||
|
getRuleCmd,
|
||||||
)
|
)
|
||||||
|
|
||||||
initControlHealthCheckCmd()
|
initControlHealthCheckCmd()
|
||||||
|
@ -42,4 +46,8 @@ func init() {
|
||||||
initControlShardsCmd()
|
initControlShardsCmd()
|
||||||
initControlSynchronizeTreeCmd()
|
initControlSynchronizeTreeCmd()
|
||||||
initControlIRCmd()
|
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
|
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
|
pool cfgObjectRoutines
|
||||||
|
|
||||||
cfgLocalStorage cfgLocalStorage
|
cfgLocalStorage cfgLocalStorage
|
||||||
|
|
|
@ -51,6 +51,7 @@ func initControlService(c *cfg) {
|
||||||
controlSvc.WithTreeService(treeSynchronizer{
|
controlSvc.WithTreeService(treeSynchronizer{
|
||||||
c.treeService,
|
c.treeService,
|
||||||
}),
|
}),
|
||||||
|
controlSvc.WithAPEChainSource(c.cfgObject.apeChainSource),
|
||||||
)
|
)
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", endpoint)
|
lis, err := net.Listen("tcp", endpoint)
|
||||||
|
|
|
@ -157,6 +157,8 @@ func initObjectService(c *cfg) {
|
||||||
|
|
||||||
c.replicator = createReplicator(c, keyStorage, c.bgClientCache)
|
c.replicator = createReplicator(c, keyStorage, c.bgClientCache)
|
||||||
|
|
||||||
|
c.cfgObject.apeChainSource = NewAPESource()
|
||||||
|
|
||||||
addPolicer(c, keyStorage, c.bgClientCache)
|
addPolicer(c, keyStorage, c.bgClientCache)
|
||||||
|
|
||||||
traverseGen := util.NewTraverserGenerator(c.netMapSource, c.cfgObject.cnrSource, c)
|
traverseGen := util.NewTraverserGenerator(c.netMapSource, c.cfgObject.cnrSource, c)
|
||||||
|
@ -424,6 +426,7 @@ func createACLServiceV2(c *cfg, splitSvc *objectService.TransportSplitter, irFet
|
||||||
c.cfgObject.eaclSource,
|
c.cfgObject.eaclSource,
|
||||||
eaclSDK.NewValidator(),
|
eaclSDK.NewValidator(),
|
||||||
ls),
|
ls),
|
||||||
|
acl.NewAPEChecker(c.log, c.cfgObject.apeChainSource),
|
||||||
c.cfgObject.cnrSource,
|
c.cfgObject.cnrSource,
|
||||||
v2.WithLogger(c.log),
|
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-observability v0.0.0-20230531082742-c97d21411eb6
|
||||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231101144515-6fbe1595cb3d
|
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20231101144515-6fbe1595cb3d
|
||||||
git.frostfs.info/TrueCloudLab/hrw v1.2.1
|
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
|
git.frostfs.info/TrueCloudLab/tzhash v1.8.0
|
||||||
github.com/cheggaaa/pb v1.0.29
|
github.com/cheggaaa/pb v1.0.29
|
||||||
github.com/chzyer/readline v1.5.1
|
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"
|
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
"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.
|
// 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.
|
// eACL table is not in source.
|
||||||
GetEACL(cid.ID) (*EACL, error)
|
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"
|
rpcStopShardEvacuation = "StopShardEvacuation"
|
||||||
rpcFlushCache = "FlushCache"
|
rpcFlushCache = "FlushCache"
|
||||||
rpcDoctor = "Doctor"
|
rpcDoctor = "Doctor"
|
||||||
|
rpcAddChainLocalOverride = "AddChainLocalOverride"
|
||||||
|
rpcGetChainLocalOverride = "GetChainLocalOverride"
|
||||||
|
rpcListChainLocalOverrides = "ListChainLocalOverrides"
|
||||||
|
rpcRemoveChainLocalOverride = "RemoveChainLocalOverride"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HealthCheck executes ControlService.HealthCheck RPC.
|
// HealthCheck executes ControlService.HealthCheck RPC.
|
||||||
|
@ -208,3 +212,55 @@ func Doctor(cli *client.Client, req *DoctorRequest, opts ...client.CallOption) (
|
||||||
|
|
||||||
return wResp.message, nil
|
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 (
|
import (
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
|
||||||
|
@ -14,6 +15,11 @@ import (
|
||||||
// Control service on storage node.
|
// Control service on storage node.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
*cfg
|
*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
|
// HealthChecker is component interface for calculating
|
||||||
|
@ -59,6 +65,8 @@ type cfg struct {
|
||||||
|
|
||||||
cnrSrc container.Source
|
cnrSrc container.Source
|
||||||
|
|
||||||
|
apeChainSrc container.AccessPolicyEngineChainSource
|
||||||
|
|
||||||
replicator *replicator.Replicator
|
replicator *replicator.Replicator
|
||||||
|
|
||||||
nodeState NodeState
|
nodeState NodeState
|
||||||
|
@ -151,3 +159,11 @@ func WithTreeService(s TreeService) Option {
|
||||||
c.treeService = s
|
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.
|
// Doctor performs storage restructuring operations on engine.
|
||||||
rpc Doctor (DoctorRequest) returns (DoctorResponse);
|
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.
|
// Health check request.
|
||||||
|
@ -405,3 +417,105 @@ message StopShardEvacuationResponse {
|
||||||
Body body = 1;
|
Body body = 1;
|
||||||
Signature signature = 2;
|
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
|
checker ACLChecker
|
||||||
|
|
||||||
|
// TODO(aarifullin): apeCheck is temporarily the part of
|
||||||
|
// acl service and must be standalone.
|
||||||
|
apeChecker APEChainChecker
|
||||||
|
|
||||||
irFetcher InnerRingFetcher
|
irFetcher InnerRingFetcher
|
||||||
|
|
||||||
nm netmap.Source
|
nm netmap.Source
|
||||||
|
@ -79,6 +83,7 @@ func New(next object.ServiceServer,
|
||||||
nm netmap.Source,
|
nm netmap.Source,
|
||||||
irf InnerRingFetcher,
|
irf InnerRingFetcher,
|
||||||
acl ACLChecker,
|
acl ACLChecker,
|
||||||
|
apeChecker APEChainChecker,
|
||||||
cs container.Source,
|
cs container.Source,
|
||||||
opts ...Option,
|
opts ...Option,
|
||||||
) Service {
|
) Service {
|
||||||
|
@ -88,6 +93,7 @@ func New(next object.ServiceServer,
|
||||||
nm: nm,
|
nm: nm,
|
||||||
irFetcher: irf,
|
irFetcher: irf,
|
||||||
checker: acl,
|
checker: acl,
|
||||||
|
apeChecker: apeChecker,
|
||||||
containers: cs,
|
containers: cs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,12 +218,6 @@ func (b Service) Head(
|
||||||
|
|
||||||
reqInfo.obj = obj
|
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)
|
resp, err := b.next.Head(ctx, request)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err = b.checker.CheckEACL(resp, reqInfo); 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
|
reqInfo.obj = obj
|
||||||
|
|
||||||
if !p.source.checker.CheckBasicACL(reqInfo) || !p.source.checker.StickyBitCheck(reqInfo, idOwner) {
|
if err := p.source.apeChecker.CheckIfRequestPermitted(reqInfo); err != nil {
|
||||||
return basicACLErr(reqInfo)
|
return err
|
||||||
} else if err := p.source.checker.CheckEACL(request, reqInfo); err != nil {
|
|
||||||
return eACLErr(reqInfo, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,3 +26,9 @@ type InnerRingFetcher interface {
|
||||||
// the actual inner ring.
|
// the actual inner ring.
|
||||||
InnerRingKeys() ([][]byte, error)
|
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