diff --git a/go.mod b/go.mod index 4ce8a0d27..e267be249 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module git.frostfs.info/TrueCloudLab/frostfs-node go 1.21 +replace git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240513163744-1f6f4163d40d => git.frostfs.info/aarifullin/policy-engine v0.0.0-20240617153626-ce600902773a + require ( code.gitea.io/sdk/gitea v0.17.1 git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240530152826-2f6d3209e1d3 diff --git a/go.sum b/go.sum index cb97af3d4..7858815ea 100644 Binary files a/go.sum and b/go.sum differ diff --git a/pkg/services/tree/ape.go b/pkg/services/tree/ape.go index 475567c5f..bcd7f8a38 100644 --- a/pkg/services/tree/ape.go +++ b/pkg/services/tree/ape.go @@ -9,7 +9,6 @@ import ( "net" "strings" - "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/ape/converter" aperequest "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/ape/request" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/ape/router" core "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" @@ -18,7 +17,6 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" cnrSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" @@ -39,20 +37,13 @@ var ( ) func (s *Service) newAPERequest(ctx context.Context, namespace string, - cid cid.ID, operation acl.Op, role acl.Role, publicKey *keys.PublicKey, + cid cid.ID, treeID, schemaMethod, schemaRole string, publicKey *keys.PublicKey, ) (aperequest.Request, error) { - schemaMethod, err := converter.SchemaMethodFromACLOperation(operation) - if err != nil { - return aperequest.Request{}, err - } - schemaRole, err := converter.SchemaRoleFromACLRole(role) - if err != nil { - return aperequest.Request{}, err - } reqProps := map[string]string{ nativeschema.PropertyKeyActorPublicKey: hex.EncodeToString(publicKey.Bytes()), nativeschema.PropertyKeyActorRole: schemaRole, } + var err error reqProps, err = s.fillWithUserClaimTags(reqProps, publicKey) if err != nil { return aperequest.Request{}, err @@ -65,9 +56,9 @@ func (s *Service) newAPERequest(ctx context.Context, namespace string, var resourceName string if namespace == "root" || namespace == "" { - resourceName = fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cid.EncodeToString()) + resourceName = fmt.Sprintf(nativeschema.ResourceFormatRootContainerTree, cid.EncodeToString(), treeID) } else { - resourceName = fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerObjects, namespace, cid.EncodeToString()) + resourceName = fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainerTree, namespace, cid.EncodeToString(), treeID) } return aperequest.NewRequest( @@ -126,29 +117,37 @@ func isValidBearer(token *bearer.Token, ownerCnr user.ID, cntID cid.ID, publicKe return nil } -func (s *Service) checkAPE(ctx context.Context, bt *bearer.Token, - container *core.Container, cid cid.ID, operation acl.Op, role acl.Role, publicKey *keys.PublicKey, -) error { +type checkAPEPrm struct { + bearerToken *bearer.Token + container *core.Container + cid cid.ID + method string + role string + treeID string + publicKey *keys.PublicKey +} + +func (s *Service) checkAPE(ctx context.Context, prm checkAPEPrm) error { namespace := "" - cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(container.Value).Zone(), ".ns") + cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(prm.container.Value).Zone(), ".ns") if hasNamespace { namespace = cntNamespace } - request, err := s.newAPERequest(ctx, namespace, cid, operation, role, publicKey) + request, err := s.newAPERequest(ctx, namespace, prm.cid, prm.treeID, prm.method, prm.role, prm.publicKey) if err != nil { return apeErr(err) } - if bt != nil && !bt.Impersonate() { - if err := isValidBearer(bt, container.Value.Owner(), cid, publicKey, s.state); err != nil { + if prm.bearerToken != nil && !prm.bearerToken.Impersonate() { + if err := isValidBearer(prm.bearerToken, prm.container.Value.Owner(), prm.cid, prm.publicKey, s.state); err != nil { return fmt.Errorf("bearer validation error: %w", err) } - btRouter, err := router.SingleUseRouterWithBearerTokenChains([]bearer.APEOverride{bt.APEOverride()}) + btRouter, err := router.SingleUseRouterWithBearerTokenChains([]bearer.APEOverride{prm.bearerToken.APEOverride()}) if err != nil { return apeErr(err) } - status, found, err := btRouter.IsAllowed(apechain.Ingress, engine.NewRequestTargetWithContainer(cid.EncodeToString()), request) + status, found, err := btRouter.IsAllowed(apechain.Ingress, engine.NewRequestTargetWithContainer(prm.cid.EncodeToString()), request) if err != nil { return apeErr(err) } @@ -161,7 +160,7 @@ func (s *Service) checkAPE(ctx context.Context, bt *bearer.Token, } } - rt := engine.NewRequestTargetExtended(namespace, cid.EncodeToString(), fmt.Sprintf("%s:%s", namespace, publicKey.Address()), nil) + rt := engine.NewRequestTargetExtended(namespace, prm.cid.EncodeToString(), fmt.Sprintf("%s:%s", namespace, prm.publicKey.Address()), nil) status, found, err := s.router.IsAllowed(apechain.Ingress, rt, request) if err != nil { return apeErr(err) diff --git a/pkg/services/tree/service.go b/pkg/services/tree/service.go index cce41e94d..a6bac37a5 100644 --- a/pkg/services/tree/service.go +++ b/pkg/services/tree/service.go @@ -11,9 +11,9 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/pilorama" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" + nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "github.com/panjf2000/ants/v2" "go.uber.org/zap" ) @@ -105,7 +105,7 @@ func (s *Service) Add(ctx context.Context, req *AddRequest) (*AddResponse, error return nil, err } - err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), acl.OpObjectPut) + err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), nativeschema.MethodAddTreeNode, b.GetTreeId()) if err != nil { return nil, err } @@ -157,7 +157,7 @@ func (s *Service) AddByPath(ctx context.Context, req *AddByPathRequest) (*AddByP return nil, err } - err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), acl.OpObjectPut) + err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), nativeschema.MethodAddTreeNodeByPath, b.GetTreeId()) if err != nil { return nil, err } @@ -221,7 +221,7 @@ func (s *Service) Remove(ctx context.Context, req *RemoveRequest) (*RemoveRespon return nil, err } - err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), acl.OpObjectPut) + err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), nativeschema.MethodRemoveTreeNode, b.GetTreeId()) if err != nil { return nil, err } @@ -274,7 +274,7 @@ func (s *Service) Move(ctx context.Context, req *MoveRequest) (*MoveResponse, er return nil, err } - err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), acl.OpObjectPut) + err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), nativeschema.MethodMoveTreeNode, b.GetTreeId()) if err != nil { return nil, err } @@ -326,7 +326,7 @@ func (s *Service) GetNodeByPath(ctx context.Context, req *GetNodeByPathRequest) return nil, err } - err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), acl.OpObjectGet) + err := s.verifyClient(ctx, req, cid, b.GetBearerToken(), nativeschema.MethodGetTreeNodeByPath, b.GetTreeId()) if err != nil { return nil, err } @@ -408,7 +408,7 @@ func (s *Service) GetSubTree(req *GetSubTreeRequest, srv TreeService_GetSubTreeS return err } - err := s.verifyClient(srv.Context(), req, cid, b.GetBearerToken(), acl.OpObjectGet) + err := s.verifyClient(srv.Context(), req, cid, b.GetBearerToken(), nativeschema.MethodGetSubtree, b.GetTreeId()) if err != nil { return err } diff --git a/pkg/services/tree/signature.go b/pkg/services/tree/signature.go index 58cab659f..2a9f44063 100644 --- a/pkg/services/tree/signature.go +++ b/pkg/services/tree/signature.go @@ -9,18 +9,14 @@ import ( "fmt" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" - "git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs" core "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto" frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "go.uber.org/zap" ) type message interface { @@ -30,32 +26,18 @@ type message interface { SetSignature(*Signature) } -func basicACLErr(op acl.Op) error { - return fmt.Errorf("access to operation %s is denied by basic ACL check", op) -} - -func eACLErr(op eacl.Operation, err error) error { - return fmt.Errorf("access to operation %s is denied by extended ACL check: %w", op, err) -} - -var ( - errBearerWrongOwner = errors.New("bearer token must be signed by the container owner") - errBearerWrongContainer = errors.New("bearer token is created for another container") - errBearerSignature = errors.New("invalid bearer token signature") -) - // verifyClient verifies if the request for a client operation // was signed by a key allowed by (e)ACL rules. // Operation must be one of: // - 1. ObjectPut; // - 2. ObjectGet. -func (s *Service) verifyClient(ctx context.Context, req message, cid cidSDK.ID, rawBearer []byte, op acl.Op) error { +func (s *Service) verifyClient(ctx context.Context, req message, cid cidSDK.ID, rawBearer []byte, method, treeID string) error { err := verifyMessage(req) if err != nil { return err } - isAuthorized, err := s.isAuthorized(req, op) + isAuthorized, err := s.isAuthorized(req, method) if isAuthorized || err != nil { return err } @@ -65,75 +47,36 @@ func (s *Service) verifyClient(ctx context.Context, req message, cid cidSDK.ID, return fmt.Errorf("can't get container %s: %w", cid, err) } - eaclOp := eACLOp(op) - - bt, err := parseBearer(rawBearer, cid, eaclOp) + bt, err := parseBearer(rawBearer) if err != nil { return err } role, pubKey, err := roleAndPubKeyFromReq(cnr, req, bt) if err != nil { - return fmt.Errorf("can't get request role: %w", err) + return fmt.Errorf("can't get role and public key: %w", err) } - basicACL := cnr.Value.BasicACL() - // Basic ACL mask can be unset, if a container operations are performed - // with strict APE checks only. - // - // FIXME(@aarifullin): tree service temporiraly performs APE checks on - // object verbs, because tree verbs have not been introduced yet. - if basicACL == 0x0 { - return s.checkAPE(ctx, bt, cnr, cid, op, role, pubKey) + prm := checkAPEPrm{ + bearerToken: bt, + container: cnr, + cid: cid, + method: method, + role: role, + treeID: treeID, + publicKey: pubKey, } - - if !basicACL.IsOpAllowed(op, role) { - return basicACLErr(op) - } - - if !basicACL.Extendable() { - return nil - } - - var useBearer bool - if len(rawBearer) != 0 { - if !basicACL.AllowedBearerRules(op) { - s.log.Debug(logs.TreeBearerPresentedButNotAllowedByACL, - zap.String("cid", cid.EncodeToString()), - zap.Stringer("op", op), - ) - } else { - useBearer = true - } - } - - var tb eacl.Table - signer := req.GetSignature().GetKey() - if useBearer && !bt.Impersonate() { - if !bearer.ResolveIssuer(*bt).Equals(cnr.Value.Owner()) { - return eACLErr(eaclOp, errBearerWrongOwner) - } - tb = bt.EACLTable() - } else { - tbCore, err := s.eaclSource.GetEACL(cid) - if err != nil { - return handleGetEACLError(err) - } - tb = *tbCore.Value - - if useBearer && bt.Impersonate() { - signer = bt.SigningKeyBytes() - } - } - - return checkEACL(tb, signer, eACLRole(role), eaclOp) + return s.checkAPE(ctx, prm) } -// Returns true iff the operation is read-only and request was signed +// Returns true if the operation is read-only and request was signed // with one of the authorized keys. -func (s *Service) isAuthorized(req message, op acl.Op) (bool, error) { - if op != acl.OpObjectGet { +func (s *Service) isAuthorized(req message, method string) (bool, error) { + switch method { + default: + // Mutable tree operations are never authorized and thus must be checked anyway. return false, nil + case nativeschema.MethodGetTreeNodeByPath, nativeschema.MethodGetSubtree, nativeschema.MethodListTrees: } sign := req.GetSignature() @@ -150,32 +93,18 @@ func (s *Service) isAuthorized(req message, op acl.Op) (bool, error) { return false, nil } -func parseBearer(rawBearer []byte, cid cidSDK.ID, eaclOp eacl.Operation) (*bearer.Token, error) { +func parseBearer(rawBearer []byte) (*bearer.Token, error) { if len(rawBearer) == 0 { return nil, nil } bt := new(bearer.Token) if err := bt.Unmarshal(rawBearer); err != nil { - return nil, eACLErr(eaclOp, fmt.Errorf("invalid bearer token: %w", err)) - } - if !bt.AssertContainer(cid) { - return nil, eACLErr(eaclOp, errBearerWrongContainer) - } - if !bt.VerifySignature() { - return nil, eACLErr(eaclOp, errBearerSignature) + return nil, fmt.Errorf("invalid bearer token: %w", err) } return bt, nil } -func handleGetEACLError(err error) error { - if client.IsErrEACLNotFound(err) { - return nil - } - - return fmt.Errorf("get eACL table: %w", err) -} - func verifyMessage(m message) error { binBody, err := m.ReadSignedData(nil) if err != nil { @@ -226,8 +155,8 @@ func SignMessage(m message, key *ecdsa.PrivateKey) error { return nil } -func roleAndPubKeyFromReq(cnr *core.Container, req message, bt *bearer.Token) (acl.Role, *keys.PublicKey, error) { - role := acl.RoleOthers +func roleAndPubKeyFromReq(cnr *core.Container, req message, bt *bearer.Token) (string, *keys.PublicKey, error) { + role := nativeschema.PropertyValueContainerRoleOthers owner := cnr.Value.Owner() rawKey := req.GetSignature().GetKey() @@ -244,89 +173,8 @@ func roleAndPubKeyFromReq(cnr *core.Container, req message, bt *bearer.Token) (a user.IDFromKey(&reqSigner, (ecdsa.PublicKey)(*pub)) if reqSigner.Equals(owner) { - role = acl.RoleOwner + role = nativeschema.PropertyValueContainerRoleOwner } return role, pub, nil } - -func eACLOp(op acl.Op) eacl.Operation { - switch op { - case acl.OpObjectGet: - return eacl.OperationGet - case acl.OpObjectPut: - return eacl.OperationPut - default: - panic(fmt.Sprintf("unexpected tree service ACL operation: %s", op)) - } -} - -func eACLRole(role acl.Role) eacl.Role { - switch role { - case acl.RoleOwner: - return eacl.RoleUser - case acl.RoleOthers: - return eacl.RoleOthers - default: - panic(fmt.Sprintf("unexpected tree service ACL role: %s", role)) - } -} - -var ( - errDENY = errors.New("DENY eACL rule") - errNoAllowRules = errors.New("not found allowing rules for the request") -) - -// checkEACL searches for the eACL rules that could be applied to the request -// (a tuple of a signer key, his FrostFS role and a request operation). -// It does not filter the request by the filters of the eACL table since tree -// requests do not contain any "object" information that could be filtered and, -// therefore, filtering leads to unexpected results. -// The code was copied with the minor updates from the SDK repo: -// https://github.com/nspcc-dev/frostfs-sdk-go/blob/43a57d42dd50dc60465bfd3482f7f12bcfcf3411/eacl/validator.go#L28. -func checkEACL(tb eacl.Table, signer []byte, role eacl.Role, op eacl.Operation) error { - for _, record := range tb.Records() { - // check type of operation - if record.Operation() != op { - continue - } - - // check target - if !targetMatches(record, role, signer) { - continue - } - - switch a := record.Action(); a { - case eacl.ActionAllow: - return nil - case eacl.ActionDeny: - return eACLErr(op, errDENY) - default: - return eACLErr(op, fmt.Errorf("unexpected action: %s", a)) - } - } - - return eACLErr(op, errNoAllowRules) -} - -func targetMatches(rec eacl.Record, role eacl.Role, signer []byte) bool { - for _, target := range rec.Targets() { - // check public key match - if pubs := target.BinaryKeys(); len(pubs) != 0 { - for _, key := range pubs { - if bytes.Equal(key, signer) { - return true - } - } - - continue - } - - // check target group match - if role == target.Role() { - return true - } - } - - return false -} diff --git a/pkg/services/tree/signature_test.go b/pkg/services/tree/signature_test.go index ce5039f7c..6a4eafbf9 100644 --- a/pkg/services/tree/signature_test.go +++ b/pkg/services/tree/signature_test.go @@ -4,22 +4,31 @@ import ( "context" "crypto/ecdsa" "crypto/sha256" + "encoding/hex" "errors" + "fmt" "testing" aclV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" + "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" + frostfsidcore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/frostfsid" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger/test" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/ape" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" - eaclSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl" netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine/inmemory" + nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" ) @@ -81,6 +90,25 @@ func testContainer(owner user.ID) container.Container { return cnt } +type dummyFrostfsIDSubjProvider struct { +} + +var _ frostfsidcore.SubjectProvider = (*dummyFrostfsIDSubjProvider)(nil) + +func (p *dummyFrostfsIDSubjProvider) GetSubject(addr util.Uint160) (*client.Subject, error) { + return &client.Subject{}, nil +} + +func (p *dummyFrostfsIDSubjProvider) GetSubjectExtended(addr util.Uint160) (*client.SubjectExtended, error) { + return &client.SubjectExtended{}, nil +} + +type dummyState struct{} + +func (d *dummyState) CurrentEpoch() uint64 { + return 0 +} + func TestMessageSign(t *testing.T) { privs := make([]*keys.PrivateKey, 4) for i := range privs { @@ -99,6 +127,9 @@ func TestMessageSign(t *testing.T) { Value: testContainer(ownerID), } + localStorageChainMock := inmemory.NewInmemoryLocalStorage() + morphStorageChainMock := inmemory.NewInmemoryMorphRuleChainStorage() + s := &Service{ cfg: cfg{ log: test.NewLogger(t), @@ -107,11 +138,10 @@ func TestMessageSign(t *testing.T) { cnrSource: dummyContainerSource{ cid1.String(): cnr, }, - eaclSource: dummyEACLSource{ - cid1.String(): &containercore.EACL{ - Value: testTable(cid1, privs[0].PublicKey(), privs[1].PublicKey()), - }, - }, + state: &dummyState{}, + eaclSource: dummyEACLSource{}, + router: policyengine.NewDefaultChainRouterWithLocalOverrides(morphStorageChainMock, localStorageChainMock), + frostfsidSubjectProvider: &dummyFrostfsIDSubjProvider{}, }, } @@ -129,61 +159,103 @@ func TestMessageSign(t *testing.T) { }, } - op := acl.OpObjectPut - cnr.Value.SetBasicACL(acl.PublicRW) + const treeID = "version" t.Run("missing signature, no panic", func(t *testing.T) { - require.Error(t, s.verifyClient(context.Background(), req, cid2, nil, op)) + require.Error(t, s.verifyClient(context.Background(), req, cid2, nil, nativeschema.MethodMoveTreeNode, treeID)) }) require.NoError(t, SignMessage(req, &privs[0].PrivateKey)) - require.NoError(t, s.verifyClient(context.Background(), req, cid1, nil, op)) - t.Run("invalid CID", func(t *testing.T) { - require.Error(t, s.verifyClient(context.Background(), req, cid2, nil, op)) + t.Run("deprecated basic ACL scheme", func(t *testing.T) { + t.Run("zero-filled bACL", func(t *testing.T) { + require.Error(t, s.verifyClient(context.Background(), req, cid1, nil, nativeschema.MethodMoveTreeNode, treeID)) + }) + + t.Run("non-zero-filled bACL", func(t *testing.T) { + cnr.Value.SetBasicACL(acl.PublicRW) + require.Error(t, s.verifyClient(context.Background(), req, cid1, nil, nativeschema.MethodMoveTreeNode, treeID)) + }) }) - cnr.Value.SetBasicACL(acl.Private) + localStorageChainMock.AddOverride(chain.Ingress, policyengine.ContainerTarget(cid1.EncodeToString()), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{ + Names: []string{nativeschema.MethodMoveTreeNode}, + }, + Resources: chain.Resources{ + Names: []string{ + nativeschema.ResourceFormatRootTrees, + }, + }, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOwner, + }, + { + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: nativeschema.PropertyKeyActorPublicKey, + Value: hex.EncodeToString(privs[0].PublicKey().Bytes()), + }, + }, + }, + { + Status: chain.Allow, + Actions: chain.Actions{ + Names: []string{nativeschema.MethodGetSubtree}, + }, + Resources: chain.Resources{ + Names: []string{ + nativeschema.ResourceFormatRootTrees, + }, + }, + Condition: []chain.Condition{}, + }, + }, + }) - t.Run("extension disabled", func(t *testing.T) { - require.NoError(t, SignMessage(req, &privs[0].PrivateKey)) - require.Error(t, s.verifyClient(context.Background(), req, cid2, nil, op)) + require.NoError(t, s.verifyClient(context.Background(), req, cid1, nil, nativeschema.MethodMoveTreeNode, treeID)) + + t.Run("invalid CID", func(t *testing.T) { + require.Error(t, s.verifyClient(context.Background(), req, cid2, nil, nativeschema.MethodMoveTreeNode, treeID)) }) t.Run("invalid key", func(t *testing.T) { require.NoError(t, SignMessage(req, &privs[1].PrivateKey)) - require.Error(t, s.verifyClient(context.Background(), req, cid1, nil, op)) + require.Error(t, s.verifyClient(context.Background(), req, cid1, nil, nativeschema.MethodMoveTreeNode, treeID)) }) t.Run("bearer", func(t *testing.T) { - bACL := acl.PrivateExtended - bACL.AllowBearerRules(op) - cnr.Value.SetBasicACL(bACL) - - bACL.DisableExtension() - t.Run("invalid bearer", func(t *testing.T) { - req.Body.BearerToken = []byte{0xFF} + req.Body.BearerToken = []byte{0xDE, 0xAD, 0xBE, 0xEF} require.NoError(t, SignMessage(req, &privs[0].PrivateKey)) - require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut)) + require.ErrorContains(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodMoveTreeNode, treeID), "cannot parse invalid wire-format data") }) t.Run("invalid bearer CID", func(t *testing.T) { - bt := testBearerToken(cid2, privs[1].PublicKey(), privs[2].PublicKey()) + bt := testBearerToken(cid2, privs[1].PublicKey(), privs[2].PublicKey()) //testBearerToken(cid2, privs[1].PublicKey(), privs[2].PublicKey()) require.NoError(t, bt.Sign(privs[0].PrivateKey)) req.Body.BearerToken = bt.Marshal() require.NoError(t, SignMessage(req, &privs[1].PrivateKey)) - require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut)) + require.ErrorContains(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodMoveTreeNode, treeID), errBearerInvalidContainerID.Error()) }) + t.Run("invalid bearer owner", func(t *testing.T) { - bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) + bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) //testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) require.NoError(t, bt.Sign(privs[1].PrivateKey)) req.Body.BearerToken = bt.Marshal() require.NoError(t, SignMessage(req, &privs[1].PrivateKey)) - require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut)) + require.ErrorContains(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodMoveTreeNode, treeID), errBearerNotSignedByOwner.Error()) }) + t.Run("invalid bearer signature", func(t *testing.T) { bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) require.NoError(t, bt.Sign(privs[0].PrivateKey)) @@ -194,20 +266,20 @@ func TestMessageSign(t *testing.T) { req.Body.BearerToken = bv2.StableMarshal(nil) require.NoError(t, SignMessage(req, &privs[1].PrivateKey)) - require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut)) + require.ErrorContains(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodMoveTreeNode, treeID), errBearerInvalidSignature.Error()) }) t.Run("impersonate", func(t *testing.T) { - cnr.Value.SetBasicACL(acl.PublicRWExtended) var bt bearer.Token + bt.SetImpersonate(true) - require.NoError(t, bt.Sign(privs[1].PrivateKey)) - req.Body.BearerToken = bt.Marshal() + req.Body.BearerToken = bt.Marshal() require.NoError(t, SignMessage(req, &privs[0].PrivateKey)) - require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut)) - require.NoError(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectGet)) + + require.ErrorContains(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodMoveTreeNode, treeID), "access to object operation denied") + require.NoError(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodGetSubtree, treeID)) }) bt := testBearerToken(cid1, privs[1].PublicKey(), privs[2].PublicKey()) @@ -217,64 +289,137 @@ func TestMessageSign(t *testing.T) { t.Run("put and get", func(t *testing.T) { require.NoError(t, SignMessage(req, &privs[1].PrivateKey)) - require.NoError(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut)) - require.NoError(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectGet)) + require.NoError(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodMoveTreeNode, treeID)) + require.NoError(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodGetSubtree, treeID)) }) t.Run("only get", func(t *testing.T) { require.NoError(t, SignMessage(req, &privs[2].PrivateKey)) - require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut)) - require.NoError(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectGet)) + require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodMoveTreeNode, treeID)) + require.NoError(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodGetSubtree, treeID)) }) t.Run("none", func(t *testing.T) { require.NoError(t, SignMessage(req, &privs[3].PrivateKey)) - require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectPut)) - require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), acl.OpObjectGet)) + require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodMoveTreeNode, treeID)) + require.Error(t, s.verifyClient(context.Background(), req, cid1, req.GetBody().GetBearerToken(), nativeschema.MethodGetSubtree, treeID)) }) }) } func testBearerToken(cid cid.ID, forPutGet, forGet *keys.PublicKey) bearer.Token { var b bearer.Token - b.SetEACLTable(*testTable(cid, forPutGet, forGet)) + b.SetExp(100) + + allowsGetOpForAnyRoleForBothKeys := chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{ + Names: []string{nativeschema.MethodGetTreeNodeByPath, + nativeschema.MethodGetSubtree, + nativeschema.MethodListTrees}, + }, + Resources: chain.Resources{ + Names: []string{ + nativeschema.ResourceFormatRootTrees, + }, + }, + Condition: []chain.Condition{ + { + Op: chain.CondSliceContains, + Kind: chain.KindRequest, + Key: nativeschema.PropertyKeyActorPublicKey, + Value: fmt.Sprintf("%s\x00%s", hex.EncodeToString(forPutGet.Bytes()), hex.EncodeToString(forGet.Bytes())), + }, + }, + }, + }, + } + + allowsPutOpForAnyRoleForOneKeyOnly := chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.Allow, + Actions: chain.Actions{ + Names: []string{nativeschema.MethodAddTreeNode, + nativeschema.MethodAddTreeNodeByPath, + nativeschema.MethodRemoveTreeNode, + nativeschema.MethodMoveTreeNode, + }, + }, + Resources: chain.Resources{ + Names: []string{ + nativeschema.ResourceFormatRootTrees, + }, + }, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: nativeschema.PropertyKeyActorPublicKey, + Value: hex.EncodeToString(forPutGet.Bytes()), + }, + }, + }, + }, + } + + deniesGetPutOpForOthers := chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{nativeschema.MethodAddTreeNode, + nativeschema.MethodAddTreeNodeByPath, + nativeschema.MethodRemoveTreeNode, + nativeschema.MethodMoveTreeNode, + nativeschema.MethodGetTreeNodeByPath, + nativeschema.MethodGetSubtree, + nativeschema.MethodListTrees}, + }, + Resources: chain.Resources{ + Names: []string{nativeschema.ResourceFormatRootTrees}, + }, + Condition: []chain.Condition{ + { + Op: chain.CondStringEquals, + Kind: chain.KindRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + }, + { + Op: chain.CondStringNotEquals, + Kind: chain.KindRequest, + Key: nativeschema.PropertyKeyActorPublicKey, + Value: hex.EncodeToString(forPutGet.Bytes()), + }, + { + Op: chain.CondStringNotEquals, + Kind: chain.KindRequest, + Key: nativeschema.PropertyKeyActorPublicKey, + Value: hex.EncodeToString(forGet.Bytes()), + }, + }, + }, + }, + } + + b.SetAPEOverride(bearer.APEOverride{ + Target: ape.ChainTarget{ + TargetType: ape.TargetTypeContainer, + Name: cid.EncodeToString(), + }, + Chains: []ape.Chain{ + { + Raw: allowsGetOpForAnyRoleForBothKeys.Bytes(), + }, + { + Raw: allowsPutOpForAnyRoleForOneKeyOnly.Bytes(), + }, + { + Raw: deniesGetPutOpForOthers.Bytes(), + }, + }, + }) return b } - -func testTable(cid cid.ID, forPutGet, forGet *keys.PublicKey) *eaclSDK.Table { - tgtGet := eaclSDK.NewTarget() - tgtGet.SetRole(eaclSDK.RoleUnknown) - tgtGet.SetBinaryKeys([][]byte{forPutGet.Bytes(), forGet.Bytes()}) - - rGet := eaclSDK.NewRecord() - rGet.SetAction(eaclSDK.ActionAllow) - rGet.SetOperation(eaclSDK.OperationGet) - rGet.SetTargets(*tgtGet) - - tgtPut := eaclSDK.NewTarget() - tgtPut.SetRole(eaclSDK.RoleUnknown) - tgtPut.SetBinaryKeys([][]byte{forPutGet.Bytes()}) - - rPut := eaclSDK.NewRecord() - rPut.SetAction(eaclSDK.ActionAllow) - rPut.SetOperation(eaclSDK.OperationPut) - rPut.SetTargets(*tgtPut) - - tb := eaclSDK.NewTable() - tb.AddRecord(rGet) - tb.AddRecord(rPut) - - tgt := eaclSDK.NewTarget() - tgt.SetRole(eaclSDK.RoleOthers) - - for _, op := range []eaclSDK.Operation{eaclSDK.OperationGet, eaclSDK.OperationPut} { - r := eaclSDK.NewRecord() - r.SetAction(eaclSDK.ActionDeny) - r.SetTargets(*tgt) - r.SetOperation(op) - tb.AddRecord(r) - } - - tb.SetCID(cid) - - return tb -}