forked from TrueCloudLab/frostfs-s3-gw
Denis Kirillov
b1775f9478
After using AddChain to provide access to container we have to wait: * tx with APE chain be accepted by blockchain * cache in storage node be updated it takes a while. So we add retry (the same as when we add bucket settings during bucket creation) Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
259 lines
12 KiB
Go
259 lines
12 KiB
Go
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
|
|
}
|