forked from TrueCloudLab/frostfs-s3-gw
[#131] authmate: Add issue-secret cobra cmd
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
This commit is contained in:
parent
0cd353707a
commit
043447600e
3 changed files with 321 additions and 0 deletions
176
cmd/s3-authmate/modules/issue-secret.go
Normal file
176
cmd/s3-authmate/modules/issue-secret.go
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
package modules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
|
||||||
|
"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`,
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.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 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 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 fmt.Errorf("failed to load gate's public key: %s", err)
|
||||||
|
}
|
||||||
|
gatesPublicKeys = append(gatesPublicKeys, gpk)
|
||||||
|
}
|
||||||
|
|
||||||
|
lifetime := viper.GetDuration(lifetimeFlag)
|
||||||
|
if lifetime <= 0 {
|
||||||
|
return fmt.Errorf("lifetime must be greater 0, current value: %d", lifetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
policies, err := parsePolicies(viper.GetString(containerPolicyFlag))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't parse container policy: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
disableImpersonate := viper.GetBool(disableImpersonateFlag)
|
||||||
|
eaclRules := viper.GetString(bearerRulesFlag)
|
||||||
|
if !disableImpersonate && eaclRules != "" {
|
||||||
|
return errors.New("--bearer-rules flag can be used only with --disable-impersonate")
|
||||||
|
}
|
||||||
|
|
||||||
|
bearerRules, err := getJSONRules(eaclRules)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't parse 'bearer-rules' flag: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionRules, skipSessionRules, err := getSessionRules(viper.GetString(sessionTokensFlag))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't parse 'session-tokens' flag: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
poolCfg := PoolConfig{
|
||||||
|
Key: &key.PrivateKey,
|
||||||
|
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 fmt.Errorf("failed to create FrostFS component: %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),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = authmate.New(log, frostFS).IssueSecret(ctx, os.Stdout, issueSecretOptions); err != nil {
|
||||||
|
return fmt.Errorf("failed to issue secret: %s", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -53,4 +53,7 @@ func init() {
|
||||||
{{printf "Version: %s" .Version }}
|
{{printf "Version: %s" .Version }}
|
||||||
GoVersion: {{ runtimeVersion }}
|
GoVersion: {{ runtimeVersion }}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(issueSecretCmd)
|
||||||
|
initIssueSecretCmd()
|
||||||
}
|
}
|
||||||
|
|
142
cmd/s3-authmate/modules/utils.go
Normal file
142
cmd/s3-authmate/modules/utils.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package modules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs"
|
||||||
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PoolConfig struct {
|
||||||
|
Key *ecdsa.PrivateKey
|
||||||
|
Address string
|
||||||
|
DialTimeout time.Duration
|
||||||
|
HealthcheckTimeout time.Duration
|
||||||
|
StreamTimeout time.Duration
|
||||||
|
RebalanceInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFrostFS(ctx context.Context, log *zap.Logger, cfg PoolConfig) (authmate.FrostFS, error) {
|
||||||
|
log.Debug("prepare connection pool")
|
||||||
|
|
||||||
|
var prm pool.InitParameters
|
||||||
|
prm.SetKey(cfg.Key)
|
||||||
|
prm.SetNodeDialTimeout(cfg.DialTimeout)
|
||||||
|
prm.SetHealthcheckTimeout(cfg.HealthcheckTimeout)
|
||||||
|
prm.SetNodeStreamTimeout(cfg.StreamTimeout)
|
||||||
|
prm.SetClientRebalanceInterval(cfg.RebalanceInterval)
|
||||||
|
prm.SetLogger(log)
|
||||||
|
prm.AddNode(pool.NewNodeParam(1, cfg.Address, 1))
|
||||||
|
|
||||||
|
p, err := pool.NewPool(prm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.Dial(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("dial pool: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frostfs.NewAuthmateFrostFS(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePolicies(val string) (authmate.ContainerPolicies, error) {
|
||||||
|
if val == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
data = []byte(val)
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if !json.Valid(data) {
|
||||||
|
if data, err = os.ReadFile(val); err != nil {
|
||||||
|
return nil, fmt.Errorf("coudln't read json file or provided json is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var policies authmate.ContainerPolicies
|
||||||
|
if err = json.Unmarshal(data, &policies); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal policies: %w", err)
|
||||||
|
}
|
||||||
|
if _, ok := policies[api.DefaultLocationConstraint]; ok {
|
||||||
|
return nil, fmt.Errorf("config overrides %s location constraint", api.DefaultLocationConstraint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return policies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJSONRules(val string) ([]byte, error) {
|
||||||
|
if val == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
data := []byte(val)
|
||||||
|
if json.Valid(data) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, err := os.ReadFile(val); err == nil {
|
||||||
|
if json.Valid(data) {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("coudln't read json file or provided json is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSessionRules reads json session rules.
|
||||||
|
// It returns true if rules must be skipped.
|
||||||
|
func getSessionRules(r string) ([]byte, bool, error) {
|
||||||
|
if r == "none" {
|
||||||
|
return nil, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := getJSONRules(r)
|
||||||
|
return data, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLogger returns new logger depending on appropriate values in viper.Viper
|
||||||
|
// if logger cannot be built it panics.
|
||||||
|
func getLogger() *zap.Logger {
|
||||||
|
if !viper.GetBool(withLogFlag) {
|
||||||
|
return zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
var zapConfig = zap.Config{
|
||||||
|
Development: true,
|
||||||
|
Encoding: "console",
|
||||||
|
Level: zap.NewAtomicLevelAt(zapcore.FatalLevel),
|
||||||
|
OutputPaths: []string{"stdout"},
|
||||||
|
EncoderConfig: zapcore.EncoderConfig{
|
||||||
|
MessageKey: "message",
|
||||||
|
LevelKey: "level",
|
||||||
|
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||||
|
TimeKey: "time",
|
||||||
|
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||||
|
CallerKey: "caller",
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.GetBool(debugFlag) {
|
||||||
|
zapConfig.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
log, err := zapConfig.Build()
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("create logger: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return log
|
||||||
|
}
|
Loading…
Reference in a new issue