github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/admin-user-svcacct-add.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"bytes"
    22  	"crypto/rand"
    23  	"encoding/base64"
    24  	"fmt"
    25  	"os"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/dustin/go-humanize"
    30  	"github.com/fatih/color"
    31  	"github.com/minio/cli"
    32  	json "github.com/minio/colorjson"
    33  	"github.com/minio/madmin-go/v3"
    34  	"github.com/minio/mc/pkg/probe"
    35  	"github.com/minio/pkg/v2/console"
    36  	"github.com/minio/pkg/v2/policy"
    37  )
    38  
    39  var adminUserSvcAcctAddFlags = []cli.Flag{
    40  	cli.StringFlag{
    41  		Name:  "access-key",
    42  		Usage: "set an access key for the service account",
    43  	},
    44  	cli.StringFlag{
    45  		Name:  "secret-key",
    46  		Usage: "set a secret key for the service account",
    47  	},
    48  	cli.StringFlag{
    49  		Name:  "policy",
    50  		Usage: "path to a JSON policy file",
    51  	},
    52  	cli.StringFlag{
    53  		Name:  "name",
    54  		Usage: "friendly name for the service account",
    55  	},
    56  	cli.StringFlag{
    57  		Name:  "description",
    58  		Usage: "description for the service account",
    59  	},
    60  	cli.StringFlag{
    61  		Name:   "comment",
    62  		Hidden: true,
    63  		Usage:  "description for the service account (DEPRECATED: use --description instead)",
    64  	},
    65  	cli.StringFlag{
    66  		Name:  "expiry",
    67  		Usage: "time of expiration for the service account",
    68  	},
    69  }
    70  
    71  var adminUserSvcAcctAddCmd = cli.Command{
    72  	Name:         "add",
    73  	Usage:        "add a new service account",
    74  	Action:       mainAdminUserSvcAcctAdd,
    75  	OnUsageError: onUsageError,
    76  	Before:       setGlobalsFromContext,
    77  	Flags:        append(adminUserSvcAcctAddFlags, globalFlags...),
    78  	CustomHelpTemplate: `NAME:
    79    {{.HelpName}} - {{.Usage}}
    80  
    81  USAGE:
    82    {{.HelpName}} ALIAS ACCOUNT [FLAGS]
    83  
    84  ACCOUNT:
    85    An account could be a regular MinIO user, STS or LDAP user.
    86  
    87  FLAGS:
    88    {{range .VisibleFlags}}{{.}}
    89    {{end}}
    90  EXAMPLES:
    91    1. Add a new service account for user 'foobar' to MinIO server with a name and description.
    92       {{.Prompt}} {{.HelpName}} myminio foobar --name uploaderKey --description "foobar uploader scripts"
    93  
    94    2. Add a new service account to MinIO server with specified access key and secret key for user 'foobar'.
    95       {{.Prompt}} {{.HelpName}} myminio foobar --access-key "myaccesskey" --secret-key "mysecretkey"
    96  
    97    3. Add a new service account to MinIO server with specified access key and random secret key for user 'foobar'.
    98       {{.Prompt}} {{.HelpName}} myminio foobar --access-key "myaccesskey"
    99  
   100    4. Add a new service account to MinIO server with specified secret key and random access key for user 'foobar'.
   101       {{.Prompt}} {{.HelpName}} myminio foobar --secret-key "mysecretkey"
   102  
   103    5. Add a new service account to MinIO server with specified expiry date in the future for user 'foobar'.
   104       {{.Prompt}} {{.HelpName}} myminio foobar --expiry 2023-06-24T10:00:00-07:00
   105  `,
   106  }
   107  
   108  // checkAdminUserSvcAcctAddSyntax - validate all the passed arguments
   109  func checkAdminUserSvcAcctAddSyntax(ctx *cli.Context) {
   110  	if len(ctx.Args()) != 2 {
   111  		showCommandHelpAndExit(ctx, 1)
   112  	}
   113  }
   114  
   115  // acctMessage container for content message structure
   116  type acctMessage struct {
   117  	op            acctOp
   118  	Status        string          `json:"status"`
   119  	AccessKey     string          `json:"accessKey,omitempty"`
   120  	SecretKey     string          `json:"secretKey,omitempty"`
   121  	ParentUser    string          `json:"parentUser,omitempty"`
   122  	ImpliedPolicy bool            `json:"impliedPolicy,omitempty"`
   123  	Policy        json.RawMessage `json:"policy,omitempty"`
   124  	Name          string          `json:"name,omitempty"`
   125  	Description   string          `json:"description,omitempty"`
   126  	AccountStatus string          `json:"accountStatus,omitempty"`
   127  	MemberOf      []string        `json:"memberOf,omitempty"`
   128  	Expiration    *time.Time      `json:"expiration,omitempty"`
   129  }
   130  
   131  const (
   132  	accessFieldMaxLen = 20
   133  )
   134  
   135  type acctOp int
   136  
   137  const (
   138  	svcAccOpAdd = acctOp(iota)
   139  	svcAccOpList
   140  	svcAccOpInfo
   141  	svcAccOpRemove
   142  	svcAccOpDisable
   143  	svcAccOpEnable
   144  	svcAccOpSet
   145  
   146  	stsAccOpInfo
   147  
   148  	// Maximum length for MinIO access key.
   149  	// There is no max length enforcement for access keys
   150  	accessKeyMaxLen = 20
   151  
   152  	// Maximum length for Expiration timestamp
   153  	expirationMaxLen = 29
   154  
   155  	// Alpha numeric table used for generating access keys.
   156  	alphaNumericTable = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
   157  
   158  	// Total length of the alpha numeric table.
   159  	alphaNumericTableLen = byte(len(alphaNumericTable))
   160  
   161  	// Maximum secret key length for MinIO, this
   162  	// is used when autogenerating new credentials.
   163  	// There is no max length enforcement for secret keys
   164  	secretKeyMaxLen = 40
   165  )
   166  
   167  var supportedTimeFormats = []string{
   168  	"2006-01-02",
   169  	"2006-01-02T15:04",
   170  	"2006-01-02T15:04:05",
   171  	time.RFC3339,
   172  }
   173  
   174  func (u acctMessage) String() string {
   175  	switch u.op {
   176  	case svcAccOpList:
   177  		// Create a new pretty table with cols configuration
   178  		return newPrettyTable(" | ",
   179  			Field{"AccessKey", accessFieldMaxLen},
   180  			Field{"Expiration", expirationMaxLen},
   181  		).buildRow(u.AccessKey, func() string {
   182  			if u.Expiration != nil && !u.Expiration.IsZero() {
   183  				return (*u.Expiration).String()
   184  			}
   185  			return "no-expiry"
   186  		}())
   187  	case stsAccOpInfo, svcAccOpInfo:
   188  		policyField := ""
   189  		if u.ImpliedPolicy {
   190  			policyField = "implied"
   191  		} else {
   192  			policyField = "embedded"
   193  		}
   194  		return console.Colorize("AccMessage", strings.Join(
   195  			[]string{
   196  				fmt.Sprintf("AccessKey: %s", u.AccessKey),
   197  				fmt.Sprintf("ParentUser: %s", u.ParentUser),
   198  				fmt.Sprintf("Status: %s", u.AccountStatus),
   199  				fmt.Sprintf("Name: %s", u.Name),
   200  				fmt.Sprintf("Description: %s", u.Description),
   201  				fmt.Sprintf("Policy: %s", policyField),
   202  				func() string {
   203  					if u.Expiration != nil {
   204  						return fmt.Sprintf("Expiration: %s", humanize.Time(*u.Expiration))
   205  					}
   206  					return "Expiration: no-expiry"
   207  				}(),
   208  			}, "\n"))
   209  	case svcAccOpRemove:
   210  		return console.Colorize("AccMessage", "Removed service account `"+u.AccessKey+"` successfully.")
   211  	case svcAccOpDisable:
   212  		return console.Colorize("AccMessage", "Disabled service account `"+u.AccessKey+"` successfully.")
   213  	case svcAccOpEnable:
   214  		return console.Colorize("AccMessage", "Enabled service account `"+u.AccessKey+"` successfully.")
   215  	case svcAccOpAdd:
   216  		if u.Expiration != nil && !u.Expiration.IsZero() && !u.Expiration.Equal(timeSentinel) {
   217  			return console.Colorize("AccMessage",
   218  				fmt.Sprintf("Access Key: %s\nSecret Key: %s\nExpiration: %s", u.AccessKey, u.SecretKey, *u.Expiration))
   219  		}
   220  		return console.Colorize("AccMessage",
   221  			fmt.Sprintf("Access Key: %s\nSecret Key: %s\nExpiration: no-expiry", u.AccessKey, u.SecretKey))
   222  	case svcAccOpSet:
   223  		return console.Colorize("AccMessage", "Edited service account `"+u.AccessKey+"` successfully.")
   224  	}
   225  	return ""
   226  }
   227  
   228  // generateCredentials - creates randomly generated credentials of maximum
   229  // allowed length.
   230  func generateCredentials() (accessKey, secretKey string, err error) {
   231  	readBytes := func(size int) (data []byte, err error) {
   232  		data = make([]byte, size)
   233  		var n int
   234  		if n, err = rand.Read(data); err != nil {
   235  			return nil, err
   236  		} else if n != size {
   237  			return nil, fmt.Errorf("Not enough data. Expected to read: %v bytes, got: %v bytes", size, n)
   238  		}
   239  		return data, nil
   240  	}
   241  
   242  	// Generate access key.
   243  	keyBytes, err := readBytes(accessKeyMaxLen)
   244  	if err != nil {
   245  		return "", "", err
   246  	}
   247  	for i := 0; i < accessKeyMaxLen; i++ {
   248  		keyBytes[i] = alphaNumericTable[keyBytes[i]%alphaNumericTableLen]
   249  	}
   250  	accessKey = string(keyBytes)
   251  
   252  	// Generate secret key.
   253  	keyBytes, err = readBytes(secretKeyMaxLen)
   254  	if err != nil {
   255  		return "", "", err
   256  	}
   257  
   258  	secretKey = strings.ReplaceAll(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]),
   259  		"/", "+")
   260  
   261  	return accessKey, secretKey, nil
   262  }
   263  
   264  func (u acctMessage) JSON() string {
   265  	u.Status = "success"
   266  	jsonMessageBytes, e := json.MarshalIndent(u, "", " ")
   267  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   268  
   269  	return string(jsonMessageBytes)
   270  }
   271  
   272  // mainAdminUserSvcAcctAdd is the handle for "mc admin user svcacct add" command.
   273  func mainAdminUserSvcAcctAdd(ctx *cli.Context) error {
   274  	checkAdminUserSvcAcctAddSyntax(ctx)
   275  
   276  	console.SetColor("AccMessage", color.New(color.FgGreen))
   277  
   278  	// Get the alias parameter from cli
   279  	args := ctx.Args()
   280  	aliasedURL := args.Get(0)
   281  	user := args.Get(1)
   282  
   283  	accessKey := ctx.String("access-key")
   284  	secretKey := ctx.String("secret-key")
   285  	policyPath := ctx.String("policy")
   286  	name := ctx.String("name")
   287  	description := ctx.String("description")
   288  	if description == "" {
   289  		description = ctx.String("comment")
   290  	}
   291  	expiry := ctx.String("expiry")
   292  
   293  	// generate access key and secret key
   294  	if len(accessKey) <= 0 || len(secretKey) <= 0 {
   295  		randomAccessKey, randomSecretKey, err := generateCredentials()
   296  		if err != nil {
   297  			fatalIf(probe.NewError(err), "Unable to add a new service account")
   298  		}
   299  		if len(accessKey) <= 0 {
   300  			accessKey = randomAccessKey
   301  		}
   302  		if len(secretKey) <= 0 {
   303  			secretKey = randomSecretKey
   304  		}
   305  	}
   306  
   307  	// Create a new MinIO Admin Client
   308  	client, err := newAdminClient(aliasedURL)
   309  	fatalIf(err, "Unable to initialize admin connection.")
   310  
   311  	var policyBytes []byte
   312  	if policyPath != "" {
   313  		// Validate the policy document and ensure it has at least when statement
   314  		var e error
   315  		policyBytes, e = os.ReadFile(policyPath)
   316  		fatalIf(probe.NewError(e), "Unable to open the policy document.")
   317  		p, e := policy.ParseConfig(bytes.NewReader(policyBytes))
   318  		fatalIf(probe.NewError(e), "Unable to parse the policy document.")
   319  		if p.IsEmpty() {
   320  			fatalIf(errInvalidArgument(), "Empty policy documents are not allowed.")
   321  		}
   322  	}
   323  
   324  	var expiryTime time.Time
   325  	var expiryPointer *time.Time
   326  
   327  	if expiry != "" {
   328  		location, e := time.LoadLocation("Local")
   329  		if e != nil {
   330  			fatalIf(probe.NewError(e), "Unable to parse the expiry argument.")
   331  		}
   332  
   333  		patternMatched := false
   334  		for _, format := range supportedTimeFormats {
   335  			t, e := time.ParseInLocation(format, expiry, location)
   336  			if e == nil {
   337  				patternMatched = true
   338  				expiryTime = t
   339  				expiryPointer = &expiryTime
   340  				break
   341  			}
   342  		}
   343  
   344  		if !patternMatched {
   345  			fatalIf(probe.NewError(fmt.Errorf("expiry argument is not matching any of the supported patterns")), "unable to parse the expiry argument.")
   346  		}
   347  	}
   348  
   349  	opts := madmin.AddServiceAccountReq{
   350  		Policy:      policyBytes,
   351  		AccessKey:   accessKey,
   352  		SecretKey:   secretKey,
   353  		Name:        name,
   354  		Description: description,
   355  		TargetUser:  user,
   356  		Expiration:  expiryPointer,
   357  	}
   358  
   359  	creds, e := client.AddServiceAccount(globalContext, opts)
   360  	fatalIf(probe.NewError(e).Trace(args...), "Unable to add a new service account.")
   361  
   362  	printMsg(acctMessage{
   363  		op:            svcAccOpAdd,
   364  		AccessKey:     creds.AccessKey,
   365  		SecretKey:     creds.SecretKey,
   366  		Expiration:    &creds.Expiration,
   367  		AccountStatus: "enabled",
   368  	})
   369  
   370  	return nil
   371  }