package object import ( "crypto/ecdsa" "errors" "fmt" "os" "strings" internal "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags" sessionCli "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/session" commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa" objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session" "github.com/spf13/cobra" "github.com/spf13/viper" ) const ( bearerTokenFlag = "bearer" rawFlag = "raw" rawFlagDesc = "Set raw request option" fileFlag = "file" binaryFlag = "binary" ) type RPCParameters interface { SetBearerToken(prm *bearer.Token) SetTTL(uint32) SetXHeaders([]string) } // InitBearer adds bearer token flag to a command. func InitBearer(cmd *cobra.Command) { flags := cmd.Flags() flags.String(bearerTokenFlag, "", "File with signed JSON or binary encoded bearer token") } // Prepare prepares object-related parameters for a command. func Prepare(cmd *cobra.Command, prms ...RPCParameters) { ttl := viper.GetUint32(commonflags.TTL) common.PrintVerbose(cmd, "TTL: %d", ttl) for i := range prms { btok := common.ReadBearerToken(cmd, bearerTokenFlag) prms[i].SetBearerToken(btok) prms[i].SetTTL(ttl) prms[i].SetXHeaders(parseXHeaders(cmd)) } } func parseXHeaders(cmd *cobra.Command) []string { xHeaders, _ := cmd.Flags().GetStringSlice(commonflags.XHeadersKey) xs := make([]string, 0, 2*len(xHeaders)) for i := range xHeaders { k, v, found := strings.Cut(xHeaders[i], "=") if !found { panic(fmt.Errorf("invalid X-Header format: %s", xHeaders[i])) } xs = append(xs, k, v) } return xs } func readObjectAddress(cmd *cobra.Command, cnr *cid.ID, obj *oid.ID) oid.Address { readCID(cmd, cnr) readOID(cmd, obj) var addr oid.Address addr.SetContainer(*cnr) addr.SetObject(*obj) return addr } func readObjectAddressBin(cmd *cobra.Command, cnr *cid.ID, obj *oid.ID, filename string) oid.Address { buf, err := os.ReadFile(filename) commonCmd.ExitOnErr(cmd, "unable to read given file: %w", err) objTemp := objectSDK.New() commonCmd.ExitOnErr(cmd, "can't unmarshal object from given file: %w", objTemp.Unmarshal(buf)) var addr oid.Address *cnr, _ = objTemp.ContainerID() *obj, _ = objTemp.ID() addr.SetContainer(*cnr) addr.SetObject(*obj) return addr } func readCID(cmd *cobra.Command, id *cid.ID) { err := id.DecodeString(cmd.Flag(commonflags.CIDFlag).Value.String()) commonCmd.ExitOnErr(cmd, "decode container ID string: %w", err) } func readOID(cmd *cobra.Command, id *oid.ID) { err := id.DecodeString(cmd.Flag(commonflags.OIDFlag).Value.String()) commonCmd.ExitOnErr(cmd, "decode object ID string: %w", err) } // SessionPrm is a common interface of object operation's input which supports // sessions. type SessionPrm interface { SetSessionToken(*session.Object) SetClient(*client.Client) } // forwards all parameters to _readVerifiedSession and object as nil. func readSessionGlobal(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID) { _readVerifiedSession(cmd, dst, key, cnr, nil) } // forwards all parameters to _readVerifiedSession. func readSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj oid.ID) { _readVerifiedSession(cmd, dst, key, cnr, &obj) } // decodes session.Object from the file by path specified in the // commonflags.SessionToken flag. Returns nil if flag is not set. func getSession(cmd *cobra.Command) *session.Object { common.PrintVerbose(cmd, "Trying to read session from the file...") path, _ := cmd.Flags().GetString(commonflags.SessionToken) if path == "" { common.PrintVerbose(cmd, "File with session token is not provided.") return nil } common.PrintVerbose(cmd, "Reading session from the file [%s]...", path) var tok session.Object err := common.ReadBinaryOrJSON(cmd, &tok, path) commonCmd.ExitOnErr(cmd, "read session: %v", err) return &tok } // decodes object session from JSON file from commonflags.SessionToken command // flag if it is provided, and writes resulting session into the provided SessionPrm. // Returns flag presence. Checks: // // - if session verb corresponds to given SessionPrm according to its type // - relation to the given container // - relation to the given object if non-nil // - relation to the given private key used within the command // - session signature // // SessionPrm MUST be one of: // // *internal.GetObjectPrm // *internal.HeadObjectPrm // *internal.SearchObjectsPrm // *internal.PayloadRangePrm // *internal.HashPayloadRangesPrm func _readVerifiedSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) { var cmdVerb session.ObjectVerb switch dst.(type) { default: panic(fmt.Sprintf("unsupported op parameters %T", dst)) case *internal.GetObjectPrm: cmdVerb = session.VerbObjectGet case *internal.HeadObjectPrm: cmdVerb = session.VerbObjectHead case *internal.SearchObjectsPrm: cmdVerb = session.VerbObjectSearch case *internal.PayloadRangePrm: cmdVerb = session.VerbObjectRange case *internal.HashPayloadRangesPrm: cmdVerb = session.VerbObjectRangeHash } tok := getSession(cmd) if tok == nil { return } common.PrintVerbose(cmd, "Checking session correctness...") switch false { case tok.AssertContainer(cnr): commonCmd.ExitOnErr(cmd, "", errors.New("unrelated container in the session")) case obj == nil || tok.AssertObject(*obj): commonCmd.ExitOnErr(cmd, "", errors.New("unrelated object in the session")) case tok.AssertVerb(cmdVerb): commonCmd.ExitOnErr(cmd, "", errors.New("wrong verb of the session")) case tok.AssertAuthKey((*frostfsecdsa.PublicKey)(&key.PublicKey)): commonCmd.ExitOnErr(cmd, "", errors.New("unrelated key in the session")) case tok.VerifySignature(): commonCmd.ExitOnErr(cmd, "", errors.New("invalid signature of the session data")) } common.PrintVerbose(cmd, "Session is correct.") dst.SetSessionToken(tok) } // ReadOrOpenSession opens client connection and calls ReadOrOpenSessionViaClient with it. func ReadOrOpenSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) { cli := internal.GetSDKClientByFlag(cmd, key, commonflags.RPC) ReadOrOpenSessionViaClient(cmd, dst, cli, key, cnr, obj) } // ReadOrOpenSessionViaClient tries to read session from the file specified in // commonflags.SessionToken flag, finalizes structures of the decoded token // and write the result into provided SessionPrm. If file is missing, // ReadOrOpenSessionViaClient calls OpenSessionViaClient. func ReadOrOpenSessionViaClient(cmd *cobra.Command, dst SessionPrm, cli *client.Client, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) { tok := getSession(cmd) if tok == nil { OpenSessionViaClient(cmd, dst, cli, key, cnr, obj) return } var objs []oid.ID if obj != nil { objs = []oid.ID{*obj} if _, ok := dst.(*internal.DeleteObjectPrm); ok { common.PrintVerbose(cmd, "Collecting relatives of the removal object...") objs = append(objs, collectObjectRelatives(cmd, cli, cnr, *obj)...) } } finalizeSession(cmd, dst, tok, key, cnr, objs...) dst.SetClient(cli) } // OpenSession opens client connection and calls OpenSessionViaClient with it. func OpenSession(cmd *cobra.Command, dst SessionPrm, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) { cli := internal.GetSDKClientByFlag(cmd, key, commonflags.RPC) OpenSessionViaClient(cmd, dst, cli, key, cnr, obj) } // OpenSessionViaClient opens object session with the remote node, finalizes // structure of the session token and writes the result into the provided // SessionPrm. Also writes provided client connection to the SessionPrm. // // SessionPrm MUST be one of: // // *internal.PutObjectPrm // *internal.DeleteObjectPrm // // If provided SessionPrm is of type internal.DeleteObjectPrm, OpenSessionViaClient // spreads the session to all object's relatives. func OpenSessionViaClient(cmd *cobra.Command, dst SessionPrm, cli *client.Client, key *ecdsa.PrivateKey, cnr cid.ID, obj *oid.ID) { var objs []oid.ID if obj != nil { if _, ok := dst.(*internal.DeleteObjectPrm); ok { common.PrintVerbose(cmd, "Collecting relatives of the removal object...") rels := collectObjectRelatives(cmd, cli, cnr, *obj) if len(rels) == 0 { objs = []oid.ID{*obj} } else { objs = append(rels, *obj) } } } var tok session.Object const sessionLifetime = 10 // in FrostFS epochs common.PrintVerbose(cmd, "Opening remote session with the node...") err := sessionCli.CreateSession(cmd.Context(), &tok, cli, sessionLifetime) commonCmd.ExitOnErr(cmd, "open remote session: %w", err) common.PrintVerbose(cmd, "Session successfully opened.") finalizeSession(cmd, dst, &tok, key, cnr, objs...) dst.SetClient(cli) } // specifies session verb, binds the session to the given container and limits // the session by the given objects (if specified). After all data is written, // signs session using provided private key and writes the session into the // given SessionPrm. // // SessionPrm MUST be one of: // // *internal.PutObjectPrm // *internal.DeleteObjectPrm func finalizeSession(cmd *cobra.Command, dst SessionPrm, tok *session.Object, key *ecdsa.PrivateKey, cnr cid.ID, objs ...oid.ID) { common.PrintVerbose(cmd, "Finalizing session token...") switch dst.(type) { default: panic(fmt.Sprintf("unsupported op parameters %T", dst)) case *internal.PutObjectPrm: common.PrintVerbose(cmd, "Binding session to object PUT...") tok.ForVerb(session.VerbObjectPut) case *internal.DeleteObjectPrm: common.PrintVerbose(cmd, "Binding session to object DELETE...") tok.ForVerb(session.VerbObjectDelete) } common.PrintVerbose(cmd, "Binding session to container %s...", cnr) tok.BindContainer(cnr) if len(objs) > 0 { common.PrintVerbose(cmd, "Limiting session by the objects %v...", objs) tok.LimitByObjects(objs...) } common.PrintVerbose(cmd, "Signing session...") err := tok.Sign(*key) commonCmd.ExitOnErr(cmd, "sign session: %w", err) common.PrintVerbose(cmd, "Session token successfully formed and attached to the request.") dst.SetSessionToken(tok) } // calls commonflags.InitSession with "object <verb>" name. func initFlagSession(cmd *cobra.Command, verb string) { commonflags.InitSession(cmd, "object "+verb) } // collects and returns all relatives of the given object stored in the specified // container. Empty result without an error means lack of relationship in the // container. // // The object itself is not included in the result. func collectObjectRelatives(cmd *cobra.Command, cli *client.Client, cnr cid.ID, obj oid.ID) []oid.ID { common.PrintVerbose(cmd, "Fetching raw object header...") // request raw header first var addrObj oid.Address addrObj.SetContainer(cnr) addrObj.SetObject(obj) var prmHead internal.HeadObjectPrm prmHead.SetClient(cli) prmHead.SetAddress(addrObj) prmHead.SetRawFlag(true) Prepare(cmd, &prmHead) o, err := internal.HeadObject(cmd.Context(), prmHead) var errSplit *objectSDK.SplitInfoError var errEC *objectSDK.ECInfoError switch { default: commonCmd.ExitOnErr(cmd, "failed to get raw object header: %w", err) case err == nil: common.PrintVerbose(cmd, "Raw header received - object is singular.") if ech := o.Header().ECHeader(); ech != nil { commonCmd.ExitOnErr(cmd, "Lock EC chunk failed: %w", errors.ErrUnsupported) } return nil case errors.As(err, &errSplit): common.PrintVerbose(cmd, "Split information received - object is virtual.") splitInfo := errSplit.SplitInfo() if members, ok := tryGetSplitMembersByLinkingObject(cmd, splitInfo, prmHead, cnr, true); ok { return members } if members, ok := tryGetSplitMembersBySplitID(cmd, splitInfo, cli, cnr); ok { return members } return tryRestoreChainInReverse(cmd, splitInfo, prmHead, cli, cnr, obj) case errors.As(err, &errEC): common.PrintVerbose(cmd, "Object is erasure-coded.") return nil } return nil } func tryGetSplitMembersByLinkingObject(cmd *cobra.Command, splitInfo *objectSDK.SplitInfo, prmHead internal.HeadObjectPrm, cnr cid.ID, withLinking bool) ([]oid.ID, bool) { // collect split chain by the descending ease of operations (ease is evaluated heuristically). // If any approach fails, we don't try the next since we assume that it will fail too. if idLinking, ok := splitInfo.Link(); ok { common.PrintVerbose(cmd, "Collecting split members using linking object %s...", idLinking) var addrObj oid.Address addrObj.SetContainer(cnr) addrObj.SetObject(idLinking) prmHead.SetAddress(addrObj) prmHead.SetRawFlag(false) // client is already set res, err := internal.HeadObject(cmd.Context(), prmHead) if err == nil { children := res.Header().Children() common.PrintVerbose(cmd, "Received split members from the linking object: %v", children) if withLinking { return append(children, idLinking), true } return children, true } // linking object is not required for // object collecting common.PrintVerbose(cmd, "failed to get linking object's header: %w", err) } return nil, false } func tryGetSplitMembersBySplitID(cmd *cobra.Command, splitInfo *objectSDK.SplitInfo, cli *client.Client, cnr cid.ID) ([]oid.ID, bool) { if idSplit := splitInfo.SplitID(); idSplit != nil { common.PrintVerbose(cmd, "Collecting split members by split ID...") var query objectSDK.SearchFilters query.AddSplitIDFilter(objectSDK.MatchStringEqual, idSplit) var prm internal.SearchObjectsPrm prm.SetContainerID(cnr) prm.SetClient(cli) prm.SetFilters(query) res, err := internal.SearchObjects(cmd.Context(), prm) commonCmd.ExitOnErr(cmd, "failed to search objects by split ID: %w", err) parts := res.IDList() common.PrintVerbose(cmd, "Found objects by split ID: %v", res.IDList()) return parts, true } return nil, false } func tryRestoreChainInReverse(cmd *cobra.Command, splitInfo *objectSDK.SplitInfo, prmHead internal.HeadObjectPrm, cli *client.Client, cnr cid.ID, obj oid.ID) []oid.ID { var addrObj oid.Address addrObj.SetContainer(cnr) idMember, ok := splitInfo.LastPart() if !ok { commonCmd.ExitOnErr(cmd, "", errors.New("missing any data in received object split information")) } common.PrintVerbose(cmd, "Traverse the object split chain in reverse...", idMember) var res *internal.HeadObjectRes var err error chain := []oid.ID{idMember} chainSet := map[oid.ID]struct{}{idMember: {}} prmHead.SetRawFlag(false) // split members are almost definitely singular, but don't get hung up on it for { common.PrintVerbose(cmd, "Reading previous element of the split chain member %s...", idMember) addrObj.SetObject(idMember) prmHead.SetAddress(addrObj) res, err = internal.HeadObject(cmd.Context(), prmHead) commonCmd.ExitOnErr(cmd, "failed to read split chain member's header: %w", err) idMember, ok = res.Header().PreviousID() if !ok { common.PrintVerbose(cmd, "Chain ended.") break } if _, ok = chainSet[idMember]; ok { commonCmd.ExitOnErr(cmd, "", fmt.Errorf("duplicated member in the split chain %s", idMember)) } chain = append(chain, idMember) chainSet[idMember] = struct{}{} } common.PrintVerbose(cmd, "Looking for a linking object...") var query objectSDK.SearchFilters query.AddParentIDFilter(objectSDK.MatchStringEqual, obj) var prmSearch internal.SearchObjectsPrm prmSearch.SetClient(cli) prmSearch.SetContainerID(cnr) prmSearch.SetFilters(query) resSearch, err := internal.SearchObjects(cmd.Context(), prmSearch) commonCmd.ExitOnErr(cmd, "failed to find object children: %w", err) list := resSearch.IDList() for i := range list { if _, ok = chainSet[list[i]]; !ok { common.PrintVerbose(cmd, "Found one more related object %s.", list[i]) chain = append(chain, list[i]) } } return chain }