15e74c8c3d
* cmd/uplink: add share command to restrict an api key This commit is an early bit of work to just implement restricting macaroon api keys from the command line. It does not convert api keys to be macaroons in general. It also does not apply the path restriction caveats appropriately yet because it does not encrypt them. * cmd/uplink: fix path encryption for shares It should now properly encrypt the path prefixes when adding caveats to a macaroon. * fix up linting problems * print summary of caveat and require iso8601 * make clone part more clear
324 lines
10 KiB
Go
324 lines
10 KiB
Go
// Copyright (C) 2019 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package cfgstruct
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
"go.uber.org/zap"
|
|
|
|
"storj.io/storj/internal/memory"
|
|
"storj.io/storj/internal/version"
|
|
)
|
|
|
|
// BindOpt is an option for the Bind method
|
|
type BindOpt struct {
|
|
isDev *bool
|
|
varfn func(vars map[string]confVar)
|
|
}
|
|
|
|
// ConfDir sets variables for default options called $CONFDIR and $CONFNAME.
|
|
func ConfDir(path string) BindOpt {
|
|
val := filepath.Clean(os.ExpandEnv(path))
|
|
return BindOpt{varfn: func(vars map[string]confVar) {
|
|
vars["CONFDIR"] = confVar{val: val, nested: false}
|
|
vars["CONFNAME"] = confVar{val: val, nested: false}
|
|
}}
|
|
}
|
|
|
|
// ConfDirNested sets variables for default options called $CONFDIR and $CONFNAME.
|
|
// ConfDirNested also appends the parent struct field name to the paths before
|
|
// descending into substructs.
|
|
func ConfDirNested(confdir string) BindOpt {
|
|
val := filepath.Clean(os.ExpandEnv(confdir))
|
|
return BindOpt{varfn: func(vars map[string]confVar) {
|
|
vars["CONFDIR"] = confVar{val: val, nested: true}
|
|
vars["CONFNAME"] = confVar{val: val, nested: true}
|
|
}}
|
|
}
|
|
|
|
// IdentityDir sets a variable for the default option called $IDENTITYDIR.
|
|
func IdentityDir(path string) BindOpt {
|
|
val := filepath.Clean(os.ExpandEnv(path))
|
|
return BindOpt{varfn: func(vars map[string]confVar) {
|
|
vars["IDENTITYDIR"] = confVar{val: val, nested: false}
|
|
}}
|
|
}
|
|
|
|
// UseDevDefaults forces the bind call to use development defaults unless
|
|
// UseReleaseDefaults is provided as a subsequent option.
|
|
// Without either, Bind will default to determining which defaults to use
|
|
// based on version.Build.Release
|
|
func UseDevDefaults() BindOpt {
|
|
dev := true
|
|
return BindOpt{isDev: &dev}
|
|
}
|
|
|
|
// UseReleaseDefaults forces the bind call to use release defaults unless
|
|
// UseDevDefaults is provided as a subsequent option.
|
|
// Without either, Bind will default to determining which defaults to use
|
|
// based on version.Build.Release
|
|
func UseReleaseDefaults() BindOpt {
|
|
dev := false
|
|
return BindOpt{isDev: &dev}
|
|
}
|
|
|
|
type confVar struct {
|
|
val string
|
|
nested bool
|
|
}
|
|
|
|
// Bind sets flags on a FlagSet that match the configuration struct
|
|
// 'config'. This works by traversing the config struct using the 'reflect'
|
|
// package. Will ignore fields with `setup:"true"` tag.
|
|
func Bind(flags FlagSet, config interface{}, opts ...BindOpt) {
|
|
bind(flags, config, false, opts...)
|
|
}
|
|
|
|
// BindSetup sets flags on a FlagSet that match the configuration struct
|
|
// 'config'. This works by traversing the config struct using the 'reflect'
|
|
// package.
|
|
func BindSetup(flags FlagSet, config interface{}, opts ...BindOpt) {
|
|
bind(flags, config, true, opts...)
|
|
}
|
|
|
|
func bind(flags FlagSet, config interface{}, setupCommand bool, opts ...BindOpt) {
|
|
ptrtype := reflect.TypeOf(config)
|
|
if ptrtype.Kind() != reflect.Ptr {
|
|
panic(fmt.Sprintf("invalid config type: %#v. Expecting pointer to struct.", config))
|
|
}
|
|
isDev := !version.Build.Release
|
|
vars := map[string]confVar{}
|
|
for _, opt := range opts {
|
|
if opt.varfn != nil {
|
|
opt.varfn(vars)
|
|
}
|
|
if opt.isDev != nil {
|
|
isDev = *opt.isDev
|
|
}
|
|
}
|
|
|
|
bindConfig(flags, "", reflect.ValueOf(config).Elem(), vars, setupCommand, false, isDev)
|
|
}
|
|
|
|
func bindConfig(flags FlagSet, prefix string, val reflect.Value, vars map[string]confVar, setupCommand, setupStruct bool, isDev bool) {
|
|
if val.Kind() != reflect.Struct {
|
|
panic(fmt.Sprintf("invalid config type: %#v. Expecting struct.", val.Interface()))
|
|
}
|
|
typ := val.Type()
|
|
resolvedVars := make(map[string]string, len(vars))
|
|
{
|
|
structpath := strings.Replace(prefix, ".", "/", -1)
|
|
for k, v := range vars {
|
|
if !v.nested {
|
|
resolvedVars[k] = v.val
|
|
continue
|
|
}
|
|
resolvedVars[k] = filepath.Join(v.val, structpath)
|
|
}
|
|
}
|
|
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
fieldval := val.Field(i)
|
|
flagname := hyphenate(snakeCase(field.Name))
|
|
|
|
if field.Tag.Get("noprefix") != "true" {
|
|
flagname = prefix + flagname
|
|
}
|
|
|
|
if field.Tag.Get("internal") == "true" {
|
|
continue
|
|
}
|
|
|
|
onlyForSetup := (field.Tag.Get("setup") == "true") || setupStruct
|
|
// ignore setup params for non setup commands
|
|
if !setupCommand && onlyForSetup {
|
|
continue
|
|
}
|
|
|
|
switch field.Type.Kind() {
|
|
case reflect.Struct:
|
|
if field.Anonymous {
|
|
bindConfig(flags, prefix, fieldval, vars, setupCommand, onlyForSetup, isDev)
|
|
} else {
|
|
bindConfig(flags, flagname+".", fieldval, vars, setupCommand, onlyForSetup, isDev)
|
|
}
|
|
case reflect.Array:
|
|
digits := len(fmt.Sprint(fieldval.Len()))
|
|
for j := 0; j < fieldval.Len(); j++ {
|
|
padding := strings.Repeat("0", digits-len(fmt.Sprint(j)))
|
|
bindConfig(flags, fmt.Sprintf("%s.%s%d.", flagname, padding, j), fieldval.Index(j), vars, setupCommand, onlyForSetup, isDev)
|
|
}
|
|
default:
|
|
help := field.Tag.Get("help")
|
|
var def string
|
|
if isDev {
|
|
def = getDefault(field.Tag, "devDefault", "releaseDefault", "default", flagname)
|
|
} else {
|
|
def = getDefault(field.Tag, "releaseDefault", "devDefault", "default", flagname)
|
|
}
|
|
fieldaddr := fieldval.Addr().Interface()
|
|
check := func(err error) {
|
|
if err != nil {
|
|
panic(fmt.Sprintf("invalid default value for %s: %#v", flagname, def))
|
|
}
|
|
}
|
|
switch field.Type {
|
|
case reflect.TypeOf(memory.Size(0)):
|
|
check(fieldaddr.(*memory.Size).Set(def))
|
|
flags.Var(fieldaddr.(*memory.Size), flagname, help)
|
|
case reflect.TypeOf(int(0)):
|
|
val, err := strconv.ParseInt(def, 0, strconv.IntSize)
|
|
check(err)
|
|
flags.IntVar(fieldaddr.(*int), flagname, int(val), help)
|
|
case reflect.TypeOf(int64(0)):
|
|
val, err := strconv.ParseInt(def, 0, 64)
|
|
check(err)
|
|
flags.Int64Var(fieldaddr.(*int64), flagname, val, help)
|
|
case reflect.TypeOf(uint(0)):
|
|
val, err := strconv.ParseUint(def, 0, strconv.IntSize)
|
|
check(err)
|
|
flags.UintVar(fieldaddr.(*uint), flagname, uint(val), help)
|
|
case reflect.TypeOf(uint64(0)):
|
|
val, err := strconv.ParseUint(def, 0, 64)
|
|
check(err)
|
|
flags.Uint64Var(fieldaddr.(*uint64), flagname, val, help)
|
|
case reflect.TypeOf(time.Duration(0)):
|
|
val, err := time.ParseDuration(def)
|
|
check(err)
|
|
flags.DurationVar(fieldaddr.(*time.Duration), flagname, val, help)
|
|
case reflect.TypeOf(float64(0)):
|
|
val, err := strconv.ParseFloat(def, 64)
|
|
check(err)
|
|
flags.Float64Var(fieldaddr.(*float64), flagname, val, help)
|
|
case reflect.TypeOf(string("")):
|
|
flags.StringVar(
|
|
fieldaddr.(*string), flagname, expand(resolvedVars, def), help)
|
|
case reflect.TypeOf(bool(false)):
|
|
val, err := strconv.ParseBool(def)
|
|
check(err)
|
|
flags.BoolVar(fieldaddr.(*bool), flagname, val, help)
|
|
case reflect.TypeOf([]string(nil)):
|
|
flags.StringArrayVar(fieldaddr.(*[]string), flagname, nil, help)
|
|
default:
|
|
panic(fmt.Sprintf("invalid field type: %s", field.Type.String()))
|
|
}
|
|
if onlyForSetup {
|
|
setBoolAnnotation(flags, flagname, "setup")
|
|
}
|
|
if field.Tag.Get("user") == "true" {
|
|
setBoolAnnotation(flags, flagname, "user")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getDefault(tag reflect.StructTag, preferred, opposite, fallback, flagname string) string {
|
|
if val, ok := tag.Lookup(preferred); ok {
|
|
if _, oppositeExists := tag.Lookup(opposite); !oppositeExists {
|
|
panic(fmt.Sprintf("%q defined but %q missing for %v", preferred, opposite, flagname))
|
|
}
|
|
if _, fallbackExists := tag.Lookup(fallback); fallbackExists {
|
|
panic(fmt.Sprintf("%q defined along with %q fallback for %v", preferred, fallback, flagname))
|
|
}
|
|
return val
|
|
}
|
|
if _, oppositeExists := tag.Lookup(opposite); oppositeExists {
|
|
panic(fmt.Sprintf("%q missing but %q defined for %v", preferred, opposite, flagname))
|
|
}
|
|
return tag.Get(fallback)
|
|
}
|
|
|
|
func setBoolAnnotation(flagset interface{}, name, key string) {
|
|
flags, ok := flagset.(*pflag.FlagSet)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
err := flags.SetAnnotation(name, key, []string{"true"})
|
|
if err != nil {
|
|
panic(fmt.Sprintf("unable to set %s annotation for %s: %v", key, name, err))
|
|
}
|
|
}
|
|
|
|
func expand(vars map[string]string, val string) string {
|
|
return os.Expand(val, func(key string) string { return vars[key] })
|
|
}
|
|
|
|
// FindConfigDirParam returns '--config-dir' param from os.Args (if exists)
|
|
func FindConfigDirParam() string {
|
|
return FindFlagEarly("config-dir")
|
|
}
|
|
|
|
// FindIdentityDirParam returns '--identity-dir' param from os.Args (if exists)
|
|
func FindIdentityDirParam() string {
|
|
return FindFlagEarly("identity-dir")
|
|
}
|
|
|
|
// FindDefaultsParam returns '--defaults' param from os.Args (if it exists)
|
|
func FindDefaultsParam() string {
|
|
return FindFlagEarly("defaults")
|
|
}
|
|
|
|
// FindFlagEarly retrieves the value of a flag before `flag.Parse` has been called
|
|
func FindFlagEarly(flagName string) string {
|
|
// workaround to have early access to 'dir' param
|
|
for i, arg := range os.Args {
|
|
if strings.HasPrefix(arg, fmt.Sprintf("--%s=", flagName)) {
|
|
return strings.TrimPrefix(arg, fmt.Sprintf("--%s=", flagName))
|
|
} else if arg == fmt.Sprintf("--%s", flagName) && i < len(os.Args)-1 {
|
|
return os.Args[i+1]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// SetupFlag sets up flags that are needed before `flag.Parse` has been called
|
|
func SetupFlag(log *zap.Logger, cmd *cobra.Command, dest *string, name, value, usage string) {
|
|
if foundValue := FindFlagEarly(name); foundValue != "" {
|
|
value = foundValue
|
|
}
|
|
cmd.PersistentFlags().StringVar(dest, name, value, usage)
|
|
if cmd.PersistentFlags().SetAnnotation(name, "setup", []string{"true"}) != nil {
|
|
log.Sugar().Errorf("Failed to set 'setup' annotation for '%s'", name)
|
|
}
|
|
}
|
|
|
|
// DefaultsFlag sets up the defaults=dev/release flag options, which is needed
|
|
// before `flag.Parse` has been called
|
|
func DefaultsFlag(cmd *cobra.Command) BindOpt {
|
|
// define a flag so that the flag parsing system will be happy.
|
|
defaults := "dev"
|
|
if version.Build.Release {
|
|
defaults = "release"
|
|
}
|
|
// we're actually going to ignore this flag entirely and parse the commandline
|
|
// arguments early instead
|
|
_ = cmd.PersistentFlags().String("defaults", defaults,
|
|
"determines which set of configuration defaults to use. can either be 'dev' or 'release'")
|
|
|
|
foundDefaults := strings.ToLower(FindDefaultsParam())
|
|
if foundDefaults == "" {
|
|
foundDefaults = defaults
|
|
}
|
|
|
|
switch foundDefaults {
|
|
case "dev":
|
|
return UseDevDefaults()
|
|
case "release":
|
|
return UseReleaseDefaults()
|
|
default:
|
|
panic(fmt.Sprintf("unsupported defaults value %q", FindDefaultsParam()))
|
|
}
|
|
}
|