mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
cadf1352b4
<!-- If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting. --> Add support for scoped API tokens in CLI This PR adds CLI support for creating and viewing API tokens with scopes and allow lists. It includes: - New `--scope` and `--allow` flags for the `tokens create` command - A new `tokens view` command to display detailed information about a token - Updated table columns in `tokens list` to show scopes and allow list entries - Updated help text and examples These changes enable users to create tokens with limited permissions through the CLI, similar to the existing functionality in the web UI.
213 lines
6.6 KiB
Go
213 lines
6.6 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/coder/coder/v2/cli/clitest"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestTokens(t *testing.T) {
|
|
t.Parallel()
|
|
client := coderdtest.New(t, nil)
|
|
adminUser := coderdtest.CreateFirstUser(t, client)
|
|
|
|
secondUserClient, secondUser := coderdtest.CreateAnotherUser(t, client, adminUser.OrganizationID)
|
|
_, thirdUser := coderdtest.CreateAnotherUser(t, client, adminUser.OrganizationID)
|
|
|
|
ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancelFunc()
|
|
|
|
// helpful empty response
|
|
inv, root := clitest.New(t, "tokens", "ls")
|
|
//nolint:gocritic // This should be run as the owner user.
|
|
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.Contains(t, res, "tokens found")
|
|
|
|
inv, root = clitest.New(t, "tokens", "create", "--name", "token-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)
|
|
id := res[:10]
|
|
|
|
allowWorkspaceID := uuid.New()
|
|
allowSpec := fmt.Sprintf("workspace:%s", allowWorkspaceID.String())
|
|
inv, root = clitest.New(t, "tokens", "create", "--name", "scoped-token", "--scope", string(codersdk.APIKeyScopeWorkspaceRead), "--allow", allowSpec)
|
|
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)
|
|
scopedTokenID := res[:10]
|
|
|
|
// Test creating a token for second user from first user's (admin) session
|
|
inv, root = clitest.New(t, "tokens", "create", "--name", "token-two", "--user", secondUser.ID.String())
|
|
clitest.SetupConfig(t, client, root)
|
|
buf = new(bytes.Buffer)
|
|
inv.Stdout = buf
|
|
err = inv.WithContext(ctx).Run()
|
|
// Test should succeed in creating token for second user
|
|
require.NoError(t, err)
|
|
res = buf.String()
|
|
require.NotEmpty(t, res)
|
|
secondTokenID := res[:10]
|
|
|
|
// Test listing tokens from the first user's (admin) session
|
|
inv, root = clitest.New(t, "tokens", "ls")
|
|
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)
|
|
// Result should only contain the tokens created for the admin user
|
|
require.Contains(t, res, "ID")
|
|
require.Contains(t, res, "EXPIRES AT")
|
|
require.Contains(t, res, "CREATED AT")
|
|
require.Contains(t, res, "LAST USED")
|
|
require.Contains(t, res, id)
|
|
// Result should not contain the token created for the second user
|
|
require.NotContains(t, res, secondTokenID)
|
|
|
|
inv, root = clitest.New(t, "tokens", "view", "scoped-token")
|
|
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.Contains(t, res, string(codersdk.APIKeyScopeWorkspaceRead))
|
|
require.Contains(t, res, allowSpec)
|
|
|
|
// Test listing tokens from the second user's session
|
|
inv, root = clitest.New(t, "tokens", "ls")
|
|
clitest.SetupConfig(t, secondUserClient, root)
|
|
buf = new(bytes.Buffer)
|
|
inv.Stdout = buf
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
res = buf.String()
|
|
require.NotEmpty(t, res)
|
|
require.Contains(t, res, "ID")
|
|
require.Contains(t, res, "EXPIRES AT")
|
|
require.Contains(t, res, "CREATED AT")
|
|
require.Contains(t, res, "LAST USED")
|
|
// Result should contain the token created for the second user
|
|
require.Contains(t, res, secondTokenID)
|
|
|
|
// Test creating a token for third user from second user's (non-admin) session
|
|
inv, root = clitest.New(t, "tokens", "create", "--name", "failed-token", "--user", thirdUser.ID.String())
|
|
clitest.SetupConfig(t, secondUserClient, root)
|
|
buf = new(bytes.Buffer)
|
|
inv.Stdout = buf
|
|
err = inv.WithContext(ctx).Run()
|
|
// User (non-admin) should not be able to create a token for another user
|
|
require.Error(t, err)
|
|
|
|
inv, root = clitest.New(t, "tokens", "create", "--name", "invalid-allow", "--allow", "badvalue")
|
|
clitest.SetupConfig(t, client, root)
|
|
buf = new(bytes.Buffer)
|
|
inv.Stdout = buf
|
|
err = inv.WithContext(ctx).Run()
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "invalid allow_list entry")
|
|
|
|
inv, root = clitest.New(t, "tokens", "ls", "--output=json")
|
|
clitest.SetupConfig(t, client, root)
|
|
buf = new(bytes.Buffer)
|
|
inv.Stdout = buf
|
|
err = inv.WithContext(ctx).Run()
|
|
require.NoError(t, err)
|
|
|
|
var tokens []codersdk.APIKey
|
|
require.NoError(t, json.Unmarshal(buf.Bytes(), &tokens))
|
|
require.Len(t, tokens, 2)
|
|
tokenByName := make(map[string]codersdk.APIKey, len(tokens))
|
|
for _, tk := range tokens {
|
|
tokenByName[tk.TokenName] = tk
|
|
}
|
|
require.Contains(t, tokenByName, "token-one")
|
|
require.Contains(t, tokenByName, "scoped-token")
|
|
scopedToken := tokenByName["scoped-token"]
|
|
require.Contains(t, scopedToken.Scopes, codersdk.APIKeyScopeWorkspaceRead)
|
|
require.Len(t, scopedToken.AllowList, 1)
|
|
require.Equal(t, allowSpec, scopedToken.AllowList[0].String())
|
|
|
|
// Delete by name
|
|
inv, root = clitest.New(t, "tokens", "rm", "token-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)
|
|
require.Contains(t, res, "deleted")
|
|
|
|
// Delete by ID
|
|
inv, root = clitest.New(t, "tokens", "rm", secondTokenID)
|
|
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)
|
|
require.Contains(t, res, "deleted")
|
|
|
|
// Delete scoped token by ID
|
|
inv, root = clitest.New(t, "tokens", "rm", scopedTokenID)
|
|
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)
|
|
require.Contains(t, res, "deleted")
|
|
|
|
// Create third token
|
|
inv, root = clitest.New(t, "tokens", "create", "--name", "token-three")
|
|
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)
|
|
fourthToken := res
|
|
|
|
// Delete by token
|
|
inv, root = clitest.New(t, "tokens", "rm", fourthToken)
|
|
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)
|
|
require.Contains(t, res, "deleted")
|
|
}
|