diff --git a/cmd/frostfs-node/config.go b/cmd/frostfs-node/config.go index 1b3e094d9..74bf6a5a9 100644 --- a/cmd/frostfs-node/config.go +++ b/cmd/frostfs-node/config.go @@ -449,6 +449,7 @@ type cfg struct { cfgMorph cfgMorph cfgAccounting cfgAccounting cfgContainer cfgContainer + cfgFrostfsID cfgFrostfsID cfgNodeInfo cfgNodeInfo cfgNetmap cfgNetmap cfgControlService cfgControlService @@ -569,6 +570,10 @@ type cfgContainer struct { workerPool util.WorkerPool // pool for asynchronous handlers } +type cfgFrostfsID struct { + scriptHash neogoutil.Uint160 +} + type cfgNetmap struct { scriptHash neogoutil.Uint160 wrapper *nmClient.Client @@ -681,6 +686,8 @@ func initCfg(appCfg *config.Config) *cfg { } c.cfgContainer = initContainer(appCfg) + c.cfgFrostfsID = initFrostfsID(appCfg) + c.cfgNetmap = initNetmap(appCfg, netState, relayOnly) c.cfgGRPC = initCfgGRPC() @@ -779,6 +786,12 @@ func initContainer(appCfg *config.Config) cfgContainer { } } +func initFrostfsID(appCfg *config.Config) cfgFrostfsID { + return cfgFrostfsID{ + scriptHash: contractsconfig.FrostfsID(appCfg), + } +} + func initCfgGRPC() cfgGRPC { maxChunkSize := uint64(maxMsgSize) * 3 / 4 // 25% to meta, 75% to payload maxAddrAmount := uint64(maxChunkSize) / addressSize // each address is about 72 bytes diff --git a/cmd/frostfs-node/config/contracts/config.go b/cmd/frostfs-node/config/contracts/config.go index c5f14f3ca..df0c0b958 100644 --- a/cmd/frostfs-node/config/contracts/config.go +++ b/cmd/frostfs-node/config/contracts/config.go @@ -38,6 +38,10 @@ func Container(c *config.Config) util.Uint160 { return contractAddress(c, "container") } +func FrostfsID(c *config.Config) util.Uint160 { + return contractAddress(c, "frostfsid") +} + // Proxy returnsthe value of "proxy" config parameter // from "contracts" section. // diff --git a/cmd/frostfs-node/container.go b/cmd/frostfs-node/container.go index 898a4eef9..28f271075 100644 --- a/cmd/frostfs-node/container.go +++ b/cmd/frostfs-node/container.go @@ -9,6 +9,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs" containerCore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" cntClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container" + "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/frostfsid" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/event" containerEvent "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/event/container" containerTransportGRPC "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network/transport/container/grpc" @@ -32,11 +33,14 @@ func initContainerService(_ context.Context, c *cfg) { cnrRdr, cnrWrt := configureEACLAndContainerSources(c, wrap, cnrSrc) + frostFSIDClient, err := frostfsid.NewFromMorph(c.cfgMorph.client, c.cfgFrostfsID.scriptHash, 0) + fatalOnErr(err) + server := containerTransportGRPC.New( containerService.NewSignService( &c.key.PrivateKey, containerService.NewAPEServer(c.cfgObject.cfgAccessPolicyEngine.accessPolicyEngine, cnrRdr, - newCachedIRFetcher(createInnerRingFetcher(c)), c.netMapSource, + newCachedIRFetcher(createInnerRingFetcher(c)), c.netMapSource, frostFSIDClient, containerService.NewExecutionService(containerMorph.NewExecutor(cnrRdr, cnrWrt), c.respSvc), ), ), diff --git a/cmd/frostfs-node/morph.go b/cmd/frostfs-node/morph.go index d26142370..698fb3b83 100644 --- a/cmd/frostfs-node/morph.go +++ b/cmd/frostfs-node/morph.go @@ -288,6 +288,7 @@ func lookupScriptHashesInNNS(c *cfg) { {&c.cfgNetmap.scriptHash, client.NNSNetmapContractName}, {&c.cfgAccounting.scriptHash, client.NNSBalanceContractName}, {&c.cfgContainer.scriptHash, client.NNSContainerContractName}, + {&c.cfgFrostfsID.scriptHash, client.NNSFrostFSIDContractName}, {&c.cfgMorph.proxyScriptHash, client.NNSProxyContractName}, {&c.cfgObject.cfgAccessPolicyEngine.policyContractHash, client.NNSPolicyContractName}, } diff --git a/go.mod b/go.mod index 3b269db34..a5cffc28c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240112150928-72885aae835c git.frostfs.info/TrueCloudLab/frostfs-contract v0.18.1-0.20240115082915-f2a82aa635aa git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65 - git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240117145620-110b7e41706e + git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240126141009-65b4525b3bf0 git.frostfs.info/TrueCloudLab/hrw v1.2.1 git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240122104724-06cbfe8691ad git.frostfs.info/TrueCloudLab/tzhash v1.8.0 diff --git a/go.sum b/go.sum index ca28fdaec..4462c3575 100644 Binary files a/go.sum and b/go.sum differ diff --git a/pkg/morph/client/frostfsid/subject.go b/pkg/morph/client/frostfsid/subject.go index b14675d58..52ae98ba5 100644 --- a/pkg/morph/client/frostfsid/subject.go +++ b/pkg/morph/client/frostfsid/subject.go @@ -33,16 +33,17 @@ func (c *Client) GetSubject(addr util.Uint160) (*frostfsidclient.Subject, error) return subj, nil } -// parseSubject from https://git.frostfs.info/TrueCloudLab/frostfs-contract/src/commit/dd5919348da9731f24504e7bc485516c2ba5f11c/frostfsid/client/client.go#L592 -func parseSubject(structArr []stackitem.Item) (*frostfsidclient.Subject, error) { - if len(structArr) < 5 { - return nil, errors.New("invalid response subject struct") +func parseSubject(res []stackitem.Item) (*frostfsidclient.Subject, error) { + if ln := len(res); ln != 1 { + return nil, fmt.Errorf("unexpected stack item count (%s): %d", methodGetSubject, ln) } - var ( - err error - subj frostfsidclient.Subject - ) + structArr, err := client.ArrayFromStackItem(res[0]) + if err != nil { + return nil, fmt.Errorf("could not get item array of container (%s): %w", methodGetSubject, err) + } + + var subj frostfsidclient.Subject subj.PrimaryKey, err = unwrap.PublicKey(makeValidRes(structArr[0])) if err != nil { diff --git a/pkg/services/container/ape.go b/pkg/services/container/ape.go index 36edf4516..c57e54d08 100644 --- a/pkg/services/container/ape.go +++ b/pkg/services/container/ape.go @@ -8,14 +8,17 @@ import ( "crypto/sha256" "errors" "fmt" + "strings" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" + "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap" "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" + cnrSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" sessionSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session" @@ -25,6 +28,7 @@ import ( "git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource" 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" ) var ( @@ -35,6 +39,7 @@ var ( errInvalidSessionTokenOwner = errors.New("malformed request: invalid session token owner") errEmptyBodySignature = errors.New("malformed request: empty body signature") errMissingOwnerID = errors.New("malformed request: missing owner ID") + errSubjectNotFound = errors.New("subject not found") undefinedContainerID = cid.ID{} ) @@ -47,22 +52,29 @@ type containers interface { Get(cid.ID) (*containercore.Container, error) } +type frostfsidSubjectProvider interface { + GetSubject(util.Uint160) (*client.Subject, error) +} + type apeChecker struct { router policyengine.ChainRouter reader containers ir ir nm netmap.Source + frostFSIDClient frostfsidSubjectProvider + next Server } -func NewAPEServer(router policyengine.ChainRouter, reader containers, ir ir, nm netmap.Source, srv Server) Server { +func NewAPEServer(router policyengine.ChainRouter, reader containers, ir ir, nm netmap.Source, frostFSIDClient frostfsidSubjectProvider, srv Server) Server { return &apeChecker{ - router: router, - reader: reader, - ir: ir, - next: srv, - nm: nm, + router: router, + reader: reader, + ir: ir, + next: srv, + nm: nm, + frostFSIDClient: frostFSIDClient, } } @@ -125,9 +137,17 @@ func (ac *apeChecker) List(ctx context.Context, req *container.ListRequest) (*co nativeschema.PropertyKeyActorRole: role, } + namespace, err := ac.namespaceByOwner(req.GetBody().GetOwnerID()) + if err != nil { + return nil, fmt.Errorf("could not get owner namespace: %w", err) + } + if err := ac.validateNamespaceByPublicKey(pk, namespace); err != nil { + return nil, err + } + request := &apeRequest{ resource: &apeResource{ - name: nativeschema.ResourceFormatRootContainers, + name: resourceName(namespace, ""), props: make(map[string]string), }, op: nativeschema.MethodListContainers, @@ -135,7 +155,7 @@ func (ac *apeChecker) List(ctx context.Context, req *container.ListRequest) (*co } s, found, err := ac.router.IsAllowed(apechain.Ingress, - policyengine.NewRequestTargetWithNamespace(""), + policyengine.NewRequestTargetWithNamespace(namespace), request) if err != nil { return nil, err @@ -162,9 +182,17 @@ func (ac *apeChecker) Put(ctx context.Context, req *container.PutRequest) (*cont nativeschema.PropertyKeyActorRole: role, } + namespace, err := ac.namespaceByOwner(req.GetBody().GetContainer().GetOwnerID()) + if err != nil { + return nil, fmt.Errorf("get namespace error: %w", err) + } + if err = validateNamespace(req.GetBody().GetContainer(), namespace); err != nil { + return nil, err + } + request := &apeRequest{ resource: &apeResource{ - name: nativeschema.ResourceFormatRootContainers, + name: resourceName(namespace, ""), props: make(map[string]string), }, op: nativeschema.MethodPutContainer, @@ -172,7 +200,7 @@ func (ac *apeChecker) Put(ctx context.Context, req *container.PutRequest) (*cont } s, found, err := ac.router.IsAllowed(apechain.Ingress, - policyengine.NewRequestTargetWithNamespace(""), + policyengine.NewRequestTargetWithNamespace(namespace), request) if err != nil { return nil, err @@ -251,9 +279,15 @@ func (ac *apeChecker) validateContainerBoundedOperation(containerID *refs.Contai return err } + namespace := "" + cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cont.Value).Zone(), ".ns") + if hasNamespace { + namespace = cntNamespace + } + request := &apeRequest{ resource: &apeResource{ - name: fmt.Sprintf(nativeschema.ResourceFormatRootContainer, id.EncodeToString()), + name: resourceName(namespace, id.EncodeToString()), props: ac.getContainerProps(cont), }, op: op, @@ -261,7 +295,7 @@ func (ac *apeChecker) validateContainerBoundedOperation(containerID *refs.Contai } s, found, err := ac.router.IsAllowed(apechain.Ingress, - policyengine.NewRequestTargetWithContainer(id.EncodeToString()), + policyengine.NewRequestTarget(cntNamespace, id.EncodeToString()), request) if err != nil { return err @@ -326,6 +360,19 @@ func (r *apeResource) Property(key string) string { return r.props[key] } +func resourceName(namespace string, container string) string { + if namespace == "" && container == "" { + return nativeschema.ResourceFormatRootContainers + } + if namespace == "" && container != "" { + return fmt.Sprintf(nativeschema.ResourceFormatRootContainer, container) + } + if namespace != "" && container == "" { + return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, namespace) + } + return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainer, namespace, container) +} + func (ac *apeChecker) getContainerProps(c *containercore.Container) map[string]string { return map[string]string{ nativeschema.PropertyKeyContainerOwnerID: c.Value.Owner().EncodeToString(), @@ -514,3 +561,70 @@ func isContainerNode(nm *netmapSDK.NetMap, pk, binCnrID []byte, cont *containerc return false, nil } + +func (ac *apeChecker) namespaceByOwner(owner *refs.OwnerID) (string, error) { + var ownerSDK user.ID + if owner == nil { + return "", fmt.Errorf("owner id is not set") + } + if err := ownerSDK.ReadFromV2(*owner); err != nil { + return "", err + } + addr, err := ownerSDK.ScriptHash() + if err != nil { + return "", err + } + + namespace := "" + subject, err := ac.frostFSIDClient.GetSubject(addr) + if err == nil { + namespace = subject.Namespace + } else { + if !strings.Contains(err.Error(), errSubjectNotFound.Error()) { + return "", fmt.Errorf("get subject error: %w", err) + } + } + return namespace, nil +} + +// validateNamespace validates a namespace set in a container. +// If frostfs-id contract stores a namespace N1 for an owner ID and a container within a request +// is set with namespace N2 (via Zone() property), then N2 is invalid and the request is denied. +func validateNamespace(cnrV2 *container.Container, ownerIDNamespace string) error { + if cnrV2 == nil { + return nil + } + var cnr cnrSDK.Container + if err := cnr.ReadFromV2(*cnrV2); err != nil { + return err + } + cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cnr).Zone(), ".ns") + if hasNamespace { + if cntNamespace != ownerIDNamespace { + if ownerIDNamespace == "" { + return fmt.Errorf("invalid domain zone: no namespace is expected") + } + return fmt.Errorf("invalid domain zone: expected namespace %s, but got %s", ownerIDNamespace, cntNamespace) + } + } else if ownerIDNamespace != "" { + return fmt.Errorf("invalid domain zone: expected namespace %s, but got invalid or empty", ownerIDNamespace) + } + return nil +} + +// validateNamespace validates if a namespace of a request actor equals to owner's namespace. +// An actor's namespace is calculated by a public key. +func (ac *apeChecker) validateNamespaceByPublicKey(pk *keys.PublicKey, ownerIDNamespace string) error { + var actor user.ID + user.IDFromKey(&actor, (ecdsa.PublicKey)(*pk)) + actorOwnerID := new(refs.OwnerID) + actor.WriteToV2(actorOwnerID) + actorNamespace, err := ac.namespaceByOwner(actorOwnerID) + if err != nil { + return fmt.Errorf("could not get actor namespace: %w", err) + } + if actorNamespace != ownerIDNamespace { + return fmt.Errorf("actor namespace %s differs from owner: %s", actorNamespace, ownerIDNamespace) + } + return nil +} diff --git a/pkg/services/container/ape_test.go b/pkg/services/container/ape_test.go index f4dfbe50f..5344d9f23 100644 --- a/pkg/services/container/ape_test.go +++ b/pkg/services/container/ape_test.go @@ -2,6 +2,7 @@ package container import ( "context" + "crypto/ecdsa" "errors" "fmt" "testing" @@ -9,10 +10,12 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs" - "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" + session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature" + "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" + cnrSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" containertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/test" @@ -24,16 +27,25 @@ import ( "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" ) +const ( + testDomainName = "testdomainname" + testDomainZone = "testdomainname.ns" +) + func TestAPE(t *testing.T) { t.Parallel() t.Run("deny get container for others", testDenyGetContainerForOthers) t.Run("deny set container eACL for IR", testDenySetContainerEACLForIR) t.Run("deny get container eACL for IR with session token", testDenyGetContainerEACLForIRSessionToken) t.Run("deny put container for others with session token", testDenyPutContainerForOthersSessionToken) + t.Run("deny put container, read namespace from frostfsID", testDenyPutContainerReadNamespaceFromFrostfsID) + t.Run("deny put container with invlaid namespace", testDenyPutContainerInvalidNamespace) t.Run("deny list containers for owner with PK", testDenyListContainersForPK) + t.Run("deny list containers by namespace invalidation", testDenyListContainersValidationNamespaceError) } func testDenyGetContainerForOthers(t *testing.T) { @@ -49,7 +61,10 @@ func testDenyGetContainerForOthers(t *testing.T) { keys: [][]byte{}, } nm := &netmapStub{} - apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + frostfsIDSubjectReader := &frostfsidStub{ + subjects: map[util.Uint160]*client.Subject{}, + } + apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv) contID := cidtest.ID() testContainer := containertest.Container() @@ -122,7 +137,10 @@ func testDenySetContainerEACLForIR(t *testing.T) { keys: [][]byte{}, } nm := &netmapStub{} - apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + frostfsIDSubjectReader := &frostfsidStub{ + subjects: map[util.Uint160]*client.Subject{}, + } + apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv) contID := cidtest.ID() testContainer := containertest.Container() @@ -197,7 +215,10 @@ func testDenyGetContainerEACLForIRSessionToken(t *testing.T) { keys: [][]byte{}, } nm := &netmapStub{} - apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + frostfsIDSubjectReader := &frostfsidStub{ + subjects: map[util.Uint160]*client.Subject{}, + } + apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv) contID := cidtest.ID() testContainer := containertest.Container() @@ -283,7 +304,10 @@ func testDenyPutContainerForOthersSessionToken(t *testing.T) { keys: [][]byte{}, } nm := &netmapStub{} - apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + frostfsIDSubjectReader := &frostfsidStub{ + subjects: map[util.Uint160]*client.Subject{}, + } + apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv) testContainer := containertest.Container() @@ -317,26 +341,7 @@ func testDenyPutContainerForOthersSessionToken(t *testing.T) { }) require.NoError(t, err) - req := &container.PutRequest{} - req.SetBody(&container.PutRequestBody{}) - var reqCont container.Container - testContainer.WriteToV2(&reqCont) - req.GetBody().SetContainer(&reqCont) - - sessionPK, err := keys.NewPrivateKey() - require.NoError(t, err) - sToken := sessiontest.ContainerSigned() - sToken.ApplyOnlyTo(cid.ID{}) - require.NoError(t, sToken.Sign(sessionPK.PrivateKey)) - var sTokenV2 session.Token - sToken.WriteToV2(&sTokenV2) - metaHeader := new(session.RequestMetaHeader) - metaHeader.SetSessionToken(&sTokenV2) - req.SetMetaHeader(metaHeader) - - pk, err := keys.NewPrivateKey() - require.NoError(t, err) - require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req)) + req := initPutRequest(t, testContainer) resp, err := apeSrv.Put(context.Background(), req) require.Nil(t, resp) @@ -344,6 +349,139 @@ func testDenyPutContainerForOthersSessionToken(t *testing.T) { require.ErrorAs(t, err, &errAccessDenied) } +func testDenyPutContainerReadNamespaceFromFrostfsID(t *testing.T) { + t.Parallel() + srv := &srvStub{ + calls: map[string]int{}, + } + router := inmemory.NewInMemory() + contRdr := &containerStub{ + c: map[cid.ID]*containercore.Container{}, + } + ir := &irStub{ + keys: [][]byte{}, + } + nm := &netmapStub{} + + cnrID, testContainer := initTestContainer(t, true) + contRdr.c[cnrID] = &containercore.Container{Value: testContainer} + + nm.currentEpoch = 100 + nm.netmaps = map[uint64]*netmap.NetMap{} + + _, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodPutContainer, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := initPutRequest(t, testContainer) + ownerScriptHash := initOwnerIDScriptHash(t, testContainer) + + frostfsIDSubjectReader := &frostfsidStub{ + subjects: map[util.Uint160]*client.Subject{ + ownerScriptHash: { + Namespace: testDomainName, + Name: testDomainName, + }, + }, + } + apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv) + resp, err := apeSrv.Put(context.Background(), req) + require.Nil(t, resp) + var errAccessDenied *apistatus.ObjectAccessDenied + require.ErrorAs(t, err, &errAccessDenied) +} + +func testDenyPutContainerInvalidNamespace(t *testing.T) { + t.Parallel() + srv := &srvStub{ + calls: map[string]int{}, + } + router := inmemory.NewInMemory() + contRdr := &containerStub{ + c: map[cid.ID]*containercore.Container{}, + } + ir := &irStub{ + keys: [][]byte{}, + } + nm := &netmapStub{} + + cnrID, testContainer := initTestContainer(t, false) + var domain cnrSDK.Domain + domain.SetName("incorrect" + testDomainName) + domain.SetZone("incorrect" + testDomainZone) + cnrSDK.WriteDomain(&testContainer, domain) + contRdr.c[cnrID] = &containercore.Container{Value: testContainer} + + nm.currentEpoch = 100 + nm.netmaps = map[uint64]*netmap.NetMap{} + + _, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodPutContainer, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := initPutRequest(t, testContainer) + ownerScriptHash := initOwnerIDScriptHash(t, testContainer) + + frostfsIDSubjectReader := &frostfsidStub{ + subjects: map[util.Uint160]*client.Subject{ + ownerScriptHash: { + Namespace: testDomainName, + Name: testDomainName, + }, + }, + } + apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv) + resp, err := apeSrv.Put(context.Background(), req) + require.Nil(t, resp) + require.ErrorContains(t, err, "invalid domain zone") +} + func testDenyListContainersForPK(t *testing.T) { t.Parallel() srv := &srvStub{ @@ -357,7 +495,10 @@ func testDenyListContainersForPK(t *testing.T) { keys: [][]byte{}, } nm := &netmapStub{} - apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + frostfsIDSubjectReader := &frostfsidStub{ + subjects: map[util.Uint160]*client.Subject{}, + } + apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv) nm.currentEpoch = 100 nm.netmaps = map[uint64]*netmap.NetMap{} @@ -409,6 +550,82 @@ func testDenyListContainersForPK(t *testing.T) { require.ErrorAs(t, err, &errAccessDenied) } +func testDenyListContainersValidationNamespaceError(t *testing.T) { + t.Parallel() + srv := &srvStub{ + calls: map[string]int{}, + } + router := inmemory.NewInMemory() + contRdr := &containerStub{ + c: map[cid.ID]*containercore.Container{}, + } + ir := &irStub{ + keys: [][]byte{}, + } + nm := &netmapStub{} + + actorPK, err := keys.NewPrivateKey() + require.NoError(t, err) + + ownerPK, err := keys.NewPrivateKey() + require.NoError(t, err) + + actorScriptHash, ownerScriptHash := initActorOwnerScriptHashes(t, actorPK, ownerPK) + + const actorDomain = "actor" + testDomainName + + frostfsIDSubjectReader := &frostfsidStub{ + subjects: map[util.Uint160]*client.Subject{ + actorScriptHash: { + Namespace: actorDomain, + Name: actorDomain, + }, + ownerScriptHash: { + Namespace: testDomainName, + Name: testDomainName, + }, + }, + } + + apeSrv := NewAPEServer(router, contRdr, ir, nm, frostfsIDSubjectReader, srv) + + nm.currentEpoch = 100 + nm.netmaps = map[uint64]*netmap.NetMap{} + + _, _, err = router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(""), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodListContainers, + }, + }, + Resources: chain.Resources{ + Names: []string{ + nativeschema.ResourceFormatRootContainers, + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorPublicKey, + Value: actorPK.PublicKey().String(), + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := initListRequest(t, actorPK, ownerPK) + + resp, err := apeSrv.List(context.Background(), req) + require.Nil(t, resp) + require.ErrorContains(t, err, "actor namespace "+actorDomain+" differs") +} + type srvStub struct { calls map[string]int } @@ -489,3 +706,424 @@ func (s *netmapStub) GetNetMapByEpoch(epoch uint64) (*netmap.NetMap, error) { func (s *netmapStub) Epoch() (uint64, error) { return s.currentEpoch, nil } + +type frostfsidStub struct { + subjects map[util.Uint160]*client.Subject +} + +func (f *frostfsidStub) GetSubject(owner util.Uint160) (*client.Subject, error) { + s, ok := f.subjects[owner] + if !ok { + return nil, errSubjectNotFound + } + return s, nil +} + +type testAPEServer struct { + engine engine.Engine + + containerReader *containerStub + + ir *irStub + + netmap *netmapStub + + frostfsIDSubjectReader *frostfsidStub + + apeChecker *apeChecker +} + +func newTestAPEServer() testAPEServer { + srv := &srvStub{ + calls: map[string]int{}, + } + + engine := inmemory.NewInMemory() + + containerReader := &containerStub{ + c: map[cid.ID]*containercore.Container{}, + } + + ir := &irStub{ + keys: [][]byte{}, + } + + netmap := &netmapStub{} + + frostfsIDSubjectReader := &frostfsidStub{ + subjects: map[util.Uint160]*client.Subject{}, + } + + apeChecker := &apeChecker{ + router: engine, + reader: containerReader, + ir: ir, + nm: netmap, + frostFSIDClient: frostfsIDSubjectReader, + next: srv, + } + + return testAPEServer{ + engine: engine, + containerReader: containerReader, + ir: ir, + netmap: netmap, + frostfsIDSubjectReader: frostfsIDSubjectReader, + apeChecker: apeChecker, + } +} + +func TestValidateContainerBoundedOperation(t *testing.T) { + t.Parallel() + + t.Run("check root-defined container in root-defined container target rule", func(t *testing.T) { + t.Parallel() + + components := newTestAPEServer() + contID, testContainer := initTestContainer(t, false) + components.containerReader.c[contID] = &containercore.Container{Value: testContainer} + initTestNetmap(components.netmap) + + _, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodGetContainer, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := initTestGetContainerRequest(t, contID) + + err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer) + aErr := apeErr(nativeschema.MethodGetContainer, chain.AccessDenied) + require.ErrorContains(t, err, aErr.Error()) + }) + + t.Run("check root-defined container in testdomain-defined container target rule", func(t *testing.T) { + t.Parallel() + + components := newTestAPEServer() + contID, testContainer := initTestContainer(t, false) + components.containerReader.c[contID] = &containercore.Container{Value: testContainer} + initTestNetmap(components.netmap) + + _, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodGetContainer, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainer, testDomainName, contID.EncodeToString()), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := initTestGetContainerRequest(t, contID) + + err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer) + require.NoError(t, err) + }) + + t.Run("check root-defined container in testdomain namespace target rule", func(t *testing.T) { + t.Parallel() + + components := newTestAPEServer() + contID, testContainer := initTestContainer(t, false) + components.containerReader.c[contID] = &containercore.Container{Value: testContainer} + initTestNetmap(components.netmap) + + _, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodGetContainer, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := initTestGetContainerRequest(t, contID) + + err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer) + require.NoError(t, err) + }) + + t.Run("check testdomain-defined container in root-defined container target rule", func(t *testing.T) { + t.Parallel() + + components := newTestAPEServer() + contID, testContainer := initTestContainer(t, true) + components.containerReader.c[contID] = &containercore.Container{Value: testContainer} + initTestNetmap(components.netmap) + + _, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodGetContainer, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := initTestGetContainerRequest(t, contID) + + err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer) + require.NoError(t, err) + }) + + t.Run("check testdomain-defined container in testdomain-defined container target rule", func(t *testing.T) { + t.Parallel() + + components := newTestAPEServer() + contID, testContainer := initTestContainer(t, true) + components.containerReader.c[contID] = &containercore.Container{Value: testContainer} + initTestNetmap(components.netmap) + + _, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodGetContainer, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainer, testDomainName, contID.EncodeToString()), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := initTestGetContainerRequest(t, contID) + + err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer) + aErr := apeErr(nativeschema.MethodGetContainer, chain.AccessDenied) + require.ErrorContains(t, err, aErr.Error()) + }) + + t.Run("check testdomain-defined container in testdomain namespace target rule", func(t *testing.T) { + t.Parallel() + + components := newTestAPEServer() + contID, testContainer := initTestContainer(t, true) + components.containerReader.c[contID] = &containercore.Container{Value: testContainer} + initTestNetmap(components.netmap) + + _, _, err := components.engine.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.NamespaceTarget(testDomainName), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodGetContainer, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, testDomainName), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := initTestGetContainerRequest(t, contID) + + err = components.apeChecker.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), nativeschema.MethodGetContainer) + aErr := apeErr(nativeschema.MethodGetContainer, chain.AccessDenied) + require.ErrorContains(t, err, aErr.Error()) + }) +} + +func initTestGetContainerRequest(t *testing.T, contID cid.ID) *container.GetRequest { + req := &container.GetRequest{} + req.SetBody(&container.GetRequestBody{}) + var refContID refs.ContainerID + contID.WriteToV2(&refContID) + req.GetBody().SetContainerID(&refContID) + + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req)) + return req +} + +func initTestNetmap(netmapStub *netmapStub) { + netmapStub.currentEpoch = 100 + netmapStub.netmaps = map[uint64]*netmap.NetMap{} + var testNetmap netmap.NetMap + testNetmap.SetEpoch(netmapStub.currentEpoch) + testNetmap.SetNodes([]netmap.NodeInfo{{}}) + netmapStub.netmaps[netmapStub.currentEpoch] = &testNetmap + netmapStub.netmaps[netmapStub.currentEpoch-1] = &testNetmap +} + +func initTestContainer(t *testing.T, isDomainSet bool) (cid.ID, cnrSDK.Container) { + contID := cidtest.ID() + testContainer := containertest.Container() + pp := netmap.PlacementPolicy{} + require.NoError(t, pp.DecodeString("REP 1")) + testContainer.SetPlacementPolicy(pp) + if isDomainSet { + // no domain defined -> container is defined in root namespace + var domain cnrSDK.Domain + domain.SetName(testDomainName) + domain.SetZone(testDomainZone) + cnrSDK.WriteDomain(&testContainer, domain) + } + return contID, testContainer +} + +func initPutRequest(t *testing.T, testContainer cnrSDK.Container) *container.PutRequest { + req := &container.PutRequest{} + req.SetBody(&container.PutRequestBody{}) + var reqCont container.Container + testContainer.WriteToV2(&reqCont) + req.GetBody().SetContainer(&reqCont) + + sessionPK, err := keys.NewPrivateKey() + require.NoError(t, err) + sToken := sessiontest.ContainerSigned() + sToken.ApplyOnlyTo(cid.ID{}) + require.NoError(t, sToken.Sign(sessionPK.PrivateKey)) + var sTokenV2 session.Token + sToken.WriteToV2(&sTokenV2) + metaHeader := new(session.RequestMetaHeader) + metaHeader.SetSessionToken(&sTokenV2) + req.SetMetaHeader(metaHeader) + + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req)) + + return req +} + +func initOwnerIDScriptHash(t *testing.T, testContainer cnrSDK.Container) util.Uint160 { + var ownerSDK *user.ID + owner := testContainer.Owner() + ownerSDK = &owner + sc, err := ownerSDK.ScriptHash() + require.NoError(t, err) + return sc +} + +func initActorOwnerScriptHashes(t *testing.T, actorPK *keys.PrivateKey, ownerPK *keys.PrivateKey) (actorScriptHash util.Uint160, ownerScriptHash util.Uint160) { + var actorUserID user.ID + user.IDFromKey(&actorUserID, ecdsa.PublicKey(*actorPK.PublicKey())) + var err error + actorScriptHash, err = actorUserID.ScriptHash() + require.NoError(t, err) + + var ownerUserID user.ID + user.IDFromKey(&ownerUserID, ecdsa.PublicKey(*ownerPK.PublicKey())) + ownerScriptHash, err = ownerUserID.ScriptHash() + require.NoError(t, err) + require.NotEqual(t, ownerScriptHash.String(), actorScriptHash.String()) + return +} + +func initListRequest(t *testing.T, actorPK *keys.PrivateKey, ownerPK *keys.PrivateKey) *container.ListRequest { + var ownerUserID user.ID + user.IDFromKey(&ownerUserID, ownerPK.PrivateKey.PublicKey) + + req := &container.ListRequest{} + req.SetBody(&container.ListRequestBody{}) + var ownerID refs.OwnerID + ownerUserID.WriteToV2(&ownerID) + req.GetBody().SetOwnerID(&ownerID) + + require.NoError(t, signature.SignServiceMessage(&actorPK.PrivateKey, req)) + return req +} diff --git a/pkg/services/object/acl/v2/service.go b/pkg/services/object/acl/v2/service.go index ed077411c..59535dc8a 100644 --- a/pkg/services/object/acl/v2/service.go +++ b/pkg/services/object/acl/v2/service.go @@ -4,8 +4,8 @@ import ( "context" "errors" "fmt" + "strings" - containerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container" objectV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object" "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" @@ -14,6 +14,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger" 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" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" @@ -744,7 +745,11 @@ func (b Service) findRequestInfo(req MetaWithToken, idCnr cid.ID, op acl.Op) (in info.operation = op info.cnrOwner = cnr.Value.Owner() info.idCnr = idCnr - info.cnrNamespace = cnr.Value.Attribute(containerV2.SysAttributeZone) + + cnrNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cnr.Value).Zone(), ".ns") + if hasNamespace { + info.cnrNamespace = cnrNamespace + } // it is assumed that at the moment the key will be valid, // otherwise the request would not pass validation