diff --git a/cli/tokens.go b/cli/tokens.go index 4961ac7e3e9b5..e61461720196f 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,29 @@ 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) + userID := codersdk.Me + if user != "" { + userID = user + adminID := codersdk.Me + res, err := client.CreateTokenForUser(inv.Context(), adminID, userID, 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(), userID, 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 }, } @@ -87,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/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) diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 32c97cf538417..3ad7d1dc42976 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -78,6 +78,45 @@ 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) + } + if adminID == targetUserID { + return GenerateAPIKeyResponse{}, fmt.Errorf("admin cannot create a token for themselves") + } + 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. 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: {}
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: