diff --git a/cmd/frostfs-node/container.go b/cmd/frostfs-node/container.go index 4fc7f5649..2b9bdd71b 100644 --- a/cmd/frostfs-node/container.go +++ b/cmd/frostfs-node/container.go @@ -35,7 +35,10 @@ func initContainerService(_ context.Context, c *cfg) { server := containerTransportGRPC.New( containerService.NewSignService( &c.key.PrivateKey, - containerService.NewExecutionService(containerMorph.NewExecutor(cnrRdr, cnrWrt), c.respSvc), + containerService.NewAPEServer(c.cfgObject.cfgAccessPolicyEngine.accessPolicyEngine, cnrRdr, + newCachedIRFetcher(createInnerRingFetcher(c)), c.netMapSource, + containerService.NewExecutionService(containerMorph.NewExecutor(cnrRdr, cnrWrt), c.respSvc), + ), ), ) diff --git a/pkg/services/container/ape.go b/pkg/services/container/ape.go new file mode 100644 index 000000000..e1ef43c63 --- /dev/null +++ b/pkg/services/container/ape.go @@ -0,0 +1,514 @@ +package container + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" + "errors" + "fmt" + + "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" + 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" + 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" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" + "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" +) + +var ( + errMissingContainerID = errors.New("missing container ID") + errSessionContainerMissmatch = errors.New("requested container is not related to the session") + errMissingVerificationHeader = errors.New("malformed request: empty verification header") + errInvalidSessionTokenSignature = errors.New("malformed request: invalid session token signature") + 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") +) + +type ir interface { + InnerRingKeys() ([][]byte, error) +} + +type containers interface { + Get(cid.ID) (*containercore.Container, error) +} + +type apeChecker struct { + router policyengine.ChainRouter + reader containers + ir ir + nm netmap.Source + + next Server +} + +func NewAPEServer(router policyengine.ChainRouter, reader containers, ir ir, nm netmap.Source, srv Server) Server { + return &apeChecker{ + router: router, + reader: reader, + ir: ir, + next: srv, + nm: nm, + } +} + +func (ac *apeChecker) AnnounceUsedSpace(ctx context.Context, req *container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) { + ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.AnnounceUsedSpace") + defer span.End() + + // this method is not used, so not checked + + return ac.next.AnnounceUsedSpace(ctx, req) +} + +func (ac *apeChecker) Delete(ctx context.Context, req *container.DeleteRequest) (*container.DeleteResponse, error) { + ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.Delete") + defer span.End() + + if err := ac.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), + nativeschema.MethodDeleteContainer); err != nil { + return nil, err + } + + return ac.next.Delete(ctx, req) +} + +func (ac *apeChecker) Get(ctx context.Context, req *container.GetRequest) (*container.GetResponse, error) { + ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.Get") + defer span.End() + + if err := ac.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), + nativeschema.MethodGetContainer); err != nil { + return nil, err + } + + return ac.next.Get(ctx, req) +} + +func (ac *apeChecker) GetExtendedACL(ctx context.Context, req *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) { + ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.GetExtendedACL") + defer span.End() + + if err := ac.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), + nativeschema.MethodGetContainerEACL); err != nil { + return nil, err + } + + return ac.next.GetExtendedACL(ctx, req) +} + +func (ac *apeChecker) List(ctx context.Context, req *container.ListRequest) (*container.ListResponse, error) { + ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.List") + defer span.End() + + role, pk, err := ac.getRoleWithoutContainerID(req.GetBody().GetOwnerID(), req.GetMetaHeader(), req.GetVerificationHeader()) + if err != nil { + return nil, err + } + + reqProps := map[string]string{ + nativeschema.PropertyKeyActorPublicKey: pk.String(), + nativeschema.PropertyKeyActorRole: role, + } + + request := &apeRequest{ + resource: &apeResource{ + name: nativeschema.ResourceFormatRootContainers, + props: make(map[string]string), + }, + op: nativeschema.MethodListContainers, + props: reqProps, + } + + s, found, err := ac.router.IsAllowed(apechain.Ingress, + policyengine.NewRequestTargetWithNamespace(""), + request) + if err != nil { + return nil, err + } + + if !found || s == apechain.Allow { + return ac.next.List(ctx, req) + } + + return nil, apeErr(nativeschema.MethodListContainers, s) +} + +func (ac *apeChecker) Put(ctx context.Context, req *container.PutRequest) (*container.PutResponse, error) { + ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.Put") + defer span.End() + + role, pk, err := ac.getRoleWithoutContainerID(req.GetBody().GetContainer().GetOwnerID(), req.GetMetaHeader(), req.GetVerificationHeader()) + if err != nil { + return nil, err + } + + reqProps := map[string]string{ + nativeschema.PropertyKeyActorPublicKey: pk.String(), + nativeschema.PropertyKeyActorRole: role, + } + + request := &apeRequest{ + resource: &apeResource{ + name: nativeschema.ResourceFormatRootContainers, + props: make(map[string]string), + }, + op: nativeschema.MethodPutContainer, + props: reqProps, + } + + s, found, err := ac.router.IsAllowed(apechain.Ingress, + policyengine.NewRequestTargetWithNamespace(""), + request) + if err != nil { + return nil, err + } + + if !found || s == apechain.Allow { + return ac.next.Put(ctx, req) + } + + return nil, apeErr(nativeschema.MethodPutContainer, s) +} + +func (ac *apeChecker) getRoleWithoutContainerID(oID *refs.OwnerID, mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader) (string, *keys.PublicKey, error) { + if vh == nil { + return "", nil, errMissingVerificationHeader + } + + if oID == nil { + return "", nil, errMissingOwnerID + } + var ownerID user.ID + if err := ownerID.ReadFromV2(*oID); err != nil { + return "", nil, err + } + + actor, pk, err := ac.getActorAndPublicKey(mh, vh, cid.ID{}) + if err != nil { + return "", nil, err + } + + if actor.Equals(ownerID) { + return nativeschema.PropertyValueContainerRoleOwner, pk, nil + } + + pkBytes := pk.Bytes() + isIR, err := ac.isInnerRingKey(pkBytes) + if err != nil { + return "", nil, err + } + if isIR { + return nativeschema.PropertyValueContainerRoleIR, pk, nil + } + + return nativeschema.PropertyValueContainerRoleOthers, pk, nil +} + +func (ac *apeChecker) SetExtendedACL(ctx context.Context, req *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) { + ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.SetExtendedACL") + defer span.End() + + if err := ac.validateContainerBoundedOperation(req.GetBody().GetEACL().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(), + nativeschema.MethodSetContainerEACL); err != nil { + return nil, err + } + + return ac.next.SetExtendedACL(ctx, req) +} + +func (ac *apeChecker) validateContainerBoundedOperation(containerID *refs.ContainerID, mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader, op string) error { + if vh == nil { + return errMissingVerificationHeader + } + + id, err := getContainerID(containerID) + if err != nil { + return err + } + + cont, err := ac.reader.Get(id) + if err != nil { + return err + } + + reqProps, err := ac.getRequestProps(mh, vh, cont, id) + if err != nil { + return err + } + + request := &apeRequest{ + resource: &apeResource{ + name: fmt.Sprintf(nativeschema.ResourceFormatRootContainer, id.EncodeToString()), + props: ac.getContainerProps(cont), + }, + op: op, + props: reqProps, + } + + s, found, err := ac.router.IsAllowed(apechain.Ingress, + policyengine.NewRequestTargetWithContainer(id.EncodeToString()), + request) + if err != nil { + return err + } + + if !found || s == apechain.Allow { + return nil + } + + return apeErr(op, s) +} + +func apeErr(operation string, status apechain.Status) error { + errAccessDenied := &apistatus.ObjectAccessDenied{} + errAccessDenied.WriteReason(fmt.Sprintf("access to container operation %s is denied by access policy engine: %s", operation, status.String())) + return errAccessDenied +} + +func getContainerID(reqContID *refs.ContainerID) (cid.ID, error) { + if reqContID == nil { + return cid.ID{}, errMissingContainerID + } + var id cid.ID + err := id.ReadFromV2(*reqContID) + if err != nil { + return cid.ID{}, fmt.Errorf("invalid container ID: %w", err) + } + return id, nil +} + +type apeRequest struct { + resource *apeResource + op string + props map[string]string +} + +// Operation implements resource.Request. +func (r *apeRequest) Operation() string { + return r.op +} + +// Property implements resource.Request. +func (r *apeRequest) Property(key string) string { + return r.props[key] +} + +// Resource implements resource.Request. +func (r *apeRequest) Resource() resource.Resource { + return r.resource +} + +type apeResource struct { + name string + props map[string]string +} + +func (r *apeResource) Name() string { + return r.name +} + +func (r *apeResource) Property(key string) string { + return r.props[key] +} + +func (ac *apeChecker) getContainerProps(c *containercore.Container) map[string]string { + return map[string]string{ + nativeschema.PropertyKeyContainerOwnerID: c.Value.Owner().EncodeToString(), + } +} + +func (ac *apeChecker) getRequestProps(mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader, + cont *containercore.Container, cnrID cid.ID, +) (map[string]string, error) { + actor, pk, err := ac.getActorAndPublicKey(mh, vh, cnrID) + if err != nil { + return nil, err + } + role, err := ac.getRole(actor, pk, cont, cnrID) + if err != nil { + return nil, err + } + return map[string]string{ + nativeschema.PropertyKeyActorPublicKey: pk.String(), + nativeschema.PropertyKeyActorRole: role, + }, nil +} + +func (ac *apeChecker) getRole(actor *user.ID, pk *keys.PublicKey, cont *containercore.Container, cnrID cid.ID) (string, error) { + if cont.Value.Owner().Equals(*actor) { + return nativeschema.PropertyValueContainerRoleOwner, nil + } + + pkBytes := pk.Bytes() + isIR, err := ac.isInnerRingKey(pkBytes) + if err != nil { + return "", err + } + if isIR { + return nativeschema.PropertyValueContainerRoleIR, nil + } + + isContainer, err := ac.isContainerKey(pkBytes, cnrID, cont) + if err != nil { + return "", err + } + if isContainer { + return nativeschema.PropertyValueContainerRoleContainer, nil + } + + return nativeschema.PropertyValueContainerRoleOthers, nil +} + +func (ac *apeChecker) getActorAndPublicKey(mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader, cnrID cid.ID) (*user.ID, *keys.PublicKey, error) { + st, err := ac.getSessionToken(mh) + if err != nil { + return nil, nil, err + } + + if st != nil { + return ac.getActorAndPKFromSessionToken(st, cnrID) + } + return ac.getActorAndPKFromSignature(vh) +} + +func (ac *apeChecker) getActorAndPKFromSignature(vh *session.RequestVerificationHeader) (*user.ID, *keys.PublicKey, error) { + for vh.GetOrigin() != nil { + vh = vh.GetOrigin() + } + sig := vh.GetBodySignature() + if sig == nil { + return nil, nil, errEmptyBodySignature + } + key, err := keys.NewPublicKeyFromBytes(sig.GetKey(), elliptic.P256()) + if err != nil { + return nil, nil, fmt.Errorf("invalid signature key: %w", err) + } + + var userID user.ID + user.IDFromKey(&userID, (ecdsa.PublicKey)(*key)) + + return &userID, key, nil +} + +func (ac *apeChecker) getSessionToken(mh *session.RequestMetaHeader) (*sessionSDK.Object, error) { + for mh.GetOrigin() != nil { + mh = mh.GetOrigin() + } + st := mh.GetSessionToken() + if st == nil { + return nil, nil + } + + var tok sessionSDK.Object + err := tok.ReadFromV2(*st) + if err != nil { + return nil, fmt.Errorf("invalid session token: %w", err) + } + + return &tok, nil +} + +func (ac *apeChecker) getActorAndPKFromSessionToken(st *sessionSDK.Object, cnrID cid.ID) (*user.ID, *keys.PublicKey, error) { + if !st.AssertContainer(cnrID) { + return nil, nil, errSessionContainerMissmatch + } + if !st.VerifySignature() { + return nil, nil, errInvalidSessionTokenSignature + } + var tok session.Token + st.WriteToV2(&tok) + + signaturePublicKey, err := keys.NewPublicKeyFromBytes(tok.GetSignature().GetKey(), elliptic.P256()) + if err != nil { + return nil, nil, fmt.Errorf("invalid key in session token signature: %w", err) + } + + tokenIssuer := st.Issuer() + if !isOwnerFromKey(tokenIssuer, signaturePublicKey) { + return nil, nil, errInvalidSessionTokenOwner + } + + return &tokenIssuer, signaturePublicKey, nil +} + +func isOwnerFromKey(id user.ID, key *keys.PublicKey) bool { + if key == nil { + return false + } + + var id2 user.ID + user.IDFromKey(&id2, (ecdsa.PublicKey)(*key)) + + return id2.Equals(id) +} + +func (ac *apeChecker) isInnerRingKey(pk []byte) (bool, error) { + innerRingKeys, err := ac.ir.InnerRingKeys() + if err != nil { + return false, err + } + + for i := range innerRingKeys { + if bytes.Equal(innerRingKeys[i], pk) { + return true, nil + } + } + + return false, nil +} + +func (ac *apeChecker) isContainerKey(pk []byte, cnrID cid.ID, cont *containercore.Container) (bool, error) { + binCnrID := make([]byte, sha256.Size) + cnrID.Encode(binCnrID) + + nm, err := netmap.GetLatestNetworkMap(ac.nm) + if err != nil { + return false, err + } + + in, err := isContainerNode(nm, pk, binCnrID, cont) + if err != nil { + return false, err + } else if in { + return true, nil + } + + // then check previous netmap, this can happen in-between epoch change + // when node migrates data from last epoch container + nm, err = netmap.GetPreviousNetworkMap(ac.nm) + if err != nil { + return false, err + } + + return isContainerNode(nm, pk, binCnrID, cont) +} + +func isContainerNode(nm *netmapSDK.NetMap, pk, binCnrID []byte, cont *containercore.Container) (bool, error) { + cnrVectors, err := nm.ContainerNodes(cont.Value.PlacementPolicy(), binCnrID) + if err != nil { + return false, err + } + + for i := range cnrVectors { + for j := range cnrVectors[i] { + if bytes.Equal(cnrVectors[i][j].PublicKey(), pk) { + return true, nil + } + } + } + + return false, nil +} diff --git a/pkg/services/container/ape_test.go b/pkg/services/container/ape_test.go new file mode 100644 index 000000000..b61e68eb5 --- /dev/null +++ b/pkg/services/container/ape_test.go @@ -0,0 +1,491 @@ +package container + +import ( + "context" + "errors" + "fmt" + "testing" + + "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" + "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/signature" + containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" + apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" + 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" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" + sessiontest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session/test" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain" + "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/stretchr/testify/require" +) + +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 list containers for owner with PK", testDenyListContainersForPK) +} + +func testDenyGetContainerForOthers(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{} + apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + + contID := cidtest.ID() + testContainer := containertest.Container() + pp := netmap.PlacementPolicy{} + require.NoError(t, pp.DecodeString("REP 1")) + testContainer.SetPlacementPolicy(pp) + contRdr.c[contID] = &containercore.Container{Value: testContainer} + + nm.currentEpoch = 100 + nm.netmaps = map[uint64]*netmap.NetMap{} + var testNetmap netmap.NetMap + testNetmap.SetEpoch(nm.currentEpoch) + testNetmap.SetNodes([]netmap.NodeInfo{{}}) + nm.netmaps[nm.currentEpoch] = &testNetmap + nm.netmaps[nm.currentEpoch-1] = &testNetmap + + _, _, err := router.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 := &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)) + + resp, err := apeSrv.Get(context.Background(), req) + require.Nil(t, resp) + var errAccessDenied *apistatus.ObjectAccessDenied + require.ErrorAs(t, err, &errAccessDenied) +} + +func testDenySetContainerEACLForIR(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{} + apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + + contID := cidtest.ID() + testContainer := containertest.Container() + pp := netmap.PlacementPolicy{} + require.NoError(t, pp.DecodeString("REP 1")) + testContainer.SetPlacementPolicy(pp) + contRdr.c[contID] = &containercore.Container{Value: testContainer} + + nm.currentEpoch = 100 + nm.netmaps = map[uint64]*netmap.NetMap{} + var testNetmap netmap.NetMap + testNetmap.SetEpoch(nm.currentEpoch) + testNetmap.SetNodes([]netmap.NodeInfo{{}}) + nm.netmaps[nm.currentEpoch] = &testNetmap + nm.netmaps[nm.currentEpoch-1] = &testNetmap + + _, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodSetContainerEACL, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleIR, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := &container.SetExtendedACLRequest{} + req.SetBody(&container.SetExtendedACLRequestBody{}) + var refContID refs.ContainerID + contID.WriteToV2(&refContID) + req.GetBody().SetEACL(&acl.Table{}) + req.GetBody().GetEACL().SetContainerID(&refContID) + + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req)) + ir.keys = append(ir.keys, pk.PublicKey().Bytes()) + + resp, err := apeSrv.SetExtendedACL(context.Background(), req) + require.Nil(t, resp) + var errAccessDenied *apistatus.ObjectAccessDenied + require.ErrorAs(t, err, &errAccessDenied) +} + +func testDenyGetContainerEACLForIRSessionToken(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{} + apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + + contID := cidtest.ID() + testContainer := containertest.Container() + pp := netmap.PlacementPolicy{} + require.NoError(t, pp.DecodeString("REP 1")) + testContainer.SetPlacementPolicy(pp) + contRdr.c[contID] = &containercore.Container{Value: testContainer} + + nm.currentEpoch = 100 + nm.netmaps = map[uint64]*netmap.NetMap{} + var testNetmap netmap.NetMap + testNetmap.SetEpoch(nm.currentEpoch) + testNetmap.SetNodes([]netmap.NodeInfo{{}}) + nm.netmaps[nm.currentEpoch] = &testNetmap + nm.netmaps[nm.currentEpoch-1] = &testNetmap + + _, _, err := router.MorphRuleChainStorage().AddMorphRuleChain(chain.Ingress, engine.ContainerTarget(contID.EncodeToString()), &chain.Chain{ + Rules: []chain.Rule{ + { + Status: chain.AccessDenied, + Actions: chain.Actions{ + Names: []string{ + nativeschema.MethodGetContainerEACL, + }, + }, + Resources: chain.Resources{ + Names: []string{ + fmt.Sprintf(nativeschema.ResourceFormatRootContainer, contID.EncodeToString()), + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleIR, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + req := &container.GetExtendedACLRequest{} + req.SetBody(&container.GetExtendedACLRequestBody{}) + 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)) + + sessionPK, err := keys.NewPrivateKey() + require.NoError(t, err) + sToken := sessiontest.ObjectSigned() + sToken.BindContainer(contID) + require.NoError(t, sToken.Sign(sessionPK.PrivateKey)) + var sTokenV2 session.Token + sToken.WriteToV2(&sTokenV2) + metaHeader := new(session.RequestMetaHeader) + metaHeader.SetSessionToken(&sTokenV2) + req.SetMetaHeader(metaHeader) + + ir.keys = append(ir.keys, sessionPK.PublicKey().Bytes()) + + resp, err := apeSrv.GetExtendedACL(context.Background(), req) + require.Nil(t, resp) + var errAccessDenied *apistatus.ObjectAccessDenied + require.ErrorAs(t, err, &errAccessDenied) +} + +func testDenyPutContainerForOthersSessionToken(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{} + apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + + testContainer := containertest.Container() + + 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.MethodPutContainer, + }, + }, + Resources: chain.Resources{ + Names: []string{ + nativeschema.ResourceFormatRootContainers, + }, + }, + Condition: []chain.Condition{ + { + Object: chain.ObjectRequest, + Key: nativeschema.PropertyKeyActorRole, + Value: nativeschema.PropertyValueContainerRoleOthers, + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + 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.ObjectSigned() + sToken.BindContainer(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)) + + resp, err := apeSrv.Put(context.Background(), req) + require.Nil(t, resp) + var errAccessDenied *apistatus.ObjectAccessDenied + require.ErrorAs(t, err, &errAccessDenied) +} + +func testDenyListContainersForPK(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{} + apeSrv := NewAPEServer(router, contRdr, ir, nm, srv) + + nm.currentEpoch = 100 + nm.netmaps = map[uint64]*netmap.NetMap{} + + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + + _, _, 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: pk.PublicKey().String(), + Op: chain.CondStringEquals, + }, + }, + }, + }, + }) + require.NoError(t, err) + + var userID user.ID + user.IDFromKey(&userID, pk.PrivateKey.PublicKey) + + req := &container.ListRequest{} + req.SetBody(&container.ListRequestBody{}) + var ownerID refs.OwnerID + userID.WriteToV2(&ownerID) + req.GetBody().SetOwnerID(&ownerID) + + require.NoError(t, signature.SignServiceMessage(&pk.PrivateKey, req)) + + resp, err := apeSrv.List(context.Background(), req) + require.Nil(t, resp) + var errAccessDenied *apistatus.ObjectAccessDenied + require.ErrorAs(t, err, &errAccessDenied) +} + +type srvStub struct { + calls map[string]int +} + +func (s *srvStub) AnnounceUsedSpace(context.Context, *container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) { + s.calls["AnnounceUsedSpace"]++ + return &container.AnnounceUsedSpaceResponse{}, nil +} + +func (s *srvStub) Delete(context.Context, *container.DeleteRequest) (*container.DeleteResponse, error) { + s.calls["Delete"]++ + return &container.DeleteResponse{}, nil +} + +func (s *srvStub) Get(context.Context, *container.GetRequest) (*container.GetResponse, error) { + s.calls["Get"]++ + return &container.GetResponse{}, nil +} + +func (s *srvStub) GetExtendedACL(context.Context, *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) { + s.calls["GetExtendedACL"]++ + return &container.GetExtendedACLResponse{}, nil +} + +func (s *srvStub) List(context.Context, *container.ListRequest) (*container.ListResponse, error) { + s.calls["List"]++ + return &container.ListResponse{}, nil +} + +func (s *srvStub) Put(context.Context, *container.PutRequest) (*container.PutResponse, error) { + s.calls["Put"]++ + return &container.PutResponse{}, nil +} + +func (s *srvStub) SetExtendedACL(context.Context, *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) { + s.calls["SetExtendedACL"]++ + return &container.SetExtendedACLResponse{}, nil +} + +type irStub struct { + keys [][]byte +} + +func (s *irStub) InnerRingKeys() ([][]byte, error) { + return s.keys, nil +} + +type containerStub struct { + c map[cid.ID]*containercore.Container +} + +func (s *containerStub) Get(id cid.ID) (*containercore.Container, error) { + if v, ok := s.c[id]; ok { + return v, nil + } + return nil, errors.New("container not found") +} + +type netmapStub struct { + netmaps map[uint64]*netmap.NetMap + currentEpoch uint64 +} + +func (s *netmapStub) GetNetMap(diff uint64) (*netmap.NetMap, error) { + if diff >= s.currentEpoch { + return nil, errors.New("invalid diff") + } + return s.GetNetMapByEpoch(s.currentEpoch - diff) +} + +func (s *netmapStub) GetNetMapByEpoch(epoch uint64) (*netmap.NetMap, error) { + if nm, found := s.netmaps[epoch]; found { + return nm, nil + } + return nil, errors.New("netmap not found") +} + +func (s *netmapStub) Epoch() (uint64, error) { + return s.currentEpoch, nil +}