package modules import ( "context" "errors" "fmt" "os" "time" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/spf13/cobra" "github.com/spf13/viper" ) 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: `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`, RunE: runIssueSecretCmd, } const ( walletFlag = "wallet" addressFlag = "address" peerFlag = "peer" bearerRulesFlag = "bearer-rules" disableImpersonateFlag = "disable-impersonate" 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" frostfsIDFlag = "frostfsid" frostfsIDProxyFlag = "frostfsid-proxy" frostfsIDNamespaceFlag = "frostfsid-namespace" rpcEndpointFlag = "rpc-endpoint" attributesFlag = "attributes" ) 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 ) const ( poolDialTimeoutFlag = "pool-dial-timeout" poolHealthcheckTimeoutFlag = "pool-healthcheck-timeout" poolRebalanceIntervalFlag = "pool-rebalance-interval" poolStreamTimeoutFlag = "pool-stream-timeout" ) 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().String(bearerRulesFlag, "", "Rules for bearer token (filepath or a plain json string are allowed, can be used only with --disable-impersonate)") issueSecretCmd.Flags().Bool(disableImpersonateFlag, false, "Mark token as not impersonate to don't consider token signer as request owner (must be provided to use --bearer-rules flag)") 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(frostfsIDFlag, "", "FrostfsID contract hash (LE) or name in NNS to register public key in contract (rpc-endpoint flag also must be provided)") issueSecretCmd.Flags().String(frostfsIDProxyFlag, "", "Proxy contract hash (LE) or name in NNS to use when interact with frostfsid contract") issueSecretCmd.Flags().String(frostfsIDNamespaceFlag, "", "Namespace to register public key in frostfsid contract") issueSecretCmd.Flags().String(rpcEndpointFlag, "", "NEO node RPC address") issueSecretCmd.Flags().String(attributesFlag, "", "User attributes in form of Key1=Value1,Key2=Value2 (note: you cannot override system attributes)") _ = 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 cnrID cid.ID containerID := viper.GetString(containerIDFlag) if len(containerID) > 0 { if err = cnrID.DecodeString(containerID); err != nil { return wrapPreparationError(fmt.Errorf("failed to parse auth container id: %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())) } disableImpersonate := viper.GetBool(disableImpersonateFlag) eaclRules := viper.GetString(bearerRulesFlag) if !disableImpersonate && eaclRules != "" { return wrapPreparationError(errors.New("--bearer-rules flag can be used only with --disable-impersonate")) } bearerRules, err := getJSONRules(eaclRules) if err != nil { return wrapPreparationError(fmt.Errorf("couldn't parse 'bearer-rules' flag: %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)) } frostFSID := viper.GetString(frostfsIDFlag) if frostFSID != "" { rpcAddress := viper.GetString(rpcEndpointFlag) if rpcAddress == "" { return wrapPreparationError(fmt.Errorf("you can use '%s' flag only along with '%s'", frostfsIDFlag, rpcEndpointFlag)) } cfg := contract.Config{ RPCAddress: rpcAddress, Contract: frostFSID, ProxyContract: viper.GetString(frostfsIDProxyFlag), Key: key, } frostfsIDClient, err := createFrostFSID(ctx, log, cfg) if err != nil { return wrapFrostFSIDInitError(err) } if err = registerPublicKey(frostfsIDClient, viper.GetString(frostfsIDNamespaceFlag), key.PublicKey()); err != nil { return wrapBusinessLogicError(fmt.Errorf("failed to register key in frostfsid: %w", err)) } } customAttrs, err := parseObjectAttrs(viper.GetString(attributesFlag)) if err != nil { return wrapPreparationError(fmt.Errorf("failed to parse attributes: %s", err)) } issueSecretOptions := &authmate.IssueSecretOptions{ Container: authmate.ContainerOptions{ ID: cnrID, FriendlyName: viper.GetString(containerFriendlyNameFlag), PlacementPolicy: viper.GetString(containerPlacementPolicyFlag), }, FrostFSKey: key, GatesPublicKeys: gatesPublicKeys, EACLRules: bearerRules, Impersonate: !disableImpersonate, SessionTokenRules: sessionRules, SkipSessionRules: skipSessionRules, ContainerPolicies: policies, Lifetime: lifetime, AwsCliCredentialsFile: viper.GetString(awsCLICredentialFlag), CustomAttributes: customAttrs, } if err = authmate.New(log, frostFS).IssueSecret(ctx, os.Stdout, issueSecretOptions); err != nil { return wrapBusinessLogicError(fmt.Errorf("failed to issue secret: %s", err)) } return nil }