package modules import ( "context" "fmt" "os" "strings" "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/handler" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" ) var issueSecretCmd = &cobra.Command{ Use: "issue-secret", Short: "Issue a secret in FrostFS network", Long: "Creates new s3 credentials to use with frostfs-s3-gw", Example: `To create new s3 credentials use: frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --attributes LOGIN=NUUb82KR2JrVByHs2YSKgtK29gKnF5q6Vt To create new s3 credentials using specific access key id and secret access key use: frostfs-s3-authmate issue-secret --wallet wallet.json --peer s01.frostfs.devenv:8080 --gate-public-key 031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a --access-key-id my-access-key-id --secret-access-key my-secret-key --container-id BpExV76416Vo7GrkJsGwXGoLM35xsBwup8voedDZR3c6 `, RunE: runIssueSecretCmd, } const ( walletFlag = "wallet" addressFlag = "address" peerFlag = "peer" gatePublicKeyFlag = "gate-public-key" containerIDFlag = "container-id" containerFriendlyNameFlag = "container-friendly-name" containerPlacementPolicyFlag = "container-placement-policy" sessionTokensFlag = "session-tokens" lifetimeFlag = "lifetime" containerPolicyFlag = "container-policy" awsCLICredentialFlag = "aws-cli-credentials" attributesFlag = "attributes" retryMaxAttemptsFlag = "retry-max-attempts" retryMaxBackoffFlag = "retry-max-backoff" retryStrategyFlag = "retry-strategy" ) const walletPassphraseCfg = "wallet.passphrase" const ( defaultAccessBoxLifetime = 30 * 24 * time.Hour defaultPoolDialTimeout = 5 * time.Second defaultPoolHealthcheckTimeout = 5 * time.Second defaultPoolRebalanceInterval = 30 * time.Second defaultPoolStreamTimeout = 10 * time.Second defaultRetryMaxAttempts = 4 defaultRetryMaxBackoff = 30 * time.Second defaultRetryStrategy = handler.RetryStrategyExponential ) const ( poolDialTimeoutFlag = "pool-dial-timeout" poolHealthcheckTimeoutFlag = "pool-healthcheck-timeout" poolRebalanceIntervalFlag = "pool-rebalance-interval" poolStreamTimeoutFlag = "pool-stream-timeout" accessKeyIDFlag = "access-key-id" secretAccessKeyFlag = "secret-access-key" ) func initIssueSecretCmd() { issueSecretCmd.Flags().String(walletFlag, "", "Path to the wallet that will be owner of the credentials") issueSecretCmd.Flags().String(addressFlag, "", "Address of the wallet account") issueSecretCmd.Flags().String(peerFlag, "", "Address of a frostfs peer to connect to") issueSecretCmd.Flags().StringSlice(gatePublicKeyFlag, nil, "Public 256r1 key of a gate (use flags repeatedly for multiple gates or separate them by comma)") issueSecretCmd.Flags().String(containerIDFlag, "", "Auth container id to put the secret into (if not provided new container will be created)") issueSecretCmd.Flags().String(containerFriendlyNameFlag, "", "Friendly name of auth container to put the secret into (flag value will be used only if --container-id is missed)") issueSecretCmd.Flags().String(containerPlacementPolicyFlag, "REP 2 IN X CBF 3 SELECT 2 FROM * AS X", "Placement policy of auth container to put the secret into (flag value will be used only if --container-id is missed)") issueSecretCmd.Flags().String(sessionTokensFlag, "", "create session tokens with rules, if the rules are set as 'none', no session tokens will be created") issueSecretCmd.Flags().Duration(lifetimeFlag, defaultAccessBoxLifetime, "Lifetime of tokens. For example 50h30m (note: max time unit is an hour so to set a day you should use 24h).\nIt will be ceil rounded to the nearest amount of epoch.") issueSecretCmd.Flags().String(containerPolicyFlag, "", "Mapping AWS storage class to FrostFS storage policy as plain json string or path to json file") issueSecretCmd.Flags().String(awsCLICredentialFlag, "", "Path to the aws cli credential file") issueSecretCmd.Flags().Duration(poolDialTimeoutFlag, defaultPoolDialTimeout, "Timeout for connection to the node in pool to be established") issueSecretCmd.Flags().Duration(poolHealthcheckTimeoutFlag, defaultPoolHealthcheckTimeout, "Timeout for request to node to decide if it is alive") issueSecretCmd.Flags().Duration(poolRebalanceIntervalFlag, defaultPoolRebalanceInterval, "Interval for updating nodes health status") issueSecretCmd.Flags().Duration(poolStreamTimeoutFlag, defaultPoolStreamTimeout, "Timeout for individual operation in streaming RPC") issueSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)") issueSecretCmd.Flags().String(accessKeyIDFlag, "", "Access key id of s3 credential that must be created") issueSecretCmd.Flags().String(secretAccessKeyFlag, "", "Secret access key of s3 credential that must be used") issueSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address (must be provided if container-id is nns name)") issueSecretCmd.Flags().Int(retryMaxAttemptsFlag, defaultRetryMaxAttempts, "Max amount of request attempts") issueSecretCmd.Flags().Duration(retryMaxBackoffFlag, defaultRetryMaxBackoff, "Max delay before next attempt") issueSecretCmd.Flags().String(retryStrategyFlag, defaultRetryStrategy, "Backoff strategy. `exponential` and `constant` are allowed") _ = issueSecretCmd.MarkFlagRequired(walletFlag) _ = issueSecretCmd.MarkFlagRequired(peerFlag) _ = issueSecretCmd.MarkFlagRequired(gatePublicKeyFlag) } func runIssueSecretCmd(cmd *cobra.Command, _ []string) error { ctx, cancel := context.WithTimeout(cmd.Context(), viper.GetDuration(timeoutFlag)) defer cancel() log := getLogger() password := wallet.GetPassword(viper.GetViper(), walletPassphraseCfg) key, err := wallet.GetKeyFromPath(viper.GetString(walletFlag), viper.GetString(addressFlag), password) if err != nil { return wrapPreparationError(fmt.Errorf("failed to load frostfs private key: %s", err)) } var gatesPublicKeys []*keys.PublicKey for _, keyStr := range viper.GetStringSlice(gatePublicKeyFlag) { gpk, err := keys.NewPublicKeyFromString(keyStr) if err != nil { return wrapPreparationError(fmt.Errorf("failed to load gate's public key: %s", err)) } gatesPublicKeys = append(gatesPublicKeys, gpk) } lifetime := viper.GetDuration(lifetimeFlag) if lifetime <= 0 { return wrapPreparationError(fmt.Errorf("lifetime must be greater 0, current value: %d", lifetime)) } policies, err := parsePolicies(viper.GetString(containerPolicyFlag)) if err != nil { return wrapPreparationError(fmt.Errorf("couldn't parse container policy: %s", err.Error())) } sessionRules, skipSessionRules, err := getSessionRules(viper.GetString(sessionTokensFlag)) if err != nil { return wrapPreparationError(fmt.Errorf("couldn't parse 'session-tokens' flag: %s", err.Error())) } poolCfg := PoolConfig{ Key: key, Address: viper.GetString(peerFlag), DialTimeout: viper.GetDuration(poolDialTimeoutFlag), HealthcheckTimeout: viper.GetDuration(poolHealthcheckTimeoutFlag), StreamTimeout: viper.GetDuration(poolStreamTimeoutFlag), RebalanceInterval: viper.GetDuration(poolRebalanceIntervalFlag), } frostFS, err := createFrostFS(ctx, log, poolCfg) if err != nil { return wrapFrostFSInitError(fmt.Errorf("failed to create FrostFS component: %s", err)) } var accessBox cid.ID if viper.IsSet(containerIDFlag) { if accessBox, err = util.ResolveContainerID(viper.GetString(containerIDFlag), viper.GetString(rpcEndpointFlag)); err != nil { return wrapPreparationError(fmt.Errorf("resolve accessbox container id (make sure you provided %s): %w", rpcEndpointFlag, err)) } } else if accessBox, err = createAccessBox(ctx, frostFS, key, log); err != nil { return wrapPreparationError(err) } accessKeyID, secretAccessKey, err := parseAccessKeys() if err != nil { return wrapPreparationError(err) } customAttrs, err := parseObjectAttrs(viper.GetString(attributesFlag)) if err != nil { return wrapPreparationError(fmt.Errorf("failed to parse attributes: %s", err)) } issueSecretOptions := &authmate.IssueSecretOptions{ Container: accessBox, AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, FrostFSKey: key, GatesPublicKeys: gatesPublicKeys, Impersonate: true, SessionTokenRules: sessionRules, SkipSessionRules: skipSessionRules, ContainerPolicies: policies, Lifetime: lifetime, AwsCliCredentialsFile: viper.GetString(awsCLICredentialFlag), CustomAttributes: customAttrs, } options := []authmate.Option{ authmate.WithRetryMaxAttempts(viper.GetInt(retryMaxAttemptsFlag)), authmate.WithRetryMaxBackoff(viper.GetDuration(retryMaxBackoffFlag)), authmate.WithRetryStrategy(handler.RetryStrategy(viper.GetString(retryStrategyFlag))), } if err = authmate.New(log, frostFS, options...).IssueSecret(ctx, os.Stdout, issueSecretOptions); err != nil { return wrapBusinessLogicError(fmt.Errorf("failed to issue secret: %s", err)) } return nil } func parseAccessKeys() (accessKeyID, secretAccessKey string, err error) { accessKeyID = viper.GetString(accessKeyIDFlag) secretAccessKey = viper.GetString(secretAccessKeyFlag) if accessKeyID == "" && secretAccessKey != "" || accessKeyID != "" && secretAccessKey == "" { return "", "", fmt.Errorf("flags %s and %s must be both provided or not", accessKeyIDFlag, secretAccessKeyFlag) } if accessKeyID != "" { if !isCustomCreds(accessKeyID) { return "", "", fmt.Errorf("invalid custom AccessKeyID format: %s", accessKeyID) } if !checkAccessKeyLength(accessKeyID) { return "", "", fmt.Errorf("invalid custom AccessKeyID length: %s", accessKeyID) } if !checkAccessKeyLength(secretAccessKey) { return "", "", fmt.Errorf("invalid custom SecretAccessKey length: %s", secretAccessKey) } } return accessKeyID, secretAccessKey, nil } func isCustomCreds(accessKeyID string) bool { var addr oid.Address return addr.DecodeString(strings.ReplaceAll(accessKeyID, "0", "/")) != nil } func checkAccessKeyLength(key string) bool { return 4 <= len(key) && len(key) <= 128 } func createAccessBox(ctx context.Context, frostFS *frostfs.AuthmateFrostFS, key *keys.PrivateKey, log *zap.Logger) (cid.ID, error) { friendlyName := viper.GetString(containerFriendlyNameFlag) placementPolicy := viper.GetString(containerPlacementPolicyFlag) log.Info(logs.CreateContainer, zap.String("friendly_name", friendlyName), zap.String("placement_policy", placementPolicy)) prm := authmate.PrmContainerCreate{ FriendlyName: friendlyName, Owner: key.PublicKey(), } if err := prm.Policy.DecodeString(placementPolicy); err != nil { return cid.ID{}, fmt.Errorf("failed to build placement policy: %w", err) } accessBox, err := frostFS.CreateContainer(ctx, prm) if err != nil { return cid.ID{}, fmt.Errorf("create container in FrostFS: %w", err) } return accessBox, nil }