package apemanager import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "errors" "fmt" ape_contract "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/ape/contract_storage" containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container" apemanager_errors "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/apemanager/errors" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger" apeV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/ape" apemanagerV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/apemanager" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs" session "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/session" cidSDK "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" policy_engine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine" "github.com/mr-tron/base58/base58" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "go.uber.org/zap" ) var errEmptyBodySignature = errors.New("malformed request: empty body signature") type cfg struct { log *logger.Logger } type Service struct { cfg cnrSrc containercore.Source contractStorage ape_contract.ProxyAdaptedContractStorage } type Option func(*cfg) func New(cnrSrc containercore.Source, contractStorage ape_contract.ProxyAdaptedContractStorage, opts ...Option) *Service { s := &Service{ cnrSrc: cnrSrc, contractStorage: contractStorage, } for i := range opts { opts[i](&s.cfg) } if s.log == nil { s.log = &logger.Logger{Logger: zap.NewNop()} } return s } func WithLogger(log *logger.Logger) Option { return func(c *cfg) { c.log = log } } var _ Server = (*Service)(nil) // validateContainerTargetRequest validates request for the container target. // It checks if request actor is the owner of the container, otherwise it denies the request. func (s *Service) validateContainerTargetRequest(cid string, pubKey *keys.PublicKey) error { var cidSDK cidSDK.ID if err := cidSDK.DecodeString(cid); err != nil { return fmt.Errorf("invalid CID format: %w", err) } isOwner, err := s.isActorContainerOwner(cidSDK, pubKey) if err != nil { return fmt.Errorf("failed to check owner: %w", err) } if !isOwner { return apemanager_errors.ErrAPEManagerAccessDenied("actor must be container owner") } return nil } func (s *Service) AddChain(_ context.Context, req *apemanagerV2.AddChainRequest) (*apemanagerV2.AddChainResponse, error) { pub, err := getSignaturePublicKey(req.GetVerificationHeader()) if err != nil { return nil, err } chain, err := decodeAndValidateChain(req.GetBody().GetChain().GetKind().(*apeV2.ChainRaw).GetRaw()) if err != nil { return nil, err } if len(chain.ID) == 0 { const randomIDLength = 10 randID, err := base58Str(randomIDLength) if err != nil { return nil, fmt.Errorf("randomize chain ID error: %w", err) } chain.ID = apechain.ID(randID) } var target policy_engine.Target switch targetType := req.GetBody().GetTarget().GetTargetType(); targetType { case apeV2.TargetTypeContainer: reqCID := req.GetBody().GetTarget().GetName() if err = s.validateContainerTargetRequest(reqCID, pub); err != nil { return nil, err } target = policy_engine.ContainerTarget(reqCID) default: return nil, fmt.Errorf("unsupported target type: %s", targetType) } if _, _, err = s.contractStorage.AddMorphRuleChain(apechain.Ingress, target, &chain); err != nil { return nil, err } body := new(apemanagerV2.AddChainResponseBody) body.SetChainID(chain.ID) resp := new(apemanagerV2.AddChainResponse) resp.SetBody(body) return resp, nil } func (s *Service) RemoveChain(_ context.Context, req *apemanagerV2.RemoveChainRequest) (*apemanagerV2.RemoveChainResponse, error) { pub, err := getSignaturePublicKey(req.GetVerificationHeader()) if err != nil { return nil, err } var target policy_engine.Target switch targetType := req.GetBody().GetTarget().GetTargetType(); targetType { case apeV2.TargetTypeContainer: reqCID := req.GetBody().GetTarget().GetName() if err = s.validateContainerTargetRequest(reqCID, pub); err != nil { return nil, err } target = policy_engine.ContainerTarget(reqCID) default: return nil, fmt.Errorf("unsupported target type: %s", targetType) } if _, _, err = s.contractStorage.RemoveMorphRuleChain(apechain.Ingress, target, req.GetBody().GetChainID()); err != nil { return nil, err } body := new(apemanagerV2.RemoveChainResponseBody) resp := new(apemanagerV2.RemoveChainResponse) resp.SetBody(body) return resp, nil } func (s *Service) ListChains(_ context.Context, req *apemanagerV2.ListChainsRequest) (*apemanagerV2.ListChainsResponse, error) { pub, err := getSignaturePublicKey(req.GetVerificationHeader()) if err != nil { return nil, err } var target policy_engine.Target switch targetType := req.GetBody().GetTarget().GetTargetType(); targetType { case apeV2.TargetTypeContainer: reqCID := req.GetBody().GetTarget().GetName() if err = s.validateContainerTargetRequest(reqCID, pub); err != nil { return nil, err } target = policy_engine.ContainerTarget(reqCID) default: return nil, fmt.Errorf("unsupported target type: %s", targetType) } chs, err := s.contractStorage.ListMorphRuleChains(apechain.Ingress, target) if err != nil { return nil, err } res := make([]*apeV2.Chain, 0, len(chs)) for _, ch := range chs { v2chraw := new(apeV2.ChainRaw) v2chraw.SetRaw(ch.Bytes()) v2ch := new(apeV2.Chain) v2ch.SetKind(v2chraw) res = append(res, v2ch) } body := new(apemanagerV2.ListChainsResponseBody) body.SetChains(res) resp := new(apemanagerV2.ListChainsResponse) resp.SetBody(body) return resp, nil } func getSignaturePublicKey(vh *session.RequestVerificationHeader) (*keys.PublicKey, error) { for vh.GetOrigin() != nil { vh = vh.GetOrigin() } sig := vh.GetBodySignature() if sig == nil { return nil, errEmptyBodySignature } key, err := keys.NewPublicKeyFromBytes(sig.GetKey(), elliptic.P256()) if err != nil { return nil, fmt.Errorf("invalid signature key: %w", err) } return key, nil } func (s *Service) isActorContainerOwner(cid cidSDK.ID, pk *keys.PublicKey) (bool, error) { var actor user.ID user.IDFromKey(&actor, (ecdsa.PublicKey)(*pk)) actorOwnerID := new(refs.OwnerID) actor.WriteToV2(actorOwnerID) cnr, err := s.cnrSrc.Get(cid) if err != nil { return false, fmt.Errorf("get container error: %w", err) } return cnr.Value.Owner().Equals(actor), nil } // base58Str generates base58 string. func base58Str(n int) (string, error) { b := make([]byte, n) _, err := rand.Read(b) if err != nil { return "", err } return base58.FastBase58Encoding(b), nil }