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 }