-
Notifications
You must be signed in to change notification settings - Fork 567
/
cmds.go
547 lines (521 loc) · 19.2 KB
/
cmds.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
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
package cmds
import (
"bufio"
"fmt"
"os"
"strings"
"text/template"
"time"
"github.com/pachyderm/pachyderm/src/client"
"github.com/pachyderm/pachyderm/src/client/auth"
"github.com/pachyderm/pachyderm/src/client/pkg/config"
"github.com/pachyderm/pachyderm/src/client/pkg/grpcutil"
"github.com/pachyderm/pachyderm/src/server/pkg/cmdutil"
"github.com/spf13/cobra"
)
var githubAuthLink = `https://github.com/login/oauth/authorize?client_id=d3481e92b4f09ea74ff8&redirect_uri=https%3A%2F%2Fpachyderm.io%2Flogin-hook%2Fdisplay-token.html`
func githubLogin() (string, error) {
fmt.Println("(1) Please paste this link into a browser:\n\n" +
githubAuthLink + "\n\n" +
"(You will be directed to GitHub and asked to authorize Pachyderm's " +
"login app on GitHub. If you accept, you will be given a token to " +
"paste here, which will give you an externally verified account in " +
"this Pachyderm cluster)\n\n(2) Please paste the token you receive " +
"from GitHub here:")
token, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return "", fmt.Errorf("error reading token: %v", err)
}
return strings.TrimSpace(token), nil // drop trailing newline
}
func writePachTokenToCfg(token string) error {
cfg, err := config.Read()
if err != nil {
return fmt.Errorf("error reading Pachyderm config (for cluster "+
"address): %v", err)
}
_, context, err := cfg.ActiveContext()
if err != nil {
return fmt.Errorf("error getting the active context: %v", err)
}
context.SessionToken = token
if err := cfg.Write(); err != nil {
return fmt.Errorf("error writing pachyderm config: %v", err)
}
return nil
}
// ActivateCmd returns a cobra.Command to activate Pachyderm's auth system
func ActivateCmd() *cobra.Command {
var initialAdmin string
activate := &cobra.Command{
Short: "Activate Pachyderm's auth system",
Long: `
Activate Pachyderm's auth system, and restrict access to existing data to the
user running the command (or the argument to --initial-admin), who will be the
first cluster admin`[1:],
Run: cmdutil.Run(func(args []string) error {
var token string
var err error
if !strings.HasPrefix(initialAdmin, auth.RobotPrefix) {
token, err = githubLogin()
if err != nil {
return err
}
}
fmt.Println("Retrieving Pachyderm token...")
// Exchange GitHub token for Pachyderm token
c, err := client.NewOnUserMachine("user")
if err != nil {
return fmt.Errorf("could not connect: %v", err)
}
defer c.Close()
resp, err := c.Activate(c.Ctx(),
&auth.ActivateRequest{
GitHubToken: token,
Subject: initialAdmin,
})
if err != nil {
return fmt.Errorf("error activating Pachyderm auth: %v",
grpcutil.ScrubGRPC(err))
}
if err := writePachTokenToCfg(resp.PachToken); err != nil {
return err
}
if strings.HasPrefix(initialAdmin, auth.RobotPrefix) {
fmt.Println("WARNING: DO NOT LOSE THE ROBOT TOKEN BELOW WITHOUT " +
"ADDING OTHER ADMINS.\nIF YOU DO, YOU WILL BE PERMANENTLY LOCKED OUT " +
"OF YOUR CLUSTER!")
fmt.Printf("Pachyderm token for \"%s\":\n%s\n", initialAdmin, resp.PachToken)
}
return nil
}),
}
activate.PersistentFlags().StringVar(&initialAdmin, "initial-admin", "", `
The subject (robot user or github user) who
will be the first cluster admin; the user running 'activate' will identify as
this user once auth is active. If you set 'initial-admin' to a robot
user, pachctl will print that robot user's Pachyderm token; this token is
effectively a root token, and if it's lost you will be locked out of your
cluster`[1:])
return cmdutil.CreateAlias(activate, "auth activate")
}
// DeactivateCmd returns a cobra.Command to delete all ACLs, tokens, and admins,
// deactivating Pachyderm's auth system
func DeactivateCmd() *cobra.Command {
deactivate := &cobra.Command{
Short: "Delete all ACLs, tokens, and admins, and deactivate Pachyderm auth",
Long: "Deactivate Pachyderm's auth system, which will delete ALL auth " +
"tokens, ACLs and admins, and expose all data in the cluster to any " +
"user with cluster access. Use with caution.",
Run: cmdutil.Run(func(args []string) error {
fmt.Println("Are you sure you want to delete ALL auth information " +
"(ACLs, tokens, and admins) in this cluster, and expose ALL data? yN")
confirm, err := bufio.NewReader(os.Stdin).ReadString('\n')
if !strings.Contains("yY", confirm[:1]) {
return fmt.Errorf("operation aborted")
}
c, err := client.NewOnUserMachine("user")
if err != nil {
return fmt.Errorf("could not connect: %v", err)
}
defer c.Close()
_, err = c.Deactivate(c.Ctx(), &auth.DeactivateRequest{})
return grpcutil.ScrubGRPC(err)
}),
}
return cmdutil.CreateAlias(deactivate, "auth deactivate")
}
// LoginCmd returns a cobra.Command to login to a Pachyderm cluster with your
// GitHub account. Any resources that have been restricted to the email address
// registered with your GitHub account will subsequently be accessible.
func LoginCmd() *cobra.Command {
var useOTP bool
login := &cobra.Command{
Short: "Log in to Pachyderm",
Long: "Login to Pachyderm. Any resources that have been restricted to " +
"the account you have with your ID provider (e.g. GitHub, Okta) " +
"account will subsequently be accessible.",
Run: cmdutil.Run(func([]string) error {
c, err := client.NewOnUserMachine("user")
if err != nil {
return fmt.Errorf("could not connect: %v", err)
}
defer c.Close()
// Issue authentication request to Pachyderm and get response
var resp *auth.AuthenticateResponse
var authErr error
if useOTP {
// Exhange short-lived Pachyderm auth code for long-lived Pachyderm token
fmt.Println("Please enter your Pachyderm One-Time Password:")
code, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return fmt.Errorf("error reading One-Time Password: %v", err)
}
code = strings.TrimSpace(code) // drop trailing newline
resp, authErr = c.Authenticate(
c.Ctx(),
&auth.AuthenticateRequest{OneTimePassword: code})
} else {
// Exchange GitHub token for Pachyderm token
token, err := githubLogin()
if err != nil {
return err
}
fmt.Println("Retrieving Pachyderm token...")
resp, authErr = c.Authenticate(
c.Ctx(),
&auth.AuthenticateRequest{GitHubToken: token})
}
// Write new Pachyderm token to config
if authErr != nil {
if auth.IsErrPartiallyActivated(authErr) {
return fmt.Errorf("%v: if pachyderm is stuck in this state, you "+
"can revert by running 'pachctl auth deactivate' or retry by "+
"running 'pachctl auth activate' again", authErr)
}
return fmt.Errorf("error authenticating with Pachyderm cluster: %v",
grpcutil.ScrubGRPC(authErr))
}
return writePachTokenToCfg(resp.PachToken)
}),
}
login.PersistentFlags().BoolVarP(&useOTP, "one-time-password", "o", false,
"If set, authenticate with a Dash-provided One-Time Password, rather than "+
"via GitHub")
return cmdutil.CreateAlias(login, "auth login")
}
// LogoutCmd returns a cobra.Command that deletes your local Pachyderm
// credential, logging you out of your cluster. Note that this is not necessary
// to do before logging in as another user, but is useful for testing.
func LogoutCmd() *cobra.Command {
logout := &cobra.Command{
Short: "Log out of Pachyderm by deleting your local credential",
Long: "Log out of Pachyderm by deleting your local credential. Note that " +
"it's not necessary to log out before logging in with another account " +
"(simply run 'pachctl auth login' twice) but 'logout' can be useful on " +
"shared workstations.",
Run: cmdutil.Run(func([]string) error {
cfg, err := config.Read()
if err != nil {
return fmt.Errorf("error reading Pachyderm config (for cluster "+
"address): %v", err)
}
_, context, err := cfg.ActiveContext()
if err != nil {
return fmt.Errorf("error getting the active context: %v", err)
}
context.SessionToken = ""
return cfg.Write()
}),
}
return cmdutil.CreateAlias(logout, "auth logout")
}
// WhoamiCmd returns a cobra.Command that deletes your local Pachyderm
// credential, logging you out of your cluster. Note that this is not necessary
// to do before logging in as another user, but is useful for testing.
func WhoamiCmd() *cobra.Command {
whoami := &cobra.Command{
Short: "Print your Pachyderm identity",
Long: "Print your Pachyderm identity.",
Run: cmdutil.Run(func([]string) error {
c, err := client.NewOnUserMachine("user")
if err != nil {
return fmt.Errorf("could not connect: %v", err)
}
defer c.Close()
resp, err := c.WhoAmI(c.Ctx(), &auth.WhoAmIRequest{})
if err != nil {
return fmt.Errorf("error: %v", grpcutil.ScrubGRPC(err))
}
fmt.Printf("You are \"%s\"\n", resp.Username)
if resp.TTL > 0 {
fmt.Printf("session expires: %v\n", time.Now().Add(time.Duration(resp.TTL)*time.Second).Format(time.RFC822))
}
if resp.IsAdmin {
fmt.Println("You are an administrator of this Pachyderm cluster")
}
return nil
}),
}
return cmdutil.CreateAlias(whoami, "auth whoami")
}
// CheckCmd returns a cobra command that sends an "Authorize" RPC to Pachd, to
// determine whether the specified user has access to the specified repo.
func CheckCmd() *cobra.Command {
check := &cobra.Command{
Use: "{{alias}} (none|reader|writer|owner) <repo>",
Short: "Check whether you have reader/writer/etc-level access to 'repo'",
Long: "Check whether you have reader/writer/etc-level access to 'repo'. " +
"For example, 'pachctl auth check reader private-data' prints \"true\" " +
"if the you have at least \"reader\" access to the repo " +
"\"private-data\" (you could be a reader, writer, or owner). Unlike " +
"`pachctl auth get`, you do not need to have access to 'repo' to " +
"discover your own access level.",
Run: cmdutil.RunFixedArgs(2, func(args []string) error {
scope, err := auth.ParseScope(args[0])
if err != nil {
return err
}
repo := args[1]
c, err := client.NewOnUserMachine("user")
if err != nil {
return fmt.Errorf("could not connect: %v", err)
}
defer c.Close()
resp, err := c.Authorize(c.Ctx(), &auth.AuthorizeRequest{
Repo: repo,
Scope: scope,
})
if err != nil {
return grpcutil.ScrubGRPC(err)
}
fmt.Printf("%t\n", resp.Authorized)
return nil
}),
}
return cmdutil.CreateAlias(check, "auth check")
}
// GetCmd returns a cobra command that gets either the ACL for a Pachyderm
// repo or another user's scope of access to that repo
func GetCmd() *cobra.Command {
get := &cobra.Command{
Use: "{{alias}} [<username>] <repo>",
Short: "Get the ACL for 'repo' or the access that 'username' has to 'repo'",
Long: "Get the ACL for 'repo' or the access that 'username' has to " +
"'repo'. For example, 'pachctl auth get github-alice private-data' " +
"prints \"reader\", \"writer\", \"owner\", or \"none\", depending on " +
"the privileges that \"github-alice\" has in \"repo\". Currently all " +
"Pachyderm authentication uses GitHub OAuth, so 'username' must be a " +
"GitHub username",
Run: cmdutil.RunBoundedArgs(1, 2, func(args []string) error {
c, err := client.NewOnUserMachine("user")
if err != nil {
return fmt.Errorf("could not connect: %v", err)
}
defer c.Close()
if len(args) == 1 {
// Get ACL for a repo
repo := args[0]
resp, err := c.GetACL(c.Ctx(), &auth.GetACLRequest{
Repo: repo,
})
if err != nil {
return grpcutil.ScrubGRPC(err)
}
t := template.Must(template.New("ACLEntries").Parse(
"{{range .}}{{.Username }}: {{.Scope}}\n{{end}}"))
return t.Execute(os.Stdout, resp.Entries)
}
// Get User's scope on an acl
username, repo := args[0], args[1]
resp, err := c.GetScope(c.Ctx(), &auth.GetScopeRequest{
Repos: []string{repo},
Username: username,
})
if err != nil {
return grpcutil.ScrubGRPC(err)
}
fmt.Println(resp.Scopes[0].String())
return nil
}),
}
return cmdutil.CreateAlias(get, "auth get")
}
// SetScopeCmd returns a cobra command that lets a user set the level of access
// that another user has to a repo
func SetScopeCmd() *cobra.Command {
setScope := &cobra.Command{
Use: "{{alias}} <username> (none|reader|writer|owner) <repo>",
Short: "Set the scope of access that 'username' has to 'repo'",
Long: "Set the scope of access that 'username' has to 'repo'. For " +
"example, 'pachctl auth set github-alice none private-data' prevents " +
"\"github-alice\" from interacting with the \"private-data\" repo in any " +
"way (the default). Similarly, 'pachctl auth set github-alice reader " +
"private-data' would let \"github-alice\" read from \"private-data\" but " +
"not create commits (writer) or modify the repo's access permissions " +
"(owner). Currently all Pachyderm authentication uses GitHub OAuth, so " +
"'username' must be a GitHub username",
Run: cmdutil.RunFixedArgs(3, func(args []string) error {
scope, err := auth.ParseScope(args[1])
if err != nil {
return err
}
username, repo := args[0], args[2]
c, err := client.NewOnUserMachine("user")
if err != nil {
return fmt.Errorf("could not connect: %v", err)
}
defer c.Close()
_, err = c.SetScope(c.Ctx(), &auth.SetScopeRequest{
Repo: repo,
Scope: scope,
Username: username,
})
return grpcutil.ScrubGRPC(err)
}),
}
return cmdutil.CreateAlias(setScope, "auth set")
}
// ListAdminsCmd returns a cobra command that lists the current cluster admins
func ListAdminsCmd() *cobra.Command {
listAdmins := &cobra.Command{
Short: "List the current cluster admins",
Long: "List the current cluster admins",
Run: cmdutil.Run(func([]string) error {
c, err := client.NewOnUserMachine("user")
if err != nil {
return err
}
defer c.Close()
resp, err := c.GetAdmins(c.Ctx(), &auth.GetAdminsRequest{})
if err != nil {
return grpcutil.ScrubGRPC(err)
}
for _, user := range resp.Admins {
fmt.Println(user)
}
return nil
}),
}
return cmdutil.CreateAlias(listAdmins, "auth list-admins")
}
// ModifyAdminsCmd returns a cobra command that modifies the set of current
// cluster admins
func ModifyAdminsCmd() *cobra.Command {
var add []string
var remove []string
modifyAdmins := &cobra.Command{
Short: "Modify the current cluster admins",
Long: "Modify the current cluster admins. --add accepts a comma-" +
"separated list of users to grant admin status, and --remove accepts a " +
"comma-separated list of users to revoke admin status",
Run: cmdutil.Run(func([]string) error {
c, err := client.NewOnUserMachine("user")
if err != nil {
return err
}
defer c.Close()
_, err = c.ModifyAdmins(c.Ctx(), &auth.ModifyAdminsRequest{
Add: add,
Remove: remove,
})
if auth.IsErrPartiallyActivated(err) {
return fmt.Errorf("%v: if pachyderm is stuck in this state, you "+
"can revert by running 'pachctl auth deactivate' or retry by "+
"running 'pachctl auth activate' again", err)
}
return grpcutil.ScrubGRPC(err)
}),
}
modifyAdmins.PersistentFlags().StringSliceVar(&add, "add", []string{},
"Comma-separated list of users to grant admin status")
modifyAdmins.PersistentFlags().StringSliceVar(&remove, "remove", []string{},
"Comma-separated list of users revoke admin status")
return cmdutil.CreateAlias(modifyAdmins, "auth modify-admins")
}
// GetAuthTokenCmd returns a cobra command that lets a user get a pachyderm
// token on behalf of themselves or another user
func GetAuthTokenCmd() *cobra.Command {
var quiet bool
getAuthToken := &cobra.Command{
Use: "{{alias}} <username>",
Short: "Get an auth token that authenticates the holder as \"username\"",
Long: "Get an auth token that authenticates the holder as \"username\"; " +
"this can only be called by cluster admins",
Run: cmdutil.RunFixedArgs(1, func(args []string) error {
subject := args[0]
c, err := client.NewOnUserMachine("user")
if err != nil {
return fmt.Errorf("could not connect: %v", err)
}
defer c.Close()
resp, err := c.GetAuthToken(c.Ctx(), &auth.GetAuthTokenRequest{
Subject: subject,
})
if err != nil {
return grpcutil.ScrubGRPC(err)
}
if quiet {
fmt.Println(resp.Token)
} else {
fmt.Printf("New credentials:\n Subject: %s\n Token: %s\n", resp.Subject, resp.Token)
}
return nil
}),
}
getAuthToken.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "if "+
"set, only print the resulting token (if successful). This is useful for "+
"scripting, as the output can be piped to use-auth-token")
return cmdutil.CreateAlias(getAuthToken, "auth get-auth-token")
}
// UseAuthTokenCmd returns a cobra command that lets a user get a pachyderm
// token on behalf of themselves or another user
func UseAuthTokenCmd() *cobra.Command {
useAuthToken := &cobra.Command{
Short: "Read a Pachyderm auth token from stdin, and write it to the " +
"current user's Pachyderm config file",
Long: "Read a Pachyderm auth token from stdin, and write it to the " +
"current user's Pachyderm config file",
Run: cmdutil.RunFixedArgs(0, func(args []string) error {
fmt.Println("Please paste your Pachyderm auth token:")
token, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return fmt.Errorf("error reading token: %v", err)
}
writePachTokenToCfg(strings.TrimSpace(token)) // drop trailing newline
return nil
}),
}
return cmdutil.CreateAlias(useAuthToken, "auth use-auth-token")
}
// GetOneTimePasswordCmd returns a cobra command that lets a user get an OTP.
func GetOneTimePasswordCmd() *cobra.Command {
getOneTimePassword := &cobra.Command{
Use: "{{alias}} <username>",
Short: "Get a one-time password that authenticates the holder as \"username\"",
Long: "Get a one-time password that authenticates the holder as \"username\"",
Run: cmdutil.RunFixedArgs(1, func(args []string) error {
subject := args[0]
c, err := client.NewOnUserMachine("user")
if err != nil {
return fmt.Errorf("could not connect: %v", err)
}
defer c.Close()
resp, err := c.GetOneTimePassword(c.Ctx(), &auth.GetOneTimePasswordRequest{
Subject: subject,
})
if err != nil {
return grpcutil.ScrubGRPC(err)
}
fmt.Println(resp.Code)
return nil
}),
}
return cmdutil.CreateAlias(getOneTimePassword, "auth get-otp")
}
// Cmds returns a list of cobra commands for authenticating and authorizing
// users in an auth-enabled Pachyderm cluster.
func Cmds() []*cobra.Command {
var commands []*cobra.Command
auth := &cobra.Command{
Short: "Auth commands manage access to data in a Pachyderm cluster",
Long: "Auth commands manage access to data in a Pachyderm cluster",
}
commands = append(commands, cmdutil.CreateAlias(auth, "auth"))
commands = append(commands, ActivateCmd())
commands = append(commands, DeactivateCmd())
commands = append(commands, LoginCmd())
commands = append(commands, LogoutCmd())
commands = append(commands, WhoamiCmd())
commands = append(commands, CheckCmd())
commands = append(commands, SetScopeCmd())
commands = append(commands, GetCmd())
commands = append(commands, ListAdminsCmd())
commands = append(commands, ModifyAdminsCmd())
commands = append(commands, GetAuthTokenCmd())
commands = append(commands, UseAuthTokenCmd())
commands = append(commands, GetConfigCmd())
commands = append(commands, SetConfigCmd())
commands = append(commands, GetOneTimePasswordCmd())
return commands
}