From 6dc1e16c8847fa23dca7beca0e2f9bb8ac92c6b8 Mon Sep 17 00:00:00 2001 From: Philip-21 Date: Fri, 31 May 2024 09:38:14 +0100 Subject: [PATCH 1/4] first commits Signed-off-by: Philip-21 --- cli/tokens.go | 30 +++++++++++++++++++++--------- codersdk/apikey.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/cli/tokens.go b/cli/tokens.go index 4961ac7e3e9b5..5269fafec7d6c 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -48,6 +48,7 @@ func (r *RootCmd) createToken() *serpent.Command { var ( tokenLifetime time.Duration name string + user string ) client := new(codersdk.Client) cmd := &serpent.Command{ @@ -58,16 +59,27 @@ func (r *RootCmd) createToken() *serpent.Command { r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { - res, err := client.CreateToken(inv.Context(), codersdk.Me, codersdk.CreateTokenRequest{ - Lifetime: tokenLifetime, - TokenName: name, - }) - if err != nil { - return xerrors.Errorf("create tokens: %w", err) + if user != "" { + adminID := codersdk.Me + res, err := client.CreateTokenForUser(inv.Context(), adminID, user, codersdk.CreateTokenRequest{ + Lifetime: tokenLifetime, + TokenName: name, + }) + if err != nil { + return xerrors.Errorf("create tokens for user %s: %w", user, err) + } + _, _ = fmt.Fprintln(inv.Stdout, res.Key) // Print the token to stdout + } else { + // Otherwise, create a token for the current user + res, err := client.CreateToken(inv.Context(), codersdk.Me, codersdk.CreateTokenRequest{ + Lifetime: tokenLifetime, + TokenName: name, + }) + if err != nil { + return xerrors.Errorf("create tokens: %w", err) + } + _, _ = fmt.Fprintln(inv.Stdout, res.Key) // Print the token to stdout } - - _, _ = fmt.Fprintln(inv.Stdout, res.Key) - return nil }, } diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 32c97cf538417..2838e5eb4e0ab 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -78,6 +78,42 @@ func (c *Client) CreateToken(ctx context.Context, userID string, req CreateToken return apiKey, json.NewDecoder(res.Body).Decode(&apiKey) } + +// CreateTokenForUser allows an admin to create a token on behalf of another user. +func (c *Client) CreateTokenForUser(ctx context.Context, adminID, targetUserID string, req CreateTokenRequest) (GenerateAPIKeyResponse, error) { + isAdmin, err := c.isAdmin(ctx, adminID) + if err != nil { + return GenerateAPIKeyResponse{}, fmt.Errorf("admin check failed: %w", err) + } + if !isAdmin { + return GenerateAPIKeyResponse{}, fmt.Errorf("user %s is not an admin", adminID) + } + return c.CreateToken(ctx, targetUserID, req) +} + +// isAdmin is a placeholder function to check if a user is an admin. +func (c *Client) isAdmin(ctx context.Context, userID string) (bool, error) { + res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/keys", userID), nil) + if err != nil { + return false, fmt.Errorf("unable to get user : %s", userID) + } + defer res.Body.Close() + if res.StatusCode > http.StatusCreated { + return false, ReadBodyAsError(res) + } + //extract roles, when the API returns roles from the response, ensuring system admin privileges + var roles []string + if err := json.NewDecoder(res.Body).Decode(&roles); err != nil { + return false, fmt.Errorf("failed to decode roles: %w", err) + } + for _, role := range roles { + if role == "admin" { + return true, nil + } + } + return false, nil +} + // CreateAPIKey generates an API key for the user ID provided. // CreateToken should be used over CreateAPIKey. CreateToken allows better // tracking of the token's usage and allows for custom expiration. From b846135f22b60043c8d6d17932010053a8ccc08e Mon Sep 17 00:00:00 2001 From: Philip-21 Date: Fri, 31 May 2024 23:48:46 +0100 Subject: [PATCH 2/4] adding more conditional statement, adding necessary userID strings Signed-off-by: Philip-21 --- cli/tokens.go | 6 ++++-- codersdk/apikey.go | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/cli/tokens.go b/cli/tokens.go index 5269fafec7d6c..16c90ebed84bc 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -59,9 +59,11 @@ func (r *RootCmd) createToken() *serpent.Command { r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + userID := codersdk.Me if user != "" { + userID = user adminID := codersdk.Me - res, err := client.CreateTokenForUser(inv.Context(), adminID, user, codersdk.CreateTokenRequest{ + res, err := client.CreateTokenForUser(inv.Context(), adminID, userID, codersdk.CreateTokenRequest{ Lifetime: tokenLifetime, TokenName: name, }) @@ -71,7 +73,7 @@ func (r *RootCmd) createToken() *serpent.Command { _, _ = fmt.Fprintln(inv.Stdout, res.Key) // Print the token to stdout } else { // Otherwise, create a token for the current user - res, err := client.CreateToken(inv.Context(), codersdk.Me, codersdk.CreateTokenRequest{ + res, err := client.CreateToken(inv.Context(), userID, codersdk.CreateTokenRequest{ Lifetime: tokenLifetime, TokenName: name, }) diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 2838e5eb4e0ab..3ad7d1dc42976 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -88,6 +88,9 @@ func (c *Client) CreateTokenForUser(ctx context.Context, adminID, targetUserID s if !isAdmin { return GenerateAPIKeyResponse{}, fmt.Errorf("user %s is not an admin", adminID) } + if adminID == targetUserID { + return GenerateAPIKeyResponse{}, fmt.Errorf("admin cannot create a token for themselves") + } return c.CreateToken(ctx, targetUserID, req) } From 778c11690be014ae38250a7f347408b3b76200d9 Mon Sep 17 00:00:00 2001 From: Philip-21 Date: Thu, 6 Jun 2024 07:33:09 +0100 Subject: [PATCH 3/4] adding new flag options Signed-off-by: Philip-21 --- cli/tokens.go | 7 +++++++ pnpm-lock.yaml | 32 +++++++++++++++++++------------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/cli/tokens.go b/cli/tokens.go index 16c90ebed84bc..e61461720196f 100644 --- a/cli/tokens.go +++ b/cli/tokens.go @@ -101,6 +101,13 @@ func (r *RootCmd) createToken() *serpent.Command { Description: "Specify a human-readable name.", Value: serpent.StringOf(&name), }, + { + Flag: "user", + FlagShorthand: "u", + Env: "CODER_TOKEN_USER", + Description: "create a token on behalf of another user", + Value: serpent.StringOf(&name), + }, } return cmd diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e5e4d2584e40f..d04d1cf7a21e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,29 +1,35 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - exec: - specifier: ^0.2.1 - version: 0.2.1 +importers: -devDependencies: - prettier: - specifier: 3.0.0 - version: 3.0.0 + .: + dependencies: + exec: + specifier: ^0.2.1 + version: 0.2.1 + devDependencies: + prettier: + specifier: 3.0.0 + version: 3.0.0 packages: - /exec@0.2.1: + exec@0.2.1: resolution: {integrity: sha512-lE5ZlJgRYh+rmwidatL2AqRA/U9IBoCpKlLriBmnfUIrV/Rj4oLjb63qZ57iBCHWi5j9IjLt5wOWkFYPiTfYAg==} engines: {node: '>= v0.9.1'} deprecated: deprecated in favor of builtin child_process.execFile - dev: false - /prettier@3.0.0: + prettier@3.0.0: resolution: {integrity: sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==} engines: {node: '>=14'} hasBin: true - dev: true + +snapshots: + + exec@0.2.1: {} + + prettier@3.0.0: {} From e576227e4138d3d50a3000f54461b3b9409df890 Mon Sep 17 00:00:00 2001 From: Philip-21 Date: Thu, 6 Jun 2024 07:49:11 +0100 Subject: [PATCH 4/4] updating tests Signed-off-by: Philip-21 --- cli/tokens_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cli/tokens_test.go b/cli/tokens_test.go index fdb062b959a3b..bbb45d9ccda35 100644 --- a/cli/tokens_test.go +++ b/cli/tokens_test.go @@ -42,6 +42,16 @@ func TestTokens(t *testing.T) { require.NotEmpty(t, res) id := res[:10] + inv, root = clitest.New(t, "tokens", "create", "-u", "user-one") + clitest.SetupConfig(t, client, root) + buf = new(bytes.Buffer) + inv.Stdout = buf + err = inv.WithContext(ctx).Run() + require.NoError(t, err) + res = buf.String() + require.NotEmpty(t, res) + + inv, root = clitest.New(t, "tokens", "ls") clitest.SetupConfig(t, client, root) buf = new(bytes.Buffer)