-
Notifications
You must be signed in to change notification settings - Fork 0
/
iam.go
449 lines (395 loc) · 13.2 KB
/
iam.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
package main
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
grpc_status "google.golang.org/grpc/status"
authz_constants "github.com/chef/automate/components/authz-service/constants"
v2_constants "github.com/chef/automate/components/authz-service/constants/v2"
"github.com/chef/automate/components/automate-cli/pkg/adminmgmt"
"github.com/chef/automate/components/automate-cli/pkg/client/apiclient"
"github.com/chef/automate/components/automate-cli/pkg/status"
policies_common "github.com/chef/automate/components/automate-gateway/api/iam/v2beta/common"
policies_req "github.com/chef/automate/components/automate-gateway/api/iam/v2beta/request"
)
var iamCmdFlags = struct {
dryRun bool
adminToken bool
tokenID string
betaVersion bool
}{}
func newIAMCommand() *cobra.Command {
return &cobra.Command{
Use: "iam COMMAND",
Short: "Chef Automate iam commands",
}
}
func newIAMAdminAccessCommand() *cobra.Command {
return &cobra.Command{
Use: "admin-access COMMAND",
Short: "Manage and restore default admin access",
}
}
func newIAMTokensCommand() *cobra.Command {
return &cobra.Command{
Use: "token COMMAND",
Short: "Manage tokens",
}
}
func newIAMCreateTokenCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "create NAME",
Short: "Generate a token",
RunE: runCreateTokenCmd,
Args: cobra.ExactArgs(1),
}
cmd.PersistentFlags().BoolVar(
&iamCmdFlags.adminToken,
"admin",
false,
"Generate a token and grant it admin-level permission (IAM v2)")
cmd.PersistentFlags().StringVar(
&iamCmdFlags.tokenID,
"id",
"",
"Specify a custom ID (if omitted, an ID will be generated based on NAME)")
return cmd
}
func newIAMRestoreDefaultAdminAccessCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "restore PASSWORD",
Short: "Restore the factory default admin user, team, and access",
Long: "Recreate the admin user, admin team, and related admin policy as needed " +
"to restore to factory default and update the admin user's password",
RunE: runRestoreDefaultAdminAccessAdminCmd,
Args: cobra.ExactArgs(1),
}
cmd.PersistentFlags().BoolVar(
&iamCmdFlags.dryRun,
"dry-run",
false,
"Show what would be updated by this command without performing any changes")
return cmd
}
func newIAMUpgradeToV2Cmd() *cobra.Command {
cmd := &cobra.Command{
Use: "upgrade-to-v2",
Short: "Upgrade to IAM v2",
Long: "Upgrade to IAM v2 and migrate existing v1 policies. " +
"On downgrade, any new v2 policies will be reverted.",
RunE: runIAMUpgradeToV2Cmd,
Args: cobra.ExactArgs(0),
}
cmd.PersistentFlags().BoolVar(
&iamCmdFlags.betaVersion,
"beta2.1",
false,
"Upgrade to version 2.1 with beta project authorization.")
err := cmd.PersistentFlags().MarkHidden("beta2.1")
// we could also ignore the lint error :shrug:
if err != nil {
fmt.Printf("failed configuring cobra: %s\n", err.Error())
panic(err.Error())
}
return cmd
}
func newIAMResetToV1Cmd() *cobra.Command {
return &cobra.Command{
Use: "reset-to-v1",
Short: "Reset to IAM v1",
Long: "Reset to IAM v1. This will revert to policies in place before upgrade to IAM v2 " +
"and will remove any v2 policies in place, even if upgrade is re-applied.",
RunE: runIAMResetToV1Cmd,
Args: cobra.ExactArgs(0),
}
}
func newIAMVersionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Retrieve IAM version in use",
RunE: runIAMVersionCmd,
Args: cobra.ExactArgs(0),
}
}
func init() {
iamCommand := newIAMCommand()
iamCommand.AddCommand(newIAMUpgradeToV2Cmd())
iamCommand.AddCommand(newIAMResetToV1Cmd())
iamCommand.AddCommand(newIAMVersionCmd())
iamAdminAccessCommand := newIAMAdminAccessCommand()
iamCommand.AddCommand(iamAdminAccessCommand)
iamAdminAccessCommand.AddCommand(newIAMRestoreDefaultAdminAccessCmd())
iamTokensCommand := newIAMTokensCommand()
iamCommand.AddCommand(iamTokensCommand)
iamTokensCommand.AddCommand(newIAMCreateTokenCommand())
RootCmd.AddCommand(iamCommand)
}
// Note: the indentation is to keep this in line with writer.Body()
const alreadyMigratedMessage = `You have already upgraded to IAM %s.
If you wish to re-run the migration, first run:
chef-automate iam reset-to-v1
Then re-run this command.`
func runIAMUpgradeToV2Cmd(cmd *cobra.Command, args []string) error {
upgradeReq := &policies_req.UpgradeToV2Req{
Flag: policies_common.Flag_VERSION_2_0,
}
isBetaVersion := iamCmdFlags.betaVersion
if isBetaVersion {
upgradeReq.Flag = policies_common.Flag_VERSION_2_1
writer.Title("Enabling IAM v2.1")
} else {
writer.Title("Upgrading to IAM v2")
}
writer.Println("Migrating v1 policies...")
ctx := context.Background()
apiClient, err := apiclient.OpenConnection(ctx)
if err != nil {
return err
}
resp, err := apiClient.PoliciesClient().UpgradeToV2(ctx, upgradeReq)
switch grpc_status.Convert(err).Code() {
case codes.OK:
if len(resp.GetReports()) > 0 {
writer.Println("\nSkipped policies:")
}
for i, report := range resp.GetReports() {
if i > 0 {
writer.Print("\n")
}
outputReport(report)
}
case codes.FailedPrecondition:
return status.Wrap(err, status.IAMUpgradeV2DatabaseError,
"Migration to IAM v2 already in progress")
case codes.AlreadyExists:
if isBetaVersion {
writer.Failf(alreadyMigratedMessage, "v2.1")
} else {
writer.Failf(alreadyMigratedMessage, "v2")
}
return nil
default: // something else: fail
return status.Wrap(err, status.IAMUpgradeV2DatabaseError,
"Failed to reset IAM v2 database state")
}
writer.Println("Creating default teams Editors and Viewers...")
for id, description := range map[string]string{
"editors": "Editors",
"viewers": "Viewers",
} {
_, found, err := adminmgmt.EnsureTeam(ctx, id, description, apiClient, false /* no dryrun here */)
if err != nil {
return err
}
if found {
writer.Skippedf("%s team already exists", description)
}
}
writer.Print("\n")
writer.Print("Migrating existing teams...\n\n")
_, err = apiClient.TeamsV2Client().ApplyV2DataMigrations(ctx,
&policies_req.ApplyV2DataMigrationsReq{})
if err != nil {
return status.Wrap(err, status.IAMUpgradeV2DatabaseError,
"Failed to migrate teams service")
}
label := map[bool]string{
true: "v2.1",
false: "v2",
}
writer.Successf("Enabled IAM %s", label[isBetaVersion])
return nil
}
func outputReport(report string) {
// if it's got ":" in it, split on the first
parts := strings.SplitN(report, ":", 2)
writer.Body(parts[0])
if parts[1] != "" {
writer.Body(strings.TrimSpace(parts[1]))
}
}
func runIAMResetToV1Cmd(cmd *cobra.Command, args []string) error {
writer.Title("Reverting to IAM v1 (with pre-upgrade v1 policies)")
ctx := context.Background()
apiClient, err := apiclient.OpenConnection(ctx)
if err != nil {
return status.Wrap(err, status.APIUnreachableError,
"Failed to create a connection to the API")
}
_, err = apiClient.PoliciesClient().ResetToV1(ctx, &policies_req.ResetToV1Req{})
switch grpc_status.Convert(err).Code() {
case codes.OK: // nice!
case codes.FailedPrecondition:
return status.Wrap(err, status.IAMResetV1DatabaseError,
"Migration to IAMv2 in progress")
default: // something else: fail
return status.Wrap(err, status.IAMResetV1DatabaseError,
"Failed to reset IAM state to v1")
}
return nil
}
func runIAMVersionCmd(cmd *cobra.Command, args []string) error {
ctx := context.Background()
apiClient, err := apiclient.OpenConnection(ctx)
if err != nil {
return status.Wrap(err, status.APIUnreachableError,
"Failed to create a connection to the API")
}
resp, err := apiClient.PoliciesClient().GetPolicyVersion(ctx, &policies_req.GetPolicyVersionReq{})
if err != nil {
return status.Wrap(err, status.APIError, "Failed to retrieve IAM version")
}
major := strings.ToLower(resp.Version.Major.String())
minor := strings.Replace(resp.Version.Minor.String(), "V", ".", 1)
version := major + minor
writer.Printf("IAM %s\n", version)
return nil
}
func runRestoreDefaultAdminAccessAdminCmd(cmd *cobra.Command, args []string) error {
if iamCmdFlags.dryRun {
writer.Title("Dry run: showing all actions needed to restore default admin access without performing any changes\n")
} else {
writer.Title("Restoring all factory defaults for admin access\n")
}
newAdminPassword := args[0]
ctx := context.Background()
apiClient, err := apiclient.OpenConnection(ctx)
if err != nil {
return status.Wrap(err, status.APIUnreachableError, "Failed to create a connection to the API")
}
// restore admin user and team if needed
userID, adminUserFound, err := adminmgmt.CreateAdminUserOrUpdatePassword(ctx,
apiClient, newAdminPassword, iamCmdFlags.dryRun)
if err != nil {
return err
}
if adminUserFound {
writer.Success("Updated existing admin user's password")
} else {
writer.Success("Created new admin user with specified password")
}
adminsTeamID, adminsTeamFound, err := adminmgmt.CreateAdminTeamIfMissing(ctx,
apiClient, iamCmdFlags.dryRun)
if err != nil {
return err
}
if adminsTeamFound {
writer.Skipped("Found admins team")
} else {
writer.Success("Recreated admins team")
}
// In dry-run mode, we might be missing some IDs that would have been created.
// We'll only hit this condition in dry-run mode.
if iamCmdFlags.dryRun && (userID == "" || adminsTeamID == "") {
writer.Success("Added admin user to admins team")
} else { // non-dry-run mode or dry-run mode where user and team already existed.
userAdded, err := adminmgmt.AddAdminUserToTeam(ctx,
apiClient, adminsTeamID, userID, iamCmdFlags.dryRun)
if err != nil {
return err
}
if userAdded {
writer.Success("Added admin user to admins team")
} else {
writer.Skipped("Admin user already existed in admins team")
}
}
// grant access to admins team if needed
resp, err := apiClient.PoliciesClient().GetPolicyVersion(ctx, &policies_req.GetPolicyVersionReq{})
if err != nil {
return status.Wrap(err, status.APIError, "Failed to verify IAM version")
}
writer.Titlef("Checking IAM %s policies for admin policy with admins team.\n", resp.Version)
switch resp.Version.Major {
case policies_common.Version_V1:
foundV1AdminPolicy, createdNewV1Policy, err := adminmgmt.UpdateV1AdminsPolicyIfNeeded(ctx,
apiClient, iamCmdFlags.dryRun)
if err != nil {
return err
}
if foundV1AdminPolicy {
writer.Skipped("Found admin policy that contains the admins team")
} else {
// Note: (tc) This should never happen currently since we currently don't support
// editing policies but adding for future-proofing against the functionality.
// Note: (sr) PurgeSubjectFromPolicies can alter policies -- when a user or a
// team is removed; so, this could be more realistic than we think.
writer.Successf("Found default admins team policy but it did not contain "+
"the admins team subject (%s). Added admins team to default admin policy.",
authz_constants.LocalAdminsTeamSubject)
}
if createdNewV1Policy {
writer.Success("Created new admins policy")
}
case policies_common.Version_V2:
foundAdminsTeaminV2AdminPolicy, err := adminmgmt.UpdateV2AdminsPolicyIfNeeded(ctx,
apiClient, iamCmdFlags.dryRun)
if err != nil {
return err
}
if !foundAdminsTeaminV2AdminPolicy {
writer.Success("Added local team: admins to Chef-managed policy: Admin")
}
writer.Skipped("Found local team: admins in Chef-managed policy: Admin")
default:
// do nothing
}
if err := apiClient.CloseConnection(); err != nil {
return status.Wrap(err, status.APIUnreachableError, "Failed to close connection to the API")
}
return nil
}
const adminTokenIAMPreconditionError = "`chef-automate iam token create NAME --admin` is an IAM v2 command.\n" +
"For v1 use `chef-automate admin-token`.\n"
func runCreateTokenCmd(cmd *cobra.Command, args []string) error {
name := args[0]
ctx := context.Background()
apiClient, err := apiclient.OpenConnection(ctx)
if err != nil {
return status.Wrap(err, status.APIUnreachableError,
"Failed to create a connection to the API.")
}
var id string
if iamCmdFlags.tokenID == "" {
re := regexp.MustCompile(`[^a-z0-9]`)
id = strings.ToLower(name)
id = re.ReplaceAllString(id, "-")
} else {
id = iamCmdFlags.tokenID
}
tokenResp, err := apiClient.TokensV2Client().CreateToken(ctx, &policies_req.CreateTokenReq{
Id: id,
Name: name,
Active: true,
// TODO (tc): Might want to let them specify a --projects list somehow eventually.
Projects: []string{},
})
if err != nil {
return status.Wrap(err, status.APIError, "Failed to generate new token.")
}
if iamCmdFlags.adminToken {
resp, err := apiClient.PoliciesClient().GetPolicyVersion(ctx, &policies_req.GetPolicyVersionReq{})
if err != nil {
return status.Wrap(err, status.APIError, "Failed to retrieve IAM version")
}
if resp.Version.Major == policies_common.Version_V1 {
return status.New(status.APIError, adminTokenIAMPreconditionError)
}
member := fmt.Sprintf("token:%s", tokenResp.Token.Id)
_, err = apiClient.PoliciesClient().AddPolicyMembers(ctx, &policies_req.AddPolicyMembersReq{
Id: v2_constants.AdminPolicyID,
Members: []string{member},
})
if err != nil {
return status.Wrap(err, status.APIError, "Failed to add token as a member of chef-managed Admin policy.")
}
}
status.GlobalResult = struct {
Token string `json:"token"`
}{Token: tokenResp.Token.Value}
writer.Println(tokenResp.Token.Value)
return nil
}