mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add multi-scope support to API keys (#19917)
# Canonicalize API Key Scopes This PR introduces canonical API key scopes with a `coder:` namespace prefix to avoid collisions with low-level resource:action names. It: 1. Renames special API key scopes in the database: - `all` → `coder:all` - `application_connect` → `coder:application_connect` 2. Adds support for a new `scopes` field in the API key creation request, allowing multiple scopes to be specified while maintaining backward compatibility with the singular `scope` field. 3. Updates the API documentation to reflect these changes, including the new endpoint for listing public API key scopes. 4. Ensures backward compatibility by mapping between legacy and canonical scope names in relevant code paths.
This commit is contained in:
@@ -872,7 +872,7 @@ codersdk/rbacresources_gen.go: scripts/typegen/codersdk.gotmpl scripts/typegen/m
|
||||
touch "$@"
|
||||
|
||||
codersdk/apikey_scopes_gen.go: scripts/apikeyscopesgen/main.go coderd/rbac/scopes_catalog.go coderd/rbac/scopes.go
|
||||
# Generate SDK constants for public low-level API key scopes.
|
||||
# Generate SDK constants for external API key scopes.
|
||||
go run ./scripts/apikeyscopesgen > /tmp/apikey_scopes_gen.go
|
||||
mv /tmp/apikey_scopes_gen.go codersdk/apikey_scopes_gen.go
|
||||
touch "$@"
|
||||
|
||||
Generated
+25
-4
@@ -11254,7 +11254,6 @@ const docTemplate = `{
|
||||
"last_used",
|
||||
"lifetime_seconds",
|
||||
"login_type",
|
||||
"scope",
|
||||
"token_name",
|
||||
"updated_at",
|
||||
"user_id"
|
||||
@@ -11292,6 +11291,7 @@ const docTemplate = `{
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"description": "Deprecated: use Scopes instead.",
|
||||
"enum": [
|
||||
"all",
|
||||
"application_connect"
|
||||
@@ -11302,6 +11302,12 @@ const docTemplate = `{
|
||||
}
|
||||
]
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.APIKeyScope"
|
||||
}
|
||||
},
|
||||
"token_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -11319,12 +11325,14 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"all",
|
||||
"application_connect",
|
||||
"api_key:*",
|
||||
"api_key:create",
|
||||
"api_key:delete",
|
||||
"api_key:read",
|
||||
"api_key:update",
|
||||
"application_connect",
|
||||
"coder:all",
|
||||
"coder:application_connect",
|
||||
"file:*",
|
||||
"file:create",
|
||||
"file:read",
|
||||
@@ -11353,12 +11361,14 @@ const docTemplate = `{
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"APIKeyScopeAll",
|
||||
"APIKeyScopeApplicationConnect",
|
||||
"APIKeyScopeApiKeyAll",
|
||||
"APIKeyScopeApiKeyCreate",
|
||||
"APIKeyScopeApiKeyDelete",
|
||||
"APIKeyScopeApiKeyRead",
|
||||
"APIKeyScopeApiKeyUpdate",
|
||||
"APIKeyScopeApplicationConnect",
|
||||
"APIKeyScopeCoderAll",
|
||||
"APIKeyScopeCoderApplicationConnect",
|
||||
"APIKeyScopeFileAll",
|
||||
"APIKeyScopeFileCreate",
|
||||
"APIKeyScopeFileRead",
|
||||
@@ -12453,7 +12463,18 @@ const docTemplate = `{
|
||||
"type": "integer"
|
||||
},
|
||||
"scope": {
|
||||
"$ref": "#/definitions/codersdk.APIKeyScope"
|
||||
"description": "Deprecated: use Scopes instead.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.APIKeyScope"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.APIKeyScope"
|
||||
}
|
||||
},
|
||||
"token_name": {
|
||||
"type": "string"
|
||||
|
||||
Generated
+25
-4
@@ -9982,7 +9982,6 @@
|
||||
"last_used",
|
||||
"lifetime_seconds",
|
||||
"login_type",
|
||||
"scope",
|
||||
"token_name",
|
||||
"updated_at",
|
||||
"user_id"
|
||||
@@ -10015,6 +10014,7 @@
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"description": "Deprecated: use Scopes instead.",
|
||||
"enum": ["all", "application_connect"],
|
||||
"allOf": [
|
||||
{
|
||||
@@ -10022,6 +10022,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.APIKeyScope"
|
||||
}
|
||||
},
|
||||
"token_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -10039,12 +10045,14 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"all",
|
||||
"application_connect",
|
||||
"api_key:*",
|
||||
"api_key:create",
|
||||
"api_key:delete",
|
||||
"api_key:read",
|
||||
"api_key:update",
|
||||
"application_connect",
|
||||
"coder:all",
|
||||
"coder:application_connect",
|
||||
"file:*",
|
||||
"file:create",
|
||||
"file:read",
|
||||
@@ -10073,12 +10081,14 @@
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"APIKeyScopeAll",
|
||||
"APIKeyScopeApplicationConnect",
|
||||
"APIKeyScopeApiKeyAll",
|
||||
"APIKeyScopeApiKeyCreate",
|
||||
"APIKeyScopeApiKeyDelete",
|
||||
"APIKeyScopeApiKeyRead",
|
||||
"APIKeyScopeApiKeyUpdate",
|
||||
"APIKeyScopeApplicationConnect",
|
||||
"APIKeyScopeCoderAll",
|
||||
"APIKeyScopeCoderApplicationConnect",
|
||||
"APIKeyScopeFileAll",
|
||||
"APIKeyScopeFileCreate",
|
||||
"APIKeyScopeFileRead",
|
||||
@@ -11114,7 +11124,18 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"scope": {
|
||||
"$ref": "#/definitions/codersdk.APIKeyScope"
|
||||
"description": "Deprecated: use Scopes instead.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/codersdk.APIKeyScope"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.APIKeyScope"
|
||||
}
|
||||
},
|
||||
"token_name": {
|
||||
"type": "string"
|
||||
|
||||
+26
-5
@@ -67,18 +67,39 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Map and validate requested scope.
|
||||
// Accept special scopes (all, application_connect) and curated public low-level scopes.
|
||||
scopes := database.APIKeyScopes{database.APIKeyScopeAll}
|
||||
if createToken.Scope != "" {
|
||||
// Accept legacy special scopes (all, application_connect) and external scopes.
|
||||
// Default to coder:all scopes for backward compatibility.
|
||||
scopes := database.APIKeyScopes{database.ApiKeyScopeCoderAll}
|
||||
if len(createToken.Scopes) > 0 {
|
||||
scopes = make(database.APIKeyScopes, 0, len(createToken.Scopes))
|
||||
for _, s := range createToken.Scopes {
|
||||
name := string(s)
|
||||
if !rbac.IsExternalScope(rbac.ScopeName(name)) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to create API key.",
|
||||
Detail: fmt.Sprintf("invalid or unsupported API key scope: %q", name),
|
||||
})
|
||||
return
|
||||
}
|
||||
scopes = append(scopes, database.APIKeyScope(name))
|
||||
}
|
||||
} else if string(createToken.Scope) != "" {
|
||||
name := string(createToken.Scope)
|
||||
if !rbac.IsExternalScope(rbac.ScopeName(name)) {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Failed to create API key.",
|
||||
Detail: fmt.Sprintf("invalid API key scope: %q", name),
|
||||
Detail: fmt.Sprintf("invalid or unsupported API key scope: %q", name),
|
||||
})
|
||||
return
|
||||
}
|
||||
scopes = database.APIKeyScopes{database.APIKeyScope(name)}
|
||||
switch name {
|
||||
case "all":
|
||||
scopes = database.APIKeyScopes{database.ApiKeyScopeCoderAll}
|
||||
case "application_connect":
|
||||
scopes = database.APIKeyScopes{database.ApiKeyScopeCoderApplicationConnect}
|
||||
default:
|
||||
scopes = database.APIKeyScopes{database.APIKeyScope(name)}
|
||||
}
|
||||
}
|
||||
|
||||
tokenName := namesgenerator.GetRandomName(1)
|
||||
|
||||
+14
-5
@@ -25,13 +25,12 @@ type CreateParams struct {
|
||||
// Optional.
|
||||
ExpiresAt time.Time
|
||||
LifetimeSeconds int64
|
||||
|
||||
// Scope is legacy single-scope input kept for backward compatibility.
|
||||
//
|
||||
// Deprecated: Prefer Scopes for new code.
|
||||
// Deprecated: use Scopes instead.
|
||||
Scope database.APIKeyScope
|
||||
// Scopes is the full list of scopes to attach to the key.
|
||||
// If empty and Scope is set, the generator will use [Scope].
|
||||
// If both are empty, the generator will default to [APIKeyScopeAll].
|
||||
Scopes database.APIKeyScopes
|
||||
TokenName string
|
||||
RemoteAddr string
|
||||
@@ -74,9 +73,19 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
|
||||
case len(params.Scopes) > 0:
|
||||
scopes = params.Scopes
|
||||
case params.Scope != "":
|
||||
scopes = database.APIKeyScopes{params.Scope}
|
||||
var scope database.APIKeyScope
|
||||
switch params.Scope {
|
||||
case "all":
|
||||
scope = database.ApiKeyScopeCoderAll
|
||||
case "application_connect":
|
||||
scope = database.ApiKeyScopeCoderApplicationConnect
|
||||
default:
|
||||
scope = params.Scope
|
||||
}
|
||||
scopes = database.APIKeyScopes{scope}
|
||||
default:
|
||||
scopes = database.APIKeyScopes{database.APIKeyScopeAll}
|
||||
// Default to coder:all scope for backward compatibility.
|
||||
scopes = database.APIKeyScopes{database.ApiKeyScopeCoderAll}
|
||||
}
|
||||
|
||||
for _, s := range scopes {
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestGenerate(t *testing.T) {
|
||||
LifetimeSeconds: int64(time.Hour.Seconds()),
|
||||
TokenName: "hello",
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -62,7 +62,7 @@ func TestGenerate(t *testing.T) {
|
||||
ExpiresAt: time.Time{},
|
||||
TokenName: "hello",
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -75,7 +75,7 @@ func TestGenerate(t *testing.T) {
|
||||
ExpiresAt: time.Time{},
|
||||
TokenName: "hello",
|
||||
RemoteAddr: "1.2.3.4",
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -88,7 +88,7 @@ func TestGenerate(t *testing.T) {
|
||||
LifetimeSeconds: int64(time.Hour.Seconds()),
|
||||
TokenName: "hello",
|
||||
RemoteAddr: "",
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -161,7 +161,7 @@ func TestGenerate(t *testing.T) {
|
||||
if tc.params.Scope != "" {
|
||||
assert.True(t, key.Scopes.Has(tc.params.Scope))
|
||||
} else {
|
||||
assert.True(t, key.Scopes.Has(database.APIKeyScopeAll))
|
||||
assert.True(t, key.Scopes.Has(database.ApiKeyScopeCoderAll))
|
||||
}
|
||||
|
||||
if tc.params.TokenName != "" {
|
||||
|
||||
@@ -22,7 +22,8 @@ func TestTokenCreation_ScopeValidation(t *testing.T) {
|
||||
{name: "AllowsPublicLowLevelScope", scope: "workspace:read", wantErr: false},
|
||||
{name: "RejectsInternalOnlyScope", scope: "debug_info:read", wantErr: true},
|
||||
{name: "AllowsLegacyScopes", scope: "application_connect", wantErr: false},
|
||||
{name: "AllowsCanonicalSpecialScope", scope: "all", wantErr: false},
|
||||
{name: "AllowsLegacyScopes2", scope: "all", wantErr: false},
|
||||
{name: "AllowsCanonicalSpecialScope", scope: "coder:all", wantErr: false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -42,6 +43,22 @@ func TestTokenCreation_ScopeValidation(t *testing.T) {
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, resp.Key)
|
||||
|
||||
// Fetch and verify the stored scopes match expectation.
|
||||
keys, err := client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, 1)
|
||||
|
||||
// Normalize legacy singular scopes to canonical coder:* values.
|
||||
expected := tc.scope
|
||||
switch tc.scope {
|
||||
case codersdk.APIKeyScopeAll:
|
||||
expected = codersdk.APIKeyScopeCoderAll
|
||||
case codersdk.APIKeyScopeApplicationConnect:
|
||||
expected = codersdk.APIKeyScopeCoderApplicationConnect
|
||||
}
|
||||
|
||||
require.Contains(t, keys[0].Scopes, expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,54 @@ func TestTokenScoped(t *testing.T) {
|
||||
require.Equal(t, keys[0].Scope, codersdk.APIKeyScopeApplicationConnect)
|
||||
}
|
||||
|
||||
// Ensure backward-compat: when a token is created using the legacy singular
|
||||
// scope names ("all" or "application_connect"), the API returns the same
|
||||
// legacy value in the deprecated singular Scope field while also supporting
|
||||
// the new multi-scope field.
|
||||
func TestTokenLegacySingularScopeCompat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
scope codersdk.APIKeyScope
|
||||
scopes []codersdk.APIKeyScope
|
||||
}{
|
||||
{
|
||||
name: "all",
|
||||
scope: codersdk.APIKeyScopeAll,
|
||||
scopes: []codersdk.APIKeyScope{codersdk.APIKeyScopeCoderAll},
|
||||
},
|
||||
{
|
||||
name: "application_connect",
|
||||
scope: codersdk.APIKeyScopeApplicationConnect,
|
||||
scopes: []codersdk.APIKeyScope{codersdk.APIKeyScopeCoderApplicationConnect},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(t.Context(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
client := coderdtest.New(t, nil)
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
||||
// Create with legacy singular scope.
|
||||
_, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{
|
||||
Scope: tc.scope,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Read back and ensure the deprecated singular field matches exactly.
|
||||
keys, err := client.Tokens(ctx, codersdk.Me, codersdk.TokensFilter{})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, keys, 1)
|
||||
require.Equal(t, tc.scope, keys[0].Scope)
|
||||
require.ElementsMatch(t, keys[0].Scopes, tc.scopes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserSetTokenDuration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ func (s *MethodTestSuite) TestAPIKey() {
|
||||
}))
|
||||
s.Run("InsertAPIKey", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
arg := database.InsertAPIKeyParams{UserID: u.ID, LoginType: database.LoginTypePassword, Scopes: database.APIKeyScopes{database.APIKeyScopeAll}, IPAddress: defaultIPAddress()}
|
||||
arg := database.InsertAPIKeyParams{UserID: u.ID, LoginType: database.LoginTypePassword, Scopes: database.APIKeyScopes{database.ApiKeyScopeCoderAll}, IPAddress: defaultIPAddress()}
|
||||
ret := testutil.Fake(s.T(), faker, database.APIKey{UserID: u.ID, LoginType: database.LoginTypePassword})
|
||||
dbm.EXPECT().InsertAPIKey(gomock.Any(), arg).Return(ret, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceApiKey.WithOwner(u.ID.String()), policy.ActionCreate)
|
||||
@@ -265,7 +265,7 @@ func (s *MethodTestSuite) TestAPIKey() {
|
||||
check.Args(arg).Asserts(a, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("DeleteApplicationConnectAPIKeysByUserID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
a := testutil.Fake(s.T(), faker, database.APIKey{Scopes: database.APIKeyScopes{database.APIKeyScopeApplicationConnect}})
|
||||
a := testutil.Fake(s.T(), faker, database.APIKey{Scopes: database.APIKeyScopes{database.ApiKeyScopeCoderApplicationConnect}})
|
||||
dbm.EXPECT().DeleteApplicationConnectAPIKeysByUserID(gomock.Any(), a.UserID).Return(nil).AnyTimes()
|
||||
check.Args(a.UserID).Asserts(rbac.ResourceApiKey.WithOwner(a.UserID.String()), policy.ActionDelete).Returns()
|
||||
}))
|
||||
|
||||
@@ -185,7 +185,7 @@ func APIKey(t testing.TB, db database.Store, seed database.APIKey, munge ...func
|
||||
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
|
||||
UpdatedAt: takeFirst(seed.UpdatedAt, dbtime.Now()),
|
||||
LoginType: takeFirst(seed.LoginType, database.LoginTypePassword),
|
||||
Scopes: takeFirstSlice([]database.APIKeyScope(seed.Scopes), []database.APIKeyScope{database.APIKeyScopeAll}),
|
||||
Scopes: takeFirstSlice([]database.APIKeyScope(seed.Scopes), []database.APIKeyScope{database.ApiKeyScopeCoderAll}),
|
||||
AllowList: takeFirstSlice(seed.AllowList, database.AllowList{database.AllowListWildcard()}),
|
||||
TokenName: takeFirst(seed.TokenName),
|
||||
}
|
||||
|
||||
Generated
+2
-2
@@ -11,8 +11,8 @@ CREATE TYPE agent_key_scope_enum AS ENUM (
|
||||
);
|
||||
|
||||
CREATE TYPE api_key_scope AS ENUM (
|
||||
'all',
|
||||
'application_connect',
|
||||
'coder:all',
|
||||
'coder:application_connect',
|
||||
'aibridge_interception:create',
|
||||
'aibridge_interception:read',
|
||||
'aibridge_interception:update',
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Revert canonicalization of special API key scopes
|
||||
-- Rename enum values back: 'coder:all' -> 'all', 'coder:application_connect' -> 'application_connect'
|
||||
|
||||
ALTER TYPE api_key_scope RENAME VALUE 'coder:all' TO 'all';
|
||||
ALTER TYPE api_key_scope RENAME VALUE 'coder:application_connect' TO 'application_connect';
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Canonicalize special API key scopes to coder:* namespace
|
||||
-- Rename enum values: 'all' -> 'coder:all', 'application_connect' -> 'coder:application_connect'
|
||||
|
||||
ALTER TYPE api_key_scope RENAME VALUE 'all' TO 'coder:all';
|
||||
ALTER TYPE api_key_scope RENAME VALUE 'application_connect' TO 'coder:application_connect';
|
||||
@@ -134,9 +134,9 @@ func (w ConnectionLog) RBACObject() rbac.Object {
|
||||
|
||||
func (s APIKeyScope) ToRBAC() rbac.ScopeName {
|
||||
switch s {
|
||||
case APIKeyScopeAll:
|
||||
case ApiKeyScopeCoderAll:
|
||||
return rbac.ScopeAll
|
||||
case APIKeyScopeApplicationConnect:
|
||||
case ApiKeyScopeCoderApplicationConnect:
|
||||
return rbac.ScopeApplicationConnect
|
||||
default:
|
||||
// Allow low-level resource:action scopes to flow through to RBAC for
|
||||
@@ -218,7 +218,8 @@ func (s APIKeyScopes) Expand() (rbac.Scope, error) {
|
||||
// Name returns a human-friendly identifier for tracing/logging.
|
||||
func (s APIKeyScopes) Name() rbac.RoleIdentifier {
|
||||
if len(s) == 0 {
|
||||
return rbac.RoleIdentifier{Name: string(APIKeyScopeAll)}
|
||||
// Return all for backward compatibility.
|
||||
return rbac.RoleIdentifier{Name: string(ApiKeyScopeCoderAll)}
|
||||
}
|
||||
names := make([]string, 0, len(s))
|
||||
for _, s := range s {
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestAPIKeyScopesExpand(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "all",
|
||||
scopes: APIKeyScopes{APIKeyScopeAll},
|
||||
scopes: APIKeyScopes{ApiKeyScopeCoderAll},
|
||||
want: func(t *testing.T, s rbac.Scope) {
|
||||
requirePermission(t, s, rbac.ResourceWildcard.Type, policy.Action(policy.WildcardSymbol))
|
||||
requireAllowAll(t, s)
|
||||
@@ -28,7 +28,7 @@ func TestAPIKeyScopesExpand(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "application_connect",
|
||||
scopes: APIKeyScopes{APIKeyScopeApplicationConnect},
|
||||
scopes: APIKeyScopes{ApiKeyScopeCoderApplicationConnect},
|
||||
want: func(t *testing.T, s rbac.Scope) {
|
||||
requirePermission(t, s, rbac.ResourceWorkspace.Type, policy.ActionApplicationConnect)
|
||||
requireAllowAll(t, s)
|
||||
@@ -69,7 +69,7 @@ func TestAPIKeyScopesExpand(t *testing.T) {
|
||||
|
||||
t.Run("merge", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
scopes := APIKeyScopes{APIKeyScopeApplicationConnect, APIKeyScopeAll, ApiKeyScopeWorkspaceRead}
|
||||
scopes := APIKeyScopes{ApiKeyScopeCoderApplicationConnect, ApiKeyScopeCoderAll, ApiKeyScopeWorkspaceRead}
|
||||
s, err := scopes.Expand()
|
||||
require.NoError(t, err)
|
||||
requirePermission(t, s, rbac.ResourceWildcard.Type, policy.Action(policy.WildcardSymbol))
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
type APIKeyScope string
|
||||
|
||||
const (
|
||||
APIKeyScopeAll APIKeyScope = "all"
|
||||
APIKeyScopeApplicationConnect APIKeyScope = "application_connect"
|
||||
ApiKeyScopeCoderAll APIKeyScope = "coder:all"
|
||||
ApiKeyScopeCoderApplicationConnect APIKeyScope = "coder:application_connect"
|
||||
ApiKeyScopeAibridgeInterceptionCreate APIKeyScope = "aibridge_interception:create"
|
||||
ApiKeyScopeAibridgeInterceptionRead APIKeyScope = "aibridge_interception:read"
|
||||
ApiKeyScopeAibridgeInterceptionUpdate APIKeyScope = "aibridge_interception:update"
|
||||
@@ -198,8 +198,8 @@ func (ns NullAPIKeyScope) Value() (driver.Value, error) {
|
||||
|
||||
func (e APIKeyScope) Valid() bool {
|
||||
switch e {
|
||||
case APIKeyScopeAll,
|
||||
APIKeyScopeApplicationConnect,
|
||||
case ApiKeyScopeCoderAll,
|
||||
ApiKeyScopeCoderApplicationConnect,
|
||||
ApiKeyScopeAibridgeInterceptionCreate,
|
||||
ApiKeyScopeAibridgeInterceptionRead,
|
||||
ApiKeyScopeAibridgeInterceptionUpdate,
|
||||
@@ -345,8 +345,8 @@ func (e APIKeyScope) Valid() bool {
|
||||
|
||||
func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
return []APIKeyScope{
|
||||
APIKeyScopeAll,
|
||||
APIKeyScopeApplicationConnect,
|
||||
ApiKeyScopeCoderAll,
|
||||
ApiKeyScopeCoderApplicationConnect,
|
||||
ApiKeyScopeAibridgeInterceptionCreate,
|
||||
ApiKeyScopeAibridgeInterceptionRead,
|
||||
ApiKeyScopeAibridgeInterceptionUpdate,
|
||||
|
||||
@@ -432,7 +432,7 @@ DELETE FROM
|
||||
api_keys
|
||||
WHERE
|
||||
user_id = $1 AND
|
||||
'application_connect'::api_key_scope = ANY(scopes)
|
||||
'coder:application_connect'::api_key_scope = ANY(scopes)
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error {
|
||||
|
||||
@@ -77,7 +77,7 @@ DELETE FROM
|
||||
api_keys
|
||||
WHERE
|
||||
user_id = $1 AND
|
||||
'application_connect'::api_key_scope = ANY(scopes);
|
||||
'coder:application_connect'::api_key_scope = ANY(scopes);
|
||||
|
||||
-- name: DeleteAPIKeysByUserID :exec
|
||||
DELETE FROM
|
||||
|
||||
@@ -313,7 +313,7 @@ func TestAPIKey(t *testing.T) {
|
||||
_, token = dbgen.APIKey(t, db, database.APIKey{
|
||||
UserID: user.ID,
|
||||
ExpiresAt: dbtime.Now().AddDate(0, 0, 1),
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeApplicationConnect},
|
||||
Scopes: database.APIKeyScopes{database.ApiKeyScopeCoderApplicationConnect},
|
||||
})
|
||||
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
@@ -330,7 +330,7 @@ func TestAPIKey(t *testing.T) {
|
||||
})(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
// Checks that it exists on the context!
|
||||
apiKey := httpmw.APIKey(r)
|
||||
assert.Equal(t, database.APIKeyScopeApplicationConnect, apiKey.Scopes[0])
|
||||
assert.Equal(t, database.ApiKeyScopeCoderApplicationConnect, apiKey.Scopes[0])
|
||||
assertActorOk(t, r)
|
||||
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.Response{
|
||||
|
||||
@@ -172,7 +172,7 @@ func addUser(t *testing.T, db database.Store, roles ...string) (database.User, s
|
||||
LastUsed: dbtime.Now(),
|
||||
ExpiresAt: dbtime.Now().Add(time.Minute),
|
||||
LoginType: database.LoginTypePassword,
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeAll},
|
||||
Scopes: database.APIKeyScopes{database.ApiKeyScopeCoderAll},
|
||||
AllowList: database.AllowList{database.AllowListWildcard()},
|
||||
IPAddress: pqtype.Inet{
|
||||
IPNet: net.IPNet{
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestWorkspaceParam(t *testing.T) {
|
||||
LastUsed: dbtime.Now(),
|
||||
ExpiresAt: dbtime.Now().Add(time.Minute),
|
||||
LoginType: database.LoginTypePassword,
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeAll},
|
||||
Scopes: database.APIKeyScopes{database.ApiKeyScopeCoderAll},
|
||||
AllowList: database.AllowList{database.AllowListWildcard()},
|
||||
IPAddress: pqtype.Inet{
|
||||
IPNet: net.IPNet{
|
||||
|
||||
@@ -61,8 +61,8 @@ func WorkspaceAgentScope(params WorkspaceAgentScopeParams) Scope {
|
||||
}
|
||||
|
||||
const (
|
||||
ScopeAll ScopeName = "all"
|
||||
ScopeApplicationConnect ScopeName = "application_connect"
|
||||
ScopeAll ScopeName = "coder:all"
|
||||
ScopeApplicationConnect ScopeName = "coder:application_connect"
|
||||
ScopeNoUserData ScopeName = "no_user_data"
|
||||
)
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ var externalLowLevel = map[ScopeName]struct{}{
|
||||
// low-level resource:action scopes.
|
||||
func IsExternalScope(name ScopeName) bool {
|
||||
switch name {
|
||||
case ScopeAll, ScopeApplicationConnect:
|
||||
// Include `all` and `application_connect` for backward compatibility.
|
||||
case "all", ScopeAll, "application_connect", ScopeApplicationConnect:
|
||||
return true
|
||||
}
|
||||
if _, ok := externalLowLevel[name]; ok {
|
||||
|
||||
@@ -153,8 +153,8 @@ const (
|
||||
// ensure changes in rbac/scopes.go remain in sync here.
|
||||
func (e ScopeName) Valid() bool {
|
||||
switch e {
|
||||
case ScopeName("all"),
|
||||
ScopeName("application_connect"),
|
||||
case ScopeName("coder:all"),
|
||||
ScopeName("coder:application_connect"),
|
||||
ScopeName("no_user_data"),
|
||||
ScopeAibridgeInterceptionCreate,
|
||||
ScopeAibridgeInterceptionRead,
|
||||
@@ -303,8 +303,8 @@ func (e ScopeName) Valid() bool {
|
||||
// including builtin and generated low-level scopes.
|
||||
func AllScopeNameValues() []ScopeName {
|
||||
return []ScopeName{
|
||||
ScopeName("all"),
|
||||
ScopeName("application_connect"),
|
||||
ScopeName("coder:all"),
|
||||
ScopeName("coder:application_connect"),
|
||||
ScopeName("no_user_data"),
|
||||
ScopeAibridgeInterceptionCreate,
|
||||
ScopeAibridgeInterceptionRead,
|
||||
|
||||
@@ -1958,7 +1958,7 @@ func TestUserLogout(t *testing.T) {
|
||||
for i := range 3 {
|
||||
key, _ := dbgen.APIKey(t, db, database.APIKey{
|
||||
UserID: newUser.ID,
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeApplicationConnect},
|
||||
Scopes: database.APIKeyScopes{database.ApiKeyScopeCoderApplicationConnect},
|
||||
})
|
||||
shouldBeDeleted[fmt.Sprintf("application_connect key owned by logout user %d", i)] = key.ID
|
||||
}
|
||||
@@ -1968,7 +1968,7 @@ func TestUserLogout(t *testing.T) {
|
||||
for i := range 3 {
|
||||
key, _ := dbgen.APIKey(t, db, database.APIKey{
|
||||
UserID: firstUser.UserID,
|
||||
Scopes: database.APIKeyScopes{database.APIKeyScopeApplicationConnect},
|
||||
Scopes: database.APIKeyScopes{database.ApiKeyScopeCoderApplicationConnect},
|
||||
})
|
||||
shouldNotBeDeleted[fmt.Sprintf("application_connect key owned by admin user %d", i)] = key.ID
|
||||
}
|
||||
|
||||
+17
-5
@@ -1570,10 +1570,21 @@ func userOrganizationIDs(ctx context.Context, api *API, user database.User) ([]u
|
||||
}
|
||||
|
||||
func convertAPIKey(k database.APIKey) codersdk.APIKey {
|
||||
// Derive a single scope from arrays for response compatibility.
|
||||
scope := database.APIKeyScopeAll
|
||||
if k.Scopes.Has(database.APIKeyScopeApplicationConnect) {
|
||||
scope = database.APIKeyScopeApplicationConnect
|
||||
// Derive a single legacy scope name for response compatibility.
|
||||
// Historically, the API exposed only two scope strings: "all" and
|
||||
// "application_connect". Continue to return those for clients even
|
||||
// though the database stores canonical values (e.g. "coder:all")
|
||||
// and may include low-level scopes.
|
||||
var legacyScope codersdk.APIKeyScope
|
||||
if k.Scopes.Has(database.ApiKeyScopeCoderApplicationConnect) {
|
||||
legacyScope = codersdk.APIKeyScopeApplicationConnect
|
||||
} else if k.Scopes.Has(database.ApiKeyScopeCoderAll) {
|
||||
legacyScope = codersdk.APIKeyScopeAll
|
||||
}
|
||||
|
||||
scopes := make([]codersdk.APIKeyScope, 0, len(k.Scopes))
|
||||
for _, s := range k.Scopes {
|
||||
scopes = append(scopes, codersdk.APIKeyScope(s))
|
||||
}
|
||||
|
||||
return codersdk.APIKey{
|
||||
@@ -1584,7 +1595,8 @@ func convertAPIKey(k database.APIKey) codersdk.APIKey {
|
||||
CreatedAt: k.CreatedAt,
|
||||
UpdatedAt: k.UpdatedAt,
|
||||
LoginType: codersdk.LoginType(k.LoginType),
|
||||
Scope: codersdk.APIKeyScope(scope),
|
||||
Scope: legacyScope,
|
||||
Scopes: scopes,
|
||||
LifetimeSeconds: k.LifetimeSeconds,
|
||||
TokenName: k.TokenName,
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request
|
||||
DefaultLifetime: api.DeploymentValues.Sessions.DefaultDuration.Value(),
|
||||
ExpiresAt: exp,
|
||||
LifetimeSeconds: lifetimeSeconds,
|
||||
Scope: database.APIKeyScopeApplicationConnect,
|
||||
Scope: database.ApiKeyScopeCoderApplicationConnect,
|
||||
})
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
|
||||
+13
-11
@@ -12,16 +12,17 @@ import (
|
||||
|
||||
// APIKey: do not ever return the HashedSecret
|
||||
type APIKey struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
UserID uuid.UUID `json:"user_id" validate:"required" format:"uuid"`
|
||||
LastUsed time.Time `json:"last_used" validate:"required" format:"date-time"`
|
||||
ExpiresAt time.Time `json:"expires_at" validate:"required" format:"date-time"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"`
|
||||
LoginType LoginType `json:"login_type" validate:"required" enums:"password,github,oidc,token"`
|
||||
Scope APIKeyScope `json:"scope" validate:"required" enums:"all,application_connect"`
|
||||
TokenName string `json:"token_name" validate:"required"`
|
||||
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
UserID uuid.UUID `json:"user_id" validate:"required" format:"uuid"`
|
||||
LastUsed time.Time `json:"last_used" validate:"required" format:"date-time"`
|
||||
ExpiresAt time.Time `json:"expires_at" validate:"required" format:"date-time"`
|
||||
CreatedAt time.Time `json:"created_at" validate:"required" format:"date-time"`
|
||||
UpdatedAt time.Time `json:"updated_at" validate:"required" format:"date-time"`
|
||||
LoginType LoginType `json:"login_type" validate:"required" enums:"password,github,oidc,token"`
|
||||
Scope APIKeyScope `json:"scope" enums:"all,application_connect"` // Deprecated: use Scopes instead.
|
||||
Scopes []APIKeyScope `json:"scopes"`
|
||||
TokenName string `json:"token_name" validate:"required"`
|
||||
LifetimeSeconds int64 `json:"lifetime_seconds" validate:"required"`
|
||||
}
|
||||
|
||||
// LoginType is the type of login used to create the API key.
|
||||
@@ -44,7 +45,8 @@ type APIKeyScope string
|
||||
|
||||
type CreateTokenRequest struct {
|
||||
Lifetime time.Duration `json:"lifetime"`
|
||||
Scope APIKeyScope `json:"scope"`
|
||||
Scope APIKeyScope `json:"scope,omitempty"` // Deprecated: use Scopes instead.
|
||||
Scopes []APIKeyScope `json:"scopes,omitempty"`
|
||||
TokenName string `json:"token_name"`
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
package codersdk
|
||||
|
||||
const (
|
||||
APIKeyScopeAll APIKeyScope = "all"
|
||||
// Deprecated: use codersdk.APIKeyScopeCoderAll instead.
|
||||
APIKeyScopeAll APIKeyScope = "all"
|
||||
// Deprecated: use codersdk.APIKeyScopeCoderApplicationConnect instead.
|
||||
APIKeyScopeApplicationConnect APIKeyScope = "application_connect"
|
||||
APIKeyScopeApiKeyAll APIKeyScope = "api_key:*"
|
||||
APIKeyScopeApiKeyCreate APIKeyScope = "api_key:create"
|
||||
APIKeyScopeApiKeyDelete APIKeyScope = "api_key:delete"
|
||||
APIKeyScopeApiKeyRead APIKeyScope = "api_key:read"
|
||||
APIKeyScopeApiKeyUpdate APIKeyScope = "api_key:update"
|
||||
APIKeyScopeApplicationConnect APIKeyScope = "application_connect"
|
||||
APIKeyScopeCoderAll APIKeyScope = "coder:all"
|
||||
APIKeyScopeCoderApplicationConnect APIKeyScope = "coder:application_connect"
|
||||
APIKeyScopeFileAll APIKeyScope = "file:*"
|
||||
APIKeyScopeFileCreate APIKeyScope = "file:create"
|
||||
APIKeyScopeFileRead APIKeyScope = "file:read"
|
||||
@@ -38,13 +42,13 @@ const (
|
||||
|
||||
// PublicAPIKeyScopes lists all public low-level API key scopes.
|
||||
var PublicAPIKeyScopes = []APIKeyScope{
|
||||
APIKeyScopeAll,
|
||||
APIKeyScopeApiKeyAll,
|
||||
APIKeyScopeApiKeyCreate,
|
||||
APIKeyScopeApiKeyDelete,
|
||||
APIKeyScopeApiKeyRead,
|
||||
APIKeyScopeApiKeyUpdate,
|
||||
APIKeyScopeApplicationConnect,
|
||||
APIKeyScopeCoderAll,
|
||||
APIKeyScopeCoderApplicationConnect,
|
||||
APIKeyScopeFileAll,
|
||||
APIKeyScopeFileCreate,
|
||||
APIKeyScopeFileRead,
|
||||
|
||||
Generated
+28
-18
@@ -426,6 +426,9 @@
|
||||
"lifetime_seconds": 0,
|
||||
"login_type": "password",
|
||||
"scope": "all",
|
||||
"scopes": [
|
||||
"all"
|
||||
],
|
||||
"token_name": "string",
|
||||
"updated_at": "2019-08-24T14:15:22Z",
|
||||
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
|
||||
@@ -434,18 +437,19 @@
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|----------------------------------------------|----------|--------------|-------------|
|
||||
| `created_at` | string | true | | |
|
||||
| `expires_at` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_used` | string | true | | |
|
||||
| `lifetime_seconds` | integer | true | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | true | | |
|
||||
| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | true | | |
|
||||
| `token_name` | string | true | | |
|
||||
| `updated_at` | string | true | | |
|
||||
| `user_id` | string | true | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------------|-------------------------------------------------------|----------|--------------|---------------------------------|
|
||||
| `created_at` | string | true | | |
|
||||
| `expires_at` | string | true | | |
|
||||
| `id` | string | true | | |
|
||||
| `last_used` | string | true | | |
|
||||
| `lifetime_seconds` | integer | true | | |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | true | | |
|
||||
| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | Deprecated: use Scopes instead. |
|
||||
| `scopes` | array of [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | |
|
||||
| `token_name` | string | true | | |
|
||||
| `updated_at` | string | true | | |
|
||||
| `user_id` | string | true | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
@@ -471,12 +475,14 @@
|
||||
| Value |
|
||||
|---------------------------------|
|
||||
| `all` |
|
||||
| `application_connect` |
|
||||
| `api_key:*` |
|
||||
| `api_key:create` |
|
||||
| `api_key:delete` |
|
||||
| `api_key:read` |
|
||||
| `api_key:update` |
|
||||
| `application_connect` |
|
||||
| `coder:all` |
|
||||
| `coder:application_connect` |
|
||||
| `file:*` |
|
||||
| `file:create` |
|
||||
| `file:read` |
|
||||
@@ -1774,17 +1780,21 @@ This is required on creation to enable a user-flow of validating a template work
|
||||
{
|
||||
"lifetime": 0,
|
||||
"scope": "all",
|
||||
"scopes": [
|
||||
"all"
|
||||
],
|
||||
"token_name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------|----------------------------------------------|----------|--------------|-------------|
|
||||
| `lifetime` | integer | false | | |
|
||||
| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | |
|
||||
| `token_name` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|--------------|-------------------------------------------------------|----------|--------------|---------------------------------|
|
||||
| `lifetime` | integer | false | | |
|
||||
| `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | Deprecated: use Scopes instead. |
|
||||
| `scopes` | array of [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | |
|
||||
| `token_name` | string | false | | |
|
||||
|
||||
## codersdk.CreateUserRequestWithOrgs
|
||||
|
||||
|
||||
Generated
+26
-13
@@ -764,6 +764,9 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens \
|
||||
"lifetime_seconds": 0,
|
||||
"login_type": "password",
|
||||
"scope": "all",
|
||||
"scopes": [
|
||||
"all"
|
||||
],
|
||||
"token_name": "string",
|
||||
"updated_at": "2019-08-24T14:15:22Z",
|
||||
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
|
||||
@@ -781,19 +784,20 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens \
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|----------------------|--------------------------------------------------------|----------|--------------|-------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» created_at` | string(date-time) | true | | |
|
||||
| `» expires_at` | string(date-time) | true | | |
|
||||
| `» id` | string | true | | |
|
||||
| `» last_used` | string(date-time) | true | | |
|
||||
| `» lifetime_seconds` | integer | true | | |
|
||||
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | true | | |
|
||||
| `» scope` | [codersdk.APIKeyScope](schemas.md#codersdkapikeyscope) | true | | |
|
||||
| `» token_name` | string | true | | |
|
||||
| `» updated_at` | string(date-time) | true | | |
|
||||
| `» user_id` | string(uuid) | true | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|----------------------|--------------------------------------------------------|----------|--------------|---------------------------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» created_at` | string(date-time) | true | | |
|
||||
| `» expires_at` | string(date-time) | true | | |
|
||||
| `» id` | string | true | | |
|
||||
| `» last_used` | string(date-time) | true | | |
|
||||
| `» lifetime_seconds` | integer | true | | |
|
||||
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | true | | |
|
||||
| `» scope` | [codersdk.APIKeyScope](schemas.md#codersdkapikeyscope) | false | | Deprecated: use Scopes instead. |
|
||||
| `» scopes` | array | false | | |
|
||||
| `» token_name` | string | true | | |
|
||||
| `» updated_at` | string(date-time) | true | | |
|
||||
| `» user_id` | string(uuid) | true | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
@@ -828,6 +832,9 @@ curl -X POST http://coder-server:8080/api/v2/users/{user}/keys/tokens \
|
||||
{
|
||||
"lifetime": 0,
|
||||
"scope": "all",
|
||||
"scopes": [
|
||||
"all"
|
||||
],
|
||||
"token_name": "string"
|
||||
}
|
||||
```
|
||||
@@ -890,6 +897,9 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/tokens/{keyname} \
|
||||
"lifetime_seconds": 0,
|
||||
"login_type": "password",
|
||||
"scope": "all",
|
||||
"scopes": [
|
||||
"all"
|
||||
],
|
||||
"token_name": "string",
|
||||
"updated_at": "2019-08-24T14:15:22Z",
|
||||
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
|
||||
@@ -937,6 +947,9 @@ curl -X GET http://coder-server:8080/api/v2/users/{user}/keys/{keyid} \
|
||||
"lifetime_seconds": 0,
|
||||
"login_type": "password",
|
||||
"scope": "all",
|
||||
"scopes": [
|
||||
"all"
|
||||
],
|
||||
"token_name": "string",
|
||||
"updated_at": "2019-08-24T14:15:22Z",
|
||||
"user_id": "a169451c-8525-4352-b8ca-070dd449a1a5"
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestAuthorization(t *testing.T) {
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
LoginType: database.LoginTypePassword,
|
||||
Scopes: []database.APIKeyScope{database.APIKeyScopeAll},
|
||||
Scopes: []database.APIKeyScope{database.ApiKeyScopeCoderAll},
|
||||
TokenName: "",
|
||||
}
|
||||
if tc.key == "" {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
@@ -26,7 +26,7 @@ func main() {
|
||||
|
||||
func generate() ([]byte, error) {
|
||||
names := rbac.ExternalScopeNames()
|
||||
sort.Strings(names)
|
||||
slices.Sort(names)
|
||||
|
||||
var b bytes.Buffer
|
||||
if _, err := b.WriteString("// Code generated by scripts/apikeyscopesgen. DO NOT EDIT.\n"); err != nil {
|
||||
@@ -35,11 +35,32 @@ func generate() ([]byte, error) {
|
||||
if _, err := b.WriteString("package codersdk\n\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// NOTE: Keep all APIKeyScope constants in a single generated file.
|
||||
// Some tooling (e.g. swaggo) can behave non-deterministically when
|
||||
// enums are spread across multiple files:
|
||||
// https://github.com/swaggo/swag/issues/2038
|
||||
// We generate everything into codersdk/apikey_scopes_gen.go as the
|
||||
// single source of truth so doc generation remains stable.
|
||||
|
||||
// Constants
|
||||
if _, err := b.WriteString("const (\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Always include legacy/deprecated aliases for backward compatibility.
|
||||
// These are kept in generated code to ensure consistent availability
|
||||
// across releases even if hand-written files change.
|
||||
if _, err := b.WriteString("\t// Deprecated: use codersdk.APIKeyScopeCoderAll instead.\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := b.WriteString("\tAPIKeyScopeAll APIKeyScope = \"all\"\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := b.WriteString("\t// Deprecated: use codersdk.APIKeyScopeCoderApplicationConnect instead.\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := b.WriteString("\tAPIKeyScopeApplicationConnect APIKeyScope = \"application_connect\"\n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, n := range names {
|
||||
res, act := splitRA(n)
|
||||
if act == policy.WildcardSymbol {
|
||||
|
||||
Generated
+7
-1
@@ -48,6 +48,7 @@ export interface APIKey {
|
||||
readonly updated_at: string;
|
||||
readonly login_type: LoginType;
|
||||
readonly scope: APIKeyScope;
|
||||
readonly scopes: readonly APIKeyScope[];
|
||||
readonly token_name: string;
|
||||
readonly lifetime_seconds: number;
|
||||
}
|
||||
@@ -61,6 +62,8 @@ export type APIKeyScope =
|
||||
| "api_key:read"
|
||||
| "api_key:update"
|
||||
| "application_connect"
|
||||
| "coder:all"
|
||||
| "coder:application_connect"
|
||||
| "file:*"
|
||||
| "file:create"
|
||||
| "file:read"
|
||||
@@ -95,6 +98,8 @@ export const APIKeyScopes: APIKeyScope[] = [
|
||||
"api_key:read",
|
||||
"api_key:update",
|
||||
"application_connect",
|
||||
"coder:all",
|
||||
"coder:application_connect",
|
||||
"file:*",
|
||||
"file:create",
|
||||
"file:read",
|
||||
@@ -632,7 +637,8 @@ export interface CreateTestAuditLogRequest {
|
||||
// From codersdk/apikey.go
|
||||
export interface CreateTokenRequest {
|
||||
readonly lifetime: number;
|
||||
readonly scope: APIKeyScope;
|
||||
readonly scope?: APIKeyScope;
|
||||
readonly scopes?: readonly APIKeyScope[];
|
||||
readonly token_name: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ export const MockToken: TypesGen.APIKeyWithOwner = {
|
||||
updated_at: "2022-12-16T20:10:45.637452Z",
|
||||
login_type: "token",
|
||||
scope: "all",
|
||||
scopes: ["coder:all"],
|
||||
lifetime_seconds: 2592000,
|
||||
token_name: "token-one",
|
||||
username: "admin",
|
||||
@@ -88,6 +89,7 @@ export const MockTokens: TypesGen.APIKeyWithOwner[] = [
|
||||
updated_at: "2022-12-16T20:10:45.637452Z",
|
||||
login_type: "token",
|
||||
scope: "all",
|
||||
scopes: ["coder:all"],
|
||||
lifetime_seconds: 2592000,
|
||||
token_name: "token-two",
|
||||
username: "admin",
|
||||
|
||||
Reference in New Issue
Block a user