diff --git a/Makefile b/Makefile index ed746b0f71..2f9a4ffd73 100644 --- a/Makefile +++ b/Makefile @@ -651,6 +651,7 @@ GEN_FILES := \ coderd/rbac/object_gen.go \ codersdk/rbacresources_gen.go \ coderd/rbac/scopes_constants_gen.go \ + codersdk/apikey_scopes_gen.go \ docs/admin/integrations/prometheus.md \ docs/reference/cli/index.md \ docs/admin/security/audit-logs.md \ @@ -870,6 +871,12 @@ codersdk/rbacresources_gen.go: scripts/typegen/codersdk.gotmpl scripts/typegen/m mv /tmp/rbacresources_gen.go codersdk/rbacresources_gen.go 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. + go run ./scripts/apikeyscopesgen > /tmp/apikey_scopes_gen.go + mv /tmp/apikey_scopes_gen.go codersdk/apikey_scopes_gen.go + touch "$@" + site/src/api/rbacresourcesGenerated.ts: site/node_modules/.installed scripts/typegen/codersdk.gotmpl scripts/typegen/main.go coderd/rbac/object.go coderd/rbac/policy/policy.go go run scripts/typegen/main.go rbac typescript > "$@" (cd site/ && pnpm exec biome format --write src/api/rbacresourcesGenerated.ts) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index ecb2b4a7ea..0961bbd904 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -324,6 +324,26 @@ const docTemplate = `{ } } }, + "/auth/scopes": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Authorization" + ], + "summary": "List API key scopes", + "operationId": "list-api-key-scopes", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ExternalAPIKeyScopes" + } + } + } + } + }, "/authcheck": { "post": { "security": [ @@ -11299,11 +11319,71 @@ const docTemplate = `{ "type": "string", "enum": [ "all", - "application_connect" + "api_key:*", + "api_key:create", + "api_key:delete", + "api_key:read", + "api_key:update", + "application_connect", + "file:*", + "file:create", + "file:read", + "template:*", + "template:create", + "template:delete", + "template:read", + "template:update", + "template:use", + "user:read_personal", + "user:update_personal", + "user_secret:*", + "user_secret:create", + "user_secret:delete", + "user_secret:read", + "user_secret:update", + "workspace:*", + "workspace:application_connect", + "workspace:create", + "workspace:delete", + "workspace:read", + "workspace:ssh", + "workspace:start", + "workspace:stop", + "workspace:update" ], "x-enum-varnames": [ "APIKeyScopeAll", - "APIKeyScopeApplicationConnect" + "APIKeyScopeApiKeyAll", + "APIKeyScopeApiKeyCreate", + "APIKeyScopeApiKeyDelete", + "APIKeyScopeApiKeyRead", + "APIKeyScopeApiKeyUpdate", + "APIKeyScopeApplicationConnect", + "APIKeyScopeFileAll", + "APIKeyScopeFileCreate", + "APIKeyScopeFileRead", + "APIKeyScopeTemplateAll", + "APIKeyScopeTemplateCreate", + "APIKeyScopeTemplateDelete", + "APIKeyScopeTemplateRead", + "APIKeyScopeTemplateUpdate", + "APIKeyScopeTemplateUse", + "APIKeyScopeUserReadPersonal", + "APIKeyScopeUserUpdatePersonal", + "APIKeyScopeUserSecretAll", + "APIKeyScopeUserSecretCreate", + "APIKeyScopeUserSecretDelete", + "APIKeyScopeUserSecretRead", + "APIKeyScopeUserSecretUpdate", + "APIKeyScopeWorkspaceAll", + "APIKeyScopeWorkspaceApplicationConnect", + "APIKeyScopeWorkspaceCreate", + "APIKeyScopeWorkspaceDelete", + "APIKeyScopeWorkspaceRead", + "APIKeyScopeWorkspaceSsh", + "APIKeyScopeWorkspaceStart", + "APIKeyScopeWorkspaceStop", + "APIKeyScopeWorkspaceUpdate" ] }, "codersdk.AddLicenseRequest": { @@ -12373,15 +12453,7 @@ const docTemplate = `{ "type": "integer" }, "scope": { - "enum": [ - "all", - "application_connect" - ], - "allOf": [ - { - "$ref": "#/definitions/codersdk.APIKeyScope" - } - ] + "$ref": "#/definitions/codersdk.APIKeyScope" }, "token_name": { "type": "string" @@ -13229,6 +13301,17 @@ const docTemplate = `{ "ExperimentAIBridge" ] }, + "codersdk.ExternalAPIKeyScopes": { + "type": "object", + "properties": { + "external": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.APIKeyScope" + } + } + } + }, "codersdk.ExternalAgentCredentials": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0607160868..1d5cdf296a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -274,6 +274,22 @@ } } }, + "/auth/scopes": { + "get": { + "produces": ["application/json"], + "tags": ["Authorization"], + "summary": "List API key scopes", + "operationId": "list-api-key-scopes", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/codersdk.ExternalAPIKeyScopes" + } + } + } + } + }, "/authcheck": { "post": { "security": [ @@ -10021,8 +10037,74 @@ }, "codersdk.APIKeyScope": { "type": "string", - "enum": ["all", "application_connect"], - "x-enum-varnames": ["APIKeyScopeAll", "APIKeyScopeApplicationConnect"] + "enum": [ + "all", + "api_key:*", + "api_key:create", + "api_key:delete", + "api_key:read", + "api_key:update", + "application_connect", + "file:*", + "file:create", + "file:read", + "template:*", + "template:create", + "template:delete", + "template:read", + "template:update", + "template:use", + "user:read_personal", + "user:update_personal", + "user_secret:*", + "user_secret:create", + "user_secret:delete", + "user_secret:read", + "user_secret:update", + "workspace:*", + "workspace:application_connect", + "workspace:create", + "workspace:delete", + "workspace:read", + "workspace:ssh", + "workspace:start", + "workspace:stop", + "workspace:update" + ], + "x-enum-varnames": [ + "APIKeyScopeAll", + "APIKeyScopeApiKeyAll", + "APIKeyScopeApiKeyCreate", + "APIKeyScopeApiKeyDelete", + "APIKeyScopeApiKeyRead", + "APIKeyScopeApiKeyUpdate", + "APIKeyScopeApplicationConnect", + "APIKeyScopeFileAll", + "APIKeyScopeFileCreate", + "APIKeyScopeFileRead", + "APIKeyScopeTemplateAll", + "APIKeyScopeTemplateCreate", + "APIKeyScopeTemplateDelete", + "APIKeyScopeTemplateRead", + "APIKeyScopeTemplateUpdate", + "APIKeyScopeTemplateUse", + "APIKeyScopeUserReadPersonal", + "APIKeyScopeUserUpdatePersonal", + "APIKeyScopeUserSecretAll", + "APIKeyScopeUserSecretCreate", + "APIKeyScopeUserSecretDelete", + "APIKeyScopeUserSecretRead", + "APIKeyScopeUserSecretUpdate", + "APIKeyScopeWorkspaceAll", + "APIKeyScopeWorkspaceApplicationConnect", + "APIKeyScopeWorkspaceCreate", + "APIKeyScopeWorkspaceDelete", + "APIKeyScopeWorkspaceRead", + "APIKeyScopeWorkspaceSsh", + "APIKeyScopeWorkspaceStart", + "APIKeyScopeWorkspaceStop", + "APIKeyScopeWorkspaceUpdate" + ] }, "codersdk.AddLicenseRequest": { "type": "object", @@ -11032,12 +11114,7 @@ "type": "integer" }, "scope": { - "enum": ["all", "application_connect"], - "allOf": [ - { - "$ref": "#/definitions/codersdk.APIKeyScope" - } - ] + "$ref": "#/definitions/codersdk.APIKeyScope" }, "token_name": { "type": "string" @@ -11863,6 +11940,17 @@ "ExperimentAIBridge" ] }, + "codersdk.ExternalAPIKeyScopes": { + "type": "object", + "properties": { + "external": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.APIKeyScope" + } + } + } + }, "codersdk.ExternalAgentCredentials": { "type": "object", "properties": { diff --git a/coderd/apikey.go b/coderd/apikey.go index 9063f0dac7..226c50c15b 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -66,9 +66,19 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) { return } - scope := database.APIKeyScopeAll - if scope != "" { - scope = database.APIKeyScope(createToken.Scope) + // 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 != "" { + 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), + }) + return + } + scopes = database.APIKeyScopes{database.APIKeyScope(name)} } tokenName := namesgenerator.GetRandomName(1) @@ -81,7 +91,7 @@ func (api *API) postToken(rw http.ResponseWriter, r *http.Request) { UserID: user.ID, LoginType: database.LoginTypeToken, DefaultLifetime: api.DeploymentValues.Sessions.DefaultTokenDuration.Value(), - Scope: scope, + Scopes: scopes, TokenName: tokenName, } diff --git a/coderd/apikey/apikey.go b/coderd/apikey/apikey.go index 10586178e1..738b2c2126 100644 --- a/coderd/apikey/apikey.go +++ b/coderd/apikey/apikey.go @@ -25,9 +25,16 @@ type CreateParams struct { // Optional. ExpiresAt time.Time LifetimeSeconds int64 - Scope database.APIKeyScope - TokenName string - RemoteAddr string + // Scope is legacy single-scope input kept for backward compatibility. + // + // Deprecated: Prefer Scopes for new code. + 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 } // Generate generates an API key, returning the key as a string as well as the @@ -62,14 +69,20 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error) bitlen := len(ip) * 8 - scope := database.APIKeyScopeAll - if params.Scope != "" { - scope = params.Scope - } - switch scope { - case database.APIKeyScopeAll, database.APIKeyScopeApplicationConnect: + var scopes database.APIKeyScopes + switch { + case len(params.Scopes) > 0: + scopes = params.Scopes + case params.Scope != "": + scopes = database.APIKeyScopes{params.Scope} default: - return database.InsertAPIKeyParams{}, "", xerrors.Errorf("invalid API key scope: %q", scope) + scopes = database.APIKeyScopes{database.APIKeyScopeAll} + } + + for _, s := range scopes { + if !s.Valid() { + return database.InsertAPIKeyParams{}, "", xerrors.Errorf("invalid API key scope: %q", s) + } } token := fmt.Sprintf("%s-%s", keyID, keySecret) @@ -92,7 +105,7 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error) UpdatedAt: dbtime.Now(), HashedSecret: hashed[:], LoginType: params.LoginType, - Scopes: database.APIKeyScopes{scope}, + Scopes: scopes, AllowList: database.AllowList{database.AllowListWildcard()}, TokenName: params.TokenName, }, token, nil diff --git a/coderd/apikey_scopes_validation_test.go b/coderd/apikey_scopes_validation_test.go new file mode 100644 index 0000000000..187ec77792 --- /dev/null +++ b/coderd/apikey_scopes_validation_test.go @@ -0,0 +1,47 @@ +package coderd_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/testutil" +) + +func TestTokenCreation_ScopeValidation(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + scope codersdk.APIKeyScope + wantErr bool + }{ + {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}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(t.Context(), testutil.WaitShort) + defer cancel() + + resp, err := client.CreateToken(ctx, codersdk.Me, codersdk.CreateTokenRequest{Scope: tc.scope}) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NotEmpty(t, resp.Key) + }) + } +} diff --git a/coderd/coderd.go b/coderd/coderd.go index 57dea0f85f..1724c369ec 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1048,6 +1048,8 @@ func New(options *Options) *API { // All CSP errors will be logged r.Post("/csp/reports", api.logReportCSPViolations) + r.Get("/auth/scopes", api.listExternalScopes) + r.Get("/buildinfo", buildInfoHandler(buildInfo)) // /regions is overridden in the enterprise version r.Group(func(r chi.Router) { diff --git a/coderd/coderdtest/swaggerparser.go b/coderd/coderdtest/swaggerparser.go index b94473ee83..4a0b6744a9 100644 --- a/coderd/coderdtest/swaggerparser.go +++ b/coderd/coderdtest/swaggerparser.go @@ -308,6 +308,7 @@ func assertSecurityDefined(t *testing.T, comment SwaggerComment) { if comment.router == "/updatecheck" || comment.router == "/buildinfo" || comment.router == "/" || + comment.router == "/auth/scopes" || comment.router == "/users/login" || comment.router == "/users/otp/request" || comment.router == "/users/otp/change-password" || diff --git a/coderd/scopes_catalog.go b/coderd/scopes_catalog.go new file mode 100644 index 0000000000..789cbb0af1 --- /dev/null +++ b/coderd/scopes_catalog.go @@ -0,0 +1,30 @@ +package coderd + +import ( + "net/http" + + "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/codersdk" +) + +// listExternalScopes returns the curated list of API key scopes (resource:action) +// requestable via the API. +// +// @Summary List API key scopes +// @ID list-api-key-scopes +// @Tags Authorization +// @Produce json +// @Success 200 {object} codersdk.ExternalAPIKeyScopes +// @Router /auth/scopes [get] +func (*API) listExternalScopes(rw http.ResponseWriter, r *http.Request) { + scopes := rbac.ExternalScopeNames() + external := make([]codersdk.APIKeyScope, 0, len(scopes)) + for _, scope := range scopes { + external = append(external, codersdk.APIKeyScope(scope)) + } + + httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.ExternalAPIKeyScopes{ + External: external, + }) +} diff --git a/coderd/scopes_catalog_api_test.go b/coderd/scopes_catalog_api_test.go new file mode 100644 index 0000000000..3de74843f3 --- /dev/null +++ b/coderd/scopes_catalog_api_test.go @@ -0,0 +1,30 @@ +package coderd_test + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/rbac" +) + +func TestListPublicLowLevelScopes(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + + res, err := client.Request(t.Context(), http.MethodGet, "/api/v2/auth/scopes", nil) + require.NoError(t, err) + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + + var got struct { + External []string `json:"external"` + } + require.NoError(t, json.NewDecoder(res.Body).Decode(&got)) + + want := rbac.ExternalScopeNames() + require.Equal(t, want, got.External) +} diff --git a/codersdk/apikey.go b/codersdk/apikey.go index 32c97cf538..fd9acf7a64 100644 --- a/codersdk/apikey.go +++ b/codersdk/apikey.go @@ -42,17 +42,9 @@ const ( type APIKeyScope string -const ( - // APIKeyScopeAll is a scope that allows the user to do everything. - APIKeyScopeAll APIKeyScope = "all" - // APIKeyScopeApplicationConnect is a scope that allows the user - // to connect to applications in a workspace. - APIKeyScopeApplicationConnect APIKeyScope = "application_connect" -) - type CreateTokenRequest struct { Lifetime time.Duration `json:"lifetime"` - Scope APIKeyScope `json:"scope" enums:"all,application_connect"` + Scope APIKeyScope `json:"scope"` TokenName string `json:"token_name"` } diff --git a/codersdk/apikey_scopes_gen.go b/codersdk/apikey_scopes_gen.go new file mode 100644 index 0000000000..eee7d57e7b --- /dev/null +++ b/codersdk/apikey_scopes_gen.go @@ -0,0 +1,73 @@ +// Code generated by scripts/apikeyscopesgen. DO NOT EDIT. +package codersdk + +const ( + APIKeyScopeAll APIKeyScope = "all" + 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" + APIKeyScopeFileAll APIKeyScope = "file:*" + APIKeyScopeFileCreate APIKeyScope = "file:create" + APIKeyScopeFileRead APIKeyScope = "file:read" + APIKeyScopeTemplateAll APIKeyScope = "template:*" + APIKeyScopeTemplateCreate APIKeyScope = "template:create" + APIKeyScopeTemplateDelete APIKeyScope = "template:delete" + APIKeyScopeTemplateRead APIKeyScope = "template:read" + APIKeyScopeTemplateUpdate APIKeyScope = "template:update" + APIKeyScopeTemplateUse APIKeyScope = "template:use" + APIKeyScopeUserReadPersonal APIKeyScope = "user:read_personal" + APIKeyScopeUserUpdatePersonal APIKeyScope = "user:update_personal" + APIKeyScopeUserSecretAll APIKeyScope = "user_secret:*" + APIKeyScopeUserSecretCreate APIKeyScope = "user_secret:create" + APIKeyScopeUserSecretDelete APIKeyScope = "user_secret:delete" + APIKeyScopeUserSecretRead APIKeyScope = "user_secret:read" + APIKeyScopeUserSecretUpdate APIKeyScope = "user_secret:update" + APIKeyScopeWorkspaceAll APIKeyScope = "workspace:*" + APIKeyScopeWorkspaceApplicationConnect APIKeyScope = "workspace:application_connect" + APIKeyScopeWorkspaceCreate APIKeyScope = "workspace:create" + APIKeyScopeWorkspaceDelete APIKeyScope = "workspace:delete" + APIKeyScopeWorkspaceRead APIKeyScope = "workspace:read" + APIKeyScopeWorkspaceSsh APIKeyScope = "workspace:ssh" + APIKeyScopeWorkspaceStart APIKeyScope = "workspace:start" + APIKeyScopeWorkspaceStop APIKeyScope = "workspace:stop" + APIKeyScopeWorkspaceUpdate APIKeyScope = "workspace:update" +) + +// PublicAPIKeyScopes lists all public low-level API key scopes. +var PublicAPIKeyScopes = []APIKeyScope{ + APIKeyScopeAll, + APIKeyScopeApiKeyAll, + APIKeyScopeApiKeyCreate, + APIKeyScopeApiKeyDelete, + APIKeyScopeApiKeyRead, + APIKeyScopeApiKeyUpdate, + APIKeyScopeApplicationConnect, + APIKeyScopeFileAll, + APIKeyScopeFileCreate, + APIKeyScopeFileRead, + APIKeyScopeTemplateAll, + APIKeyScopeTemplateCreate, + APIKeyScopeTemplateDelete, + APIKeyScopeTemplateRead, + APIKeyScopeTemplateUpdate, + APIKeyScopeTemplateUse, + APIKeyScopeUserReadPersonal, + APIKeyScopeUserUpdatePersonal, + APIKeyScopeUserSecretAll, + APIKeyScopeUserSecretCreate, + APIKeyScopeUserSecretDelete, + APIKeyScopeUserSecretRead, + APIKeyScopeUserSecretUpdate, + APIKeyScopeWorkspaceAll, + APIKeyScopeWorkspaceApplicationConnect, + APIKeyScopeWorkspaceCreate, + APIKeyScopeWorkspaceDelete, + APIKeyScopeWorkspaceRead, + APIKeyScopeWorkspaceSsh, + APIKeyScopeWorkspaceStart, + APIKeyScopeWorkspaceStop, + APIKeyScopeWorkspaceUpdate, +} diff --git a/codersdk/scopes_catalog.go b/codersdk/scopes_catalog.go new file mode 100644 index 0000000000..220dca3fa5 --- /dev/null +++ b/codersdk/scopes_catalog.go @@ -0,0 +1,5 @@ +package codersdk + +type ExternalAPIKeyScopes struct { + External []APIKeyScope `json:"external"` +} diff --git a/docs/reference/api/authorization.md b/docs/reference/api/authorization.md index 3565a8c922..e13964b869 100644 --- a/docs/reference/api/authorization.md +++ b/docs/reference/api/authorization.md @@ -1,5 +1,35 @@ # Authorization +## List API key scopes + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/auth/scopes \ + -H 'Accept: application/json' +``` + +`GET /auth/scopes` + +### Example responses + +> 200 Response + +```json +{ + "external": [ + "all" + ] +} +``` + +### Responses + +| Status | Meaning | Description | Schema | +|--------|---------------------------------------------------------|-------------|--------------------------------------------------------------------------| +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.ExternalAPIKeyScopes](schemas.md#codersdkexternalapikeyscopes) | + ## Check authorization ### Code samples diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 1f3f231f45..a121ea236f 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -468,10 +468,40 @@ #### Enumerated Values -| Value | -|-----------------------| -| `all` | -| `application_connect` | +| Value | +|---------------------------------| +| `all` | +| `api_key:*` | +| `api_key:create` | +| `api_key:delete` | +| `api_key:read` | +| `api_key:update` | +| `application_connect` | +| `file:*` | +| `file:create` | +| `file:read` | +| `template:*` | +| `template:create` | +| `template:delete` | +| `template:read` | +| `template:update` | +| `template:use` | +| `user:read_personal` | +| `user:update_personal` | +| `user_secret:*` | +| `user_secret:create` | +| `user_secret:delete` | +| `user_secret:read` | +| `user_secret:update` | +| `workspace:*` | +| `workspace:application_connect` | +| `workspace:create` | +| `workspace:delete` | +| `workspace:read` | +| `workspace:ssh` | +| `workspace:start` | +| `workspace:stop` | +| `workspace:update` | ## codersdk.AddLicenseRequest @@ -1756,13 +1786,6 @@ This is required on creation to enable a user-flow of validating a template work | `scope` | [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | | | `token_name` | string | false | | | -#### Enumerated Values - -| Property | Value | -|----------|-----------------------| -| `scope` | `all` | -| `scope` | `application_connect` | - ## codersdk.CreateUserRequestWithOrgs ```json @@ -3489,6 +3512,22 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `workspace-sharing` | | `aibridge` | +## codersdk.ExternalAPIKeyScopes + +```json +{ + "external": [ + "all" + ] +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|------------|-------------------------------------------------------|----------|--------------|-------------| +| `external` | array of [codersdk.APIKeyScope](#codersdkapikeyscope) | false | | | + ## codersdk.ExternalAgentCredentials ```json diff --git a/scripts/apikeyscopesgen/main.go b/scripts/apikeyscopesgen/main.go new file mode 100644 index 0000000000..7a131df4a6 --- /dev/null +++ b/scripts/apikeyscopesgen/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "bytes" + "fmt" + "go/format" + "os" + "sort" + "strings" + + "github.com/coder/coder/v2/coderd/rbac" + "github.com/coder/coder/v2/coderd/rbac/policy" +) + +func main() { + out, err := generate() + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "generate apikey scopes: %v\n", err) + os.Exit(1) + } + if _, err := fmt.Print(string(out)); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "write output: %v\n", err) + os.Exit(1) + } +} + +func generate() ([]byte, error) { + names := rbac.ExternalScopeNames() + sort.Strings(names) + + var b bytes.Buffer + if _, err := b.WriteString("// Code generated by scripts/apikeyscopesgen. DO NOT EDIT.\n"); err != nil { + return nil, err + } + if _, err := b.WriteString("package codersdk\n\n"); err != nil { + return nil, err + } + + // Constants + if _, err := b.WriteString("const (\n"); err != nil { + return nil, err + } + for _, n := range names { + res, act := splitRA(n) + if act == policy.WildcardSymbol { + act = "All" + } + constName := fmt.Sprintf("APIKeyScope%s%s", pascal(res), pascal(act)) + if _, err := fmt.Fprintf(&b, "\t%s APIKeyScope = \"%s\"\n", constName, n); err != nil { + return nil, err + } + } + if _, err := b.WriteString(")\n\n"); err != nil { + return nil, err + } + + // Slices + if _, err := b.WriteString("// PublicAPIKeyScopes lists all public low-level API key scopes.\n"); err != nil { + return nil, err + } + if _, err := b.WriteString("var PublicAPIKeyScopes = []APIKeyScope{\n"); err != nil { + return nil, err + } + for _, n := range names { + res, act := splitRA(n) + if act == policy.WildcardSymbol { + act = "All" + } + constName := fmt.Sprintf("APIKeyScope%s%s", pascal(res), pascal(act)) + if _, err := fmt.Fprintf(&b, "\t%s,\n", constName); err != nil { + return nil, err + } + } + if _, err := b.WriteString("}\n\n"); err != nil { + return nil, err + } + + return format.Source(b.Bytes()) +} + +func splitRA(name string) (resource string, action string) { + parts := strings.SplitN(name, ":", 2) + if len(parts) != 2 { + return name, "" + } + return parts[0], parts[1] +} + +func pascal(s string) string { + // Replace non-identifier separators with spaces, then Title-case and strip. + s = strings.ReplaceAll(s, "_", " ") + s = strings.ReplaceAll(s, "-", " ") + s = strings.ReplaceAll(s, ":", " ") + words := strings.Fields(s) + for i := range words { + words[i] = strings.ToUpper(words[i][:1]) + words[i][1:] + } + return strings.Join(words, "") +} diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index d8f80586fe..bfd5532510 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -53,9 +53,74 @@ export interface APIKey { } // From codersdk/apikey.go -export type APIKeyScope = "all" | "application_connect"; +export type APIKeyScope = + | "all" + | "api_key:*" + | "api_key:create" + | "api_key:delete" + | "api_key:read" + | "api_key:update" + | "application_connect" + | "file:*" + | "file:create" + | "file:read" + | "template:*" + | "template:create" + | "template:delete" + | "template:read" + | "template:update" + | "template:use" + | "user:read_personal" + | "user_secret:*" + | "user_secret:create" + | "user_secret:delete" + | "user_secret:read" + | "user_secret:update" + | "user:update_personal" + | "workspace:*" + | "workspace:application_connect" + | "workspace:create" + | "workspace:delete" + | "workspace:read" + | "workspace:ssh" + | "workspace:start" + | "workspace:stop" + | "workspace:update"; -export const APIKeyScopes: APIKeyScope[] = ["all", "application_connect"]; +export const APIKeyScopes: APIKeyScope[] = [ + "all", + "api_key:*", + "api_key:create", + "api_key:delete", + "api_key:read", + "api_key:update", + "application_connect", + "file:*", + "file:create", + "file:read", + "template:*", + "template:create", + "template:delete", + "template:read", + "template:update", + "template:use", + "user:read_personal", + "user_secret:*", + "user_secret:create", + "user_secret:delete", + "user_secret:read", + "user_secret:update", + "user:update_personal", + "workspace:*", + "workspace:application_connect", + "workspace:create", + "workspace:delete", + "workspace:read", + "workspace:ssh", + "workspace:start", + "workspace:stop", + "workspace:update", +]; // From codersdk/apikey.go export interface APIKeyWithOwner extends APIKey { @@ -983,6 +1048,11 @@ export const Experiments: Experiment[] = [ "workspace-usage", ]; +// From codersdk/scopes_catalog.go +export interface ExternalAPIKeyScopes { + readonly external: readonly APIKeyScope[]; +} + // From codersdk/workspaces.go export interface ExternalAgentCredentials { readonly command: string;