From a2ccfb08f4a4f963f0f7683b9f02da53ee60e851 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sat, 30 Sep 2023 20:45:09 +0000 Subject: [PATCH 01/19] feat: allow external services to be authable --- cli/errors.go | 3 +- cli/server.go | 4 ++ coderd/apidoc/docs.go | 20 +++++-- coderd/apidoc/swagger.json | 26 +++++---- coderd/externalauth.go | 2 +- coderd/externalauth/config.go | 12 ++++ coderd/templateversions.go | 2 + codersdk/deployment.go | 2 + codersdk/externalauth.go | 2 +- codersdk/templateversions.go | 2 + codersdk/workspaceagents.go | 2 +- docs/api/general.md | 2 + docs/api/git.md | 2 +- docs/api/schemas.md | 32 +++++++--- docs/api/templates.md | 18 +++--- site/src/api/typesGenerated.ts | 10 +++- .../CreateWorkspacePageView.stories.tsx | 4 ++ .../CreateWorkspacePageView.tsx | 7 ++- .../ExternalAuth.stories.tsx | 24 +++++--- .../CreateWorkspacePage/ExternalAuth.tsx | 58 ++++++------------- .../GitAuthSettingsPageView.stories.tsx | 2 + .../ExternalAuthPageView.stories.tsx | 10 ++-- .../ExternalAuthPage/ExternalAuthPageView.tsx | 12 ++-- site/src/testHelpers/entities.ts | 4 ++ .../icon/azure-devops.svg} | 8 +-- .../icon/bitbucket.svg} | 8 +-- site/static/icon/github.svg | 1 + site/static/icon/gitlab.svg | 25 ++++++++ 28 files changed, 193 insertions(+), 111 deletions(-) rename site/{src/components/Icons/AzureDevOpsIcon.tsx => static/icon/azure-devops.svg} (76%) rename site/{src/components/Icons/BitbucketIcon.tsx => static/icon/bitbucket.svg} (89%) create mode 100644 site/static/icon/github.svg create mode 100644 site/static/icon/gitlab.svg diff --git a/cli/errors.go b/cli/errors.go index 6f873f06f8045..12567e0400ac5 100644 --- a/cli/errors.go +++ b/cli/errors.go @@ -6,9 +6,10 @@ import ( "net/http/httptest" "os" + "golang.org/x/xerrors" + "github.com/coder/coder/v2/cli/clibase" "github.com/coder/coder/v2/codersdk" - "golang.org/x/xerrors" ) func (RootCmd) errorExample() *clibase.Cmd { diff --git a/cli/server.go b/cli/server.go index b0ec9f999b448..0cc065998b7b5 100644 --- a/cli/server.go +++ b/cli/server.go @@ -171,6 +171,10 @@ func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.GitAuthConfig, er provider.AppInstallURL = v.Value case "APP_INSTALLATIONS_URL": provider.AppInstallationsURL = v.Value + case "DISPLAY_NAME": + provider.DisplayName = v.Value + case "DISPLAY_ICON": + provider.DisplayIcon = v.Value } providers[providerNum] = provider } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index ab794794f6e3e..69c4863a92f09 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8203,6 +8203,9 @@ const docTemplate = `{ "device": { "type": "boolean" }, + "display_name": { + "type": "string" + }, "installations": { "description": "AppInstallations are the installations that the user has access to.", "type": "array", @@ -8210,9 +8213,6 @@ const docTemplate = `{ "$ref": "#/definitions/codersdk.ExternalAuthAppInstallation" } }, - "type": { - "type": "string" - }, "user": { "description": "User is the user that authenticated with the provider.", "allOf": [ @@ -8264,7 +8264,7 @@ const docTemplate = `{ "github", "gitlab", "bitbucket", - "openid-connect" + "oidc" ], "x-enum-varnames": [ "ExternalAuthProviderAzureDevops", @@ -8351,6 +8351,12 @@ const docTemplate = `{ "device_flow": { "type": "boolean" }, + "display_icon": { + "type": "string" + }, + "display_name": { + "type": "string" + }, "id": { "type": "string" }, @@ -10018,6 +10024,12 @@ const docTemplate = `{ "authenticated": { "type": "boolean" }, + "display_icon": { + "type": "string" + }, + "display_name": { + "type": "string" + }, "id": { "type": "string" }, diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 7a7ee0a69e12c..eacd7b96075ee 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7351,6 +7351,9 @@ "device": { "type": "boolean" }, + "display_name": { + "type": "string" + }, "installations": { "description": "AppInstallations are the installations that the user has access to.", "type": "array", @@ -7358,9 +7361,6 @@ "$ref": "#/definitions/codersdk.ExternalAuthAppInstallation" } }, - "type": { - "type": "string" - }, "user": { "description": "User is the user that authenticated with the provider.", "allOf": [ @@ -7407,13 +7407,7 @@ }, "codersdk.ExternalAuthProvider": { "type": "string", - "enum": [ - "azure-devops", - "github", - "gitlab", - "bitbucket", - "openid-connect" - ], + "enum": ["azure-devops", "github", "gitlab", "bitbucket", "oidc"], "x-enum-varnames": [ "ExternalAuthProviderAzureDevops", "ExternalAuthProviderGitHub", @@ -7499,6 +7493,12 @@ "device_flow": { "type": "boolean" }, + "display_icon": { + "type": "string" + }, + "display_name": { + "type": "string" + }, "id": { "type": "string" }, @@ -9065,6 +9065,12 @@ "authenticated": { "type": "boolean" }, + "display_icon": { + "type": "string" + }, + "display_name": { + "type": "string" + }, "id": { "type": "string" }, diff --git a/coderd/externalauth.go b/coderd/externalauth.go index f668d6b5a980a..e98d31a36701a 100644 --- a/coderd/externalauth.go +++ b/coderd/externalauth.go @@ -33,7 +33,7 @@ func (api *API) externalAuthByID(w http.ResponseWriter, r *http.Request) { Authenticated: false, Device: config.DeviceAuth != nil, AppInstallURL: config.AppInstallURL, - Type: config.Type.Pretty(), + DisplayName: config.DisplayName, AppInstallations: []codersdk.ExternalAuthAppInstallation{}, } diff --git a/coderd/externalauth/config.go b/coderd/externalauth/config.go index 51ef86635466b..2d0c5b3bbc213 100644 --- a/coderd/externalauth/config.go +++ b/coderd/externalauth/config.go @@ -37,6 +37,10 @@ type Config struct { Type codersdk.ExternalAuthProvider // DeviceAuth is set if the provider uses the device flow. DeviceAuth *DeviceAuth + // DisplayName is the name of the provider to display to the user. + DisplayName string + // DisplayIcon is the path to an image that will be displayed to the user. + DisplayIcon string // NoRefresh stops Coder from using the refresh token // to renew the access token. @@ -258,6 +262,8 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con typ = codersdk.ExternalAuthProviderGitHub case codersdk.ExternalAuthProviderGitLab: typ = codersdk.ExternalAuthProviderGitLab + case codersdk.ExternalAuthProviderOpenIDConnect: + typ = codersdk.ExternalAuthProviderOpenIDConnect default: return nil, xerrors.Errorf("unknown git provider type: %q", entry.Type) } @@ -332,6 +338,12 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con ValidateURL: entry.ValidateURL, AppInstallationsURL: entry.AppInstallationsURL, AppInstallURL: entry.AppInstallURL, + DisplayName: entry.DisplayName, + DisplayIcon: entry.DisplayIcon, + } + + if cfg.DisplayName == "" { + cfg.DisplayName = typ.Pretty() } if entry.DeviceFlow { diff --git a/coderd/templateversions.go b/coderd/templateversions.go index cbc9bb0605f0d..e5f5314e6242c 100644 --- a/coderd/templateversions.go +++ b/coderd/templateversions.go @@ -320,6 +320,8 @@ func (api *API) templateVersionExternalAuth(rw http.ResponseWriter, r *http.Requ ID: config.ID, Type: config.Type, AuthenticateURL: redirectURL.String(), + DisplayName: config.DisplayName, + DisplayIcon: config.DisplayIcon, } authLink, err := api.Database.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{ diff --git a/codersdk/deployment.go b/codersdk/deployment.go index be629236d344b..5b3de8790fcfe 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -336,6 +336,8 @@ type GitAuthConfig struct { Scopes []string `json:"scopes"` DeviceFlow bool `json:"device_flow"` DeviceCodeURL string `json:"device_code_url"` + DisplayName string `json:"display_name"` + DisplayIcon string `json:"display_icon"` } type ProvisionerConfig struct { diff --git a/codersdk/externalauth.go b/codersdk/externalauth.go index 5350dc3a6b93d..2625daf1fb6c5 100644 --- a/codersdk/externalauth.go +++ b/codersdk/externalauth.go @@ -10,7 +10,7 @@ import ( type ExternalAuth struct { Authenticated bool `json:"authenticated"` Device bool `json:"device"` - Type string `json:"type"` + DisplayName string `json:"display_name"` // User is the user that authenticated with the provider. User *ExternalAuthUser `json:"user"` diff --git a/codersdk/templateversions.go b/codersdk/templateversions.go index fb1f0b21febf0..280fe32113d1f 100644 --- a/codersdk/templateversions.go +++ b/codersdk/templateversions.go @@ -36,6 +36,8 @@ type TemplateVersion struct { type TemplateVersionExternalAuth struct { ID string `json:"id"` Type ExternalAuthProvider `json:"type"` + DisplayName string `json:"display_name"` + DisplayIcon string `json:"display_icon"` AuthenticateURL string `json:"authenticate_url"` Authenticated bool `json:"authenticated"` } diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 0461e3ae07c33..9c26f8e3746e8 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -770,7 +770,7 @@ const ( ExternalAuthProviderGitHub ExternalAuthProvider = "github" ExternalAuthProviderGitLab ExternalAuthProvider = "gitlab" ExternalAuthProviderBitBucket ExternalAuthProvider = "bitbucket" - ExternalAuthProviderOpenIDConnect ExternalAuthProvider = "openid-connect" + ExternalAuthProviderOpenIDConnect ExternalAuthProvider = "oidc" ) type WorkspaceAgentLog struct { diff --git a/docs/api/general.md b/docs/api/general.md index ded8e5df4d319..0bb3dc48d40e6 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -222,6 +222,8 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "client_id": "string", "device_code_url": "string", "device_flow": true, + "display_icon": "string", + "display_name": "string", "id": "string", "no_refresh": true, "regex": "string", diff --git a/docs/api/git.md b/docs/api/git.md index bcc88890069f5..9f8e88b89036f 100644 --- a/docs/api/git.md +++ b/docs/api/git.md @@ -29,6 +29,7 @@ curl -X GET http://coder-server:8080/api/v2/externalauth/{externalauth} \ "app_installable": true, "authenticated": true, "device": true, + "display_name": "string", "installations": [ { "account": { @@ -41,7 +42,6 @@ curl -X GET http://coder-server:8080/api/v2/externalauth/{externalauth} \ "id": 0 } ], - "type": "string", "user": { "avatar_url": "string", "login": "string", diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 00b61e6202351..f6cfb604254ae 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -632,6 +632,8 @@ _None_ "client_id": "string", "device_code_url": "string", "device_flow": true, + "display_icon": "string", + "display_name": "string", "id": "string", "no_refresh": true, "regex": "string", @@ -2053,6 +2055,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "client_id": "string", "device_code_url": "string", "device_flow": true, + "display_icon": "string", + "display_name": "string", "id": "string", "no_refresh": true, "regex": "string", @@ -2418,6 +2422,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "client_id": "string", "device_code_url": "string", "device_flow": true, + "display_icon": "string", + "display_name": "string", "id": "string", "no_refresh": true, "regex": "string", @@ -2760,6 +2766,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "app_installable": true, "authenticated": true, "device": true, + "display_name": "string", "installations": [ { "account": { @@ -2772,7 +2779,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "id": 0 } ], - "type": "string", "user": { "avatar_url": "string", "login": "string", @@ -2790,8 +2796,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `app_installable` | boolean | false | | App installable is true if the request for app installs was successful. | | `authenticated` | boolean | false | | | | `device` | boolean | false | | | +| `display_name` | string | false | | | | `installations` | array of [codersdk.ExternalAuthAppInstallation](#codersdkexternalauthappinstallation) | false | | Installations are the installations that the user has access to. | -| `type` | string | false | | | | `user` | [codersdk.ExternalAuthUser](#codersdkexternalauthuser) | false | | User is the user that authenticated with the provider. | ## codersdk.ExternalAuthAppInstallation @@ -2849,13 +2855,13 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in #### Enumerated Values -| Value | -| ---------------- | -| `azure-devops` | -| `github` | -| `gitlab` | -| `bitbucket` | -| `openid-connect` | +| Value | +| -------------- | +| `azure-devops` | +| `github` | +| `gitlab` | +| `bitbucket` | +| `oidc` | ## codersdk.ExternalAuthUser @@ -2955,6 +2961,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "client_id": "string", "device_code_url": "string", "device_flow": true, + "display_icon": "string", + "display_name": "string", "id": "string", "no_refresh": true, "regex": "string", @@ -2975,6 +2983,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `client_id` | string | false | | | | `device_code_url` | string | false | | | | `device_flow` | boolean | false | | | +| `display_icon` | string | false | | | +| `display_name` | string | false | | | | `id` | string | false | | | | `no_refresh` | boolean | false | | | | `regex` | string | false | | | @@ -4741,6 +4751,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in { "authenticate_url": "string", "authenticated": true, + "display_icon": "string", + "display_name": "string", "id": "string", "type": "azure-devops" } @@ -4752,6 +4764,8 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | ------------------ | -------------------------------------------------------------- | -------- | ------------ | ----------- | | `authenticate_url` | string | false | | | | `authenticated` | boolean | false | | | +| `display_icon` | string | false | | | +| `display_name` | string | false | | | | `id` | string | false | | | | `type` | [codersdk.ExternalAuthProvider](#codersdkexternalauthprovider) | false | | | diff --git a/docs/api/templates.md b/docs/api/templates.md index fe9d76633e47a..649fe82b51e45 100644 --- a/docs/api/templates.md +++ b/docs/api/templates.md @@ -1828,6 +1828,8 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/e { "authenticate_url": "string", "authenticated": true, + "display_icon": "string", + "display_name": "string", "id": "string", "type": "azure-devops" } @@ -1849,18 +1851,20 @@ Status Code **200** | `[array item]` | array | false | | | | `» authenticate_url` | string | false | | | | `» authenticated` | boolean | false | | | +| `» display_icon` | string | false | | | +| `» display_name` | string | false | | | | `» id` | string | false | | | | `» type` | [codersdk.ExternalAuthProvider](schemas.md#codersdkexternalauthprovider) | false | | | #### Enumerated Values -| Property | Value | -| -------- | ---------------- | -| `type` | `azure-devops` | -| `type` | `github` | -| `type` | `gitlab` | -| `type` | `bitbucket` | -| `type` | `openid-connect` | +| Property | Value | +| -------- | -------------- | +| `type` | `azure-devops` | +| `type` | `github` | +| `type` | `gitlab` | +| `type` | `bitbucket` | +| `type` | `oidc` | To perform this operation, you must be authenticated. [Learn more](authentication.md). diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0b7f0c4cde7a4..5c70b6b2b43cc 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -426,7 +426,7 @@ export type Experiments = Experiment[]; export interface ExternalAuth { readonly authenticated: boolean; readonly device: boolean; - readonly type: string; + readonly display_name: string; readonly user?: ExternalAuthUser; readonly app_installable: boolean; readonly installations: ExternalAuthAppInstallation[]; @@ -496,6 +496,8 @@ export interface GitAuthConfig { readonly scopes: string[]; readonly device_flow: boolean; readonly device_code_url: string; + readonly display_name: string; + readonly display_icon: string; } // From codersdk/gitsshkey.go @@ -1013,6 +1015,8 @@ export interface TemplateVersion { export interface TemplateVersionExternalAuth { readonly id: string; readonly type: ExternalAuthProvider; + readonly display_name: string; + readonly display_icon: string; readonly authenticate_url: string; readonly authenticated: boolean; } @@ -1661,13 +1665,13 @@ export type ExternalAuthProvider = | "bitbucket" | "github" | "gitlab" - | "openid-connect"; + | "oidc"; export const ExternalAuthProviders: ExternalAuthProvider[] = [ "azure-devops", "bitbucket", "github", "gitlab", - "openid-connect", + "oidc", ]; // From codersdk/deployment.go diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx index 5a0478ca4dfe3..9d856266008b2 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.stories.tsx @@ -94,12 +94,16 @@ export const ExternalAuth: Story = { type: "github", authenticated: false, authenticate_url: "", + display_icon: "/icon/github.svg", + display_name: "GitHub", }, { id: "gitlab", type: "gitlab", authenticated: true, authenticate_url: "", + display_icon: "/icon/gitlab.svg", + display_name: "GitLab", }, ], }, diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 2776a43ef3e8e..c473d12ef52e5 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -163,8 +163,8 @@ export const CreateWorkspacePageView: FC = ({ {externalAuth && externalAuth.length > 0 && ( {externalAuth.map((auth) => ( @@ -174,7 +174,8 @@ export const CreateWorkspacePageView: FC = ({ authenticated={auth.authenticated} externalAuthPollingState={externalAuthPollingState} startPollingExternalAuth={startPollingExternalAuth} - type={auth.type} + displayName={auth.display_name} + displayIcon={auth.display_icon} error={externalAuthErrors[auth.id]} /> ))} diff --git a/site/src/pages/CreateWorkspacePage/ExternalAuth.stories.tsx b/site/src/pages/CreateWorkspacePage/ExternalAuth.stories.tsx index 9c65abc5a0f83..32d114f67df40 100644 --- a/site/src/pages/CreateWorkspacePage/ExternalAuth.stories.tsx +++ b/site/src/pages/CreateWorkspacePage/ExternalAuth.stories.tsx @@ -11,56 +11,64 @@ type Story = StoryObj; export const GithubNotAuthenticated: Story = { args: { - type: "github", + displayIcon: "/icon/github.svg", + displayName: "GitHub", authenticated: false, }, }; export const GithubAuthenticated: Story = { args: { - type: "github", + displayIcon: "/icon/github.svg", + displayName: "GitHub", authenticated: true, }, }; export const GitlabNotAuthenticated: Story = { args: { - type: "gitlab", + displayIcon: "/icon/gitlab.svg", + displayName: "GitLab", authenticated: false, }, }; export const GitlabAuthenticated: Story = { args: { - type: "gitlab", + displayIcon: "/icon/gitlab.svg", + displayName: "GitLab", authenticated: true, }, }; export const AzureDevOpsNotAuthenticated: Story = { args: { - type: "azure-devops", + displayIcon: "/icon/azure-devops.svg", + displayName: "Azure DevOps", authenticated: false, }, }; export const AzureDevOpsAuthenticated: Story = { args: { - type: "azure-devops", + displayIcon: "/icon/azure-devops.svg", + displayName: "Azure DevOps", authenticated: true, }, }; export const BitbucketNotAuthenticated: Story = { args: { - type: "bitbucket", + displayIcon: "/icon/bitbucket.svg", + displayName: "Bitbucket", authenticated: false, }, }; export const BitbucketAuthenticated: Story = { args: { - type: "bitbucket", + displayIcon: "/icon/bitbucket.svg", + displayName: "Bitbucket", authenticated: true, }, }; diff --git a/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx b/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx index 4c685089b7782..f84a835c9f0f0 100644 --- a/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx +++ b/site/src/pages/CreateWorkspacePage/ExternalAuth.tsx @@ -1,21 +1,16 @@ +import ReplayIcon from "@mui/icons-material/Replay"; import Button from "@mui/material/Button"; import FormHelperText from "@mui/material/FormHelperText"; -import { SvgIconProps } from "@mui/material/SvgIcon"; import Tooltip from "@mui/material/Tooltip"; -import GitHub from "@mui/icons-material/GitHub"; -import * as TypesGen from "api/typesGenerated"; -import { AzureDevOpsIcon } from "components/Icons/AzureDevOpsIcon"; -import { BitbucketIcon } from "components/Icons/BitbucketIcon"; -import { GitlabIcon } from "components/Icons/GitlabIcon"; -import { FC } from "react"; import { makeStyles } from "@mui/styles"; -import { type ExternalAuthPollingState } from "./CreateWorkspacePage"; -import { Stack } from "components/Stack/Stack"; -import ReplayIcon from "@mui/icons-material/Replay"; import { LoadingButton } from "components/LoadingButton/LoadingButton"; +import { Stack } from "components/Stack/Stack"; +import { FC } from "react"; +import { type ExternalAuthPollingState } from "./CreateWorkspacePage"; export interface ExternalAuthProps { - type: TypesGen.ExternalAuthProvider; + displayName: string; + displayIcon: string; authenticated: boolean; authenticateURL: string; externalAuthPollingState: ExternalAuthPollingState; @@ -25,7 +20,8 @@ export interface ExternalAuthProps { export const ExternalAuth: FC = (props) => { const { - type, + displayName, + displayIcon, authenticated, authenticateURL, externalAuthPollingState, @@ -37,32 +33,9 @@ export const ExternalAuth: FC = (props) => { error: typeof error !== "undefined", }); - let prettyName: string; - let Icon: (props: SvgIconProps) => JSX.Element; - switch (type) { - case "azure-devops": - prettyName = "Azure DevOps"; - Icon = AzureDevOpsIcon; - break; - case "bitbucket": - prettyName = "Bitbucket"; - Icon = BitbucketIcon; - break; - case "github": - prettyName = "GitHub"; - Icon = GitHub as (props: SvgIconProps) => JSX.Element; - break; - case "gitlab": - prettyName = "GitLab"; - Icon = GitlabIcon; - break; - default: - throw new Error("invalid git provider: " + type); - } - return ( = (props) => { href={authenticateURL} variant="contained" size="large" - startIcon={} + startIcon={ + {`${displayName} + } disabled={authenticated} className={styles.button} color={error ? "error" : undefined} @@ -86,8 +66,8 @@ export const ExternalAuth: FC = (props) => { }} > {authenticated - ? `Authenticated with ${prettyName}` - : `Login with ${prettyName}`} + ? `Authenticated with ${displayName}` + : `Login with ${displayName}`} {externalAuthPollingState === "abandoned" && ( diff --git a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx index 1708841caaca2..7e3b297178180 100644 --- a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx @@ -21,6 +21,8 @@ const meta: Meta = { scopes: [], device_flow: true, device_code_url: "", + display_icon: "", + display_name: "GitHub", }, ], }, diff --git a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx index 2d975245d50d9..b3622f222a139 100644 --- a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx +++ b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.stories.tsx @@ -15,12 +15,12 @@ const Template: StoryFn = (args) => ( export const WebAuthenticated = Template.bind({}); WebAuthenticated.args = { externalAuth: { - type: "BitBucket", authenticated: true, device: false, installations: [], app_install_url: "", app_installable: false, + display_name: "BitBucket", user: { avatar_url: "", login: "kylecarbs", @@ -33,7 +33,7 @@ WebAuthenticated.args = { export const DeviceUnauthenticated = Template.bind({}); DeviceUnauthenticated.args = { externalAuth: { - type: "GitHub", + display_name: "GitHub", authenticated: false, device: true, installations: [], @@ -52,7 +52,7 @@ DeviceUnauthenticated.args = { export const DeviceUnauthenticatedError = Template.bind({}); DeviceUnauthenticatedError.args = { externalAuth: { - type: "GitHub", + display_name: "GitHub", authenticated: false, device: true, installations: [], @@ -76,7 +76,7 @@ export const DeviceAuthenticatedNotInstalled = Template.bind({}); DeviceAuthenticatedNotInstalled.args = { viewExternalAuthConfig: true, externalAuth: { - type: "GitHub", + display_name: "GitHub", authenticated: true, device: true, installations: [], @@ -94,7 +94,7 @@ DeviceAuthenticatedNotInstalled.args = { export const DeviceAuthenticatedInstalled = Template.bind({}); DeviceAuthenticatedInstalled.args = { externalAuth: { - type: "GitHub", + display_name: "GitHub", authenticated: true, device: true, installations: [ diff --git a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx index 3984f35e37b24..38ee0334b251f 100644 --- a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx +++ b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx @@ -35,7 +35,7 @@ const ExternalAuthPageView: FC = ({ if (!externalAuth.authenticated) { return ( - + {externalAuth.device && ( = ({ // We only want to wrap this with a link if an install URL is available! let installTheApp: JSX.Element = ( - <>{`install the ${externalAuth.type} App`} + <>{`install the ${externalAuth.display_name} App`} ); if (externalAuth.app_install_url) { installTheApp = ( @@ -67,12 +67,12 @@ const ExternalAuthPageView: FC = ({ return ( - +

- Hey @{externalAuth.user?.login}! 👋{" "} + {externalAuth.user?.login && `Hey @${externalAuth.user?.login}! 👋 `} {(!externalAuth.app_installable || externalAuth.installations.length > 0) && - "You are now authenticated with Git. Feel free to close this window!"} + "You are now authenticated. Feel free to close this window!"}

{externalAuth.installations.length > 0 && ( @@ -126,7 +126,7 @@ const ExternalAuthPageView: FC = ({ {externalAuth.installations.length > 0 ? "Configure" : "Install"}{" "} - the {externalAuth.type} App + the {externalAuth.display_name} App )} ( - + ( - -); + diff --git a/site/src/components/Icons/BitbucketIcon.tsx b/site/static/icon/bitbucket.svg similarity index 89% rename from site/src/components/Icons/BitbucketIcon.tsx rename to site/static/icon/bitbucket.svg index 8cb419ade8c35..4188ff11f8b00 100644 --- a/site/src/components/Icons/BitbucketIcon.tsx +++ b/site/static/icon/bitbucket.svg @@ -1,7 +1,4 @@ -import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; - -export const BitbucketIcon = (props: SvgIconProps): JSX.Element => ( - + ( - -); + diff --git a/site/static/icon/github.svg b/site/static/icon/github.svg new file mode 100644 index 0000000000000..c679c236fd224 --- /dev/null +++ b/site/static/icon/github.svg @@ -0,0 +1 @@ + diff --git a/site/static/icon/gitlab.svg b/site/static/icon/gitlab.svg new file mode 100644 index 0000000000000..a43690a1c045b --- /dev/null +++ b/site/static/icon/gitlab.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + From 614313b953944640ad44928205594f3b72414c0a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 2 Oct 2023 15:39:46 +0000 Subject: [PATCH 02/19] Refactor external auth config structure for defaults --- cli/cliui/{gitauth.go => externalauth.go} | 8 +- .../{gitauth_test.go => externalauth_test.go} | 5 +- cli/create.go | 2 +- cli/server.go | 8 +- cmd/cliui/main.go | 2 +- .../{config.go => externalauth.go} | 299 +++++++++++++++--- .../{config_test.go => externalauth_test.go} | 22 +- coderd/externalauth/oauth.go | 212 ------------- codersdk/deployment.go | 94 +++--- codersdk/workspaceagents.go | 17 - 10 files changed, 331 insertions(+), 338 deletions(-) rename cli/cliui/{gitauth.go => externalauth.go} (88%) rename cli/cliui/{gitauth_test.go => externalauth_test.go} (89%) rename coderd/externalauth/{config.go => externalauth.go} (57%) rename coderd/externalauth/{config_test.go => externalauth_test.go} (95%) delete mode 100644 coderd/externalauth/oauth.go diff --git a/cli/cliui/gitauth.go b/cli/cliui/externalauth.go similarity index 88% rename from cli/cliui/gitauth.go rename to cli/cliui/externalauth.go index 7c42160da7230..2e416ae3b5825 100644 --- a/cli/cliui/gitauth.go +++ b/cli/cliui/externalauth.go @@ -11,12 +11,12 @@ import ( "github.com/coder/coder/v2/codersdk" ) -type GitAuthOptions struct { +type ExternalAuthOptions struct { Fetch func(context.Context) ([]codersdk.TemplateVersionExternalAuth, error) FetchInterval time.Duration } -func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error { +func ExternalAuth(ctx context.Context, writer io.Writer, opts ExternalAuthOptions) error { if opts.FetchInterval == 0 { opts.FetchInterval = 500 * time.Millisecond } @@ -38,7 +38,7 @@ func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error { return nil } - _, _ = fmt.Fprintf(writer, "You must authenticate with %s to create a workspace with this template. Visit:\n\n\t%s\n\n", auth.Type.Pretty(), auth.AuthenticateURL) + _, _ = fmt.Fprintf(writer, "You must authenticate with %s to create a workspace with this template. Visit:\n\n\t%s\n\n", auth.DisplayName, auth.AuthenticateURL) ticker.Reset(opts.FetchInterval) spin.Start() @@ -66,7 +66,7 @@ func GitAuth(ctx context.Context, writer io.Writer, opts GitAuthOptions) error { } } spin.Stop() - _, _ = fmt.Fprintf(writer, "Successfully authenticated with %s!\n\n", auth.Type.Pretty()) + _, _ = fmt.Fprintf(writer, "Successfully authenticated with %s!\n\n", auth.DisplayName) } return nil } diff --git a/cli/cliui/gitauth_test.go b/cli/cliui/externalauth_test.go similarity index 89% rename from cli/cliui/gitauth_test.go rename to cli/cliui/externalauth_test.go index 3adbfc051032b..5c0a9df2a37d3 100644 --- a/cli/cliui/gitauth_test.go +++ b/cli/cliui/externalauth_test.go @@ -15,7 +15,7 @@ import ( "github.com/coder/coder/v2/testutil" ) -func TestGitAuth(t *testing.T) { +func TestExternalAuth(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) @@ -25,11 +25,12 @@ func TestGitAuth(t *testing.T) { cmd := &clibase.Cmd{ Handler: func(inv *clibase.Invocation) error { var fetched atomic.Bool - return cliui.GitAuth(inv.Context(), inv.Stdout, cliui.GitAuthOptions{ + return cliui.ExternalAuth(inv.Context(), inv.Stdout, cliui.ExternalAuthOptions{ Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) { defer fetched.Store(true) return []codersdk.TemplateVersionExternalAuth{{ ID: "github", + DisplayName: "GitHub", Type: codersdk.ExternalAuthProviderGitHub, Authenticated: fetched.Load(), AuthenticateURL: "https://example.com/gitauth/github", diff --git a/cli/create.go b/cli/create.go index 8c5bc6b3e759c..9511322d55cb6 100644 --- a/cli/create.go +++ b/cli/create.go @@ -265,7 +265,7 @@ func prepWorkspaceBuild(inv *clibase.Invocation, client *codersdk.Client, args p return nil, err } - err = cliui.GitAuth(ctx, inv.Stdout, cliui.GitAuthOptions{ + err = cliui.ExternalAuth(ctx, inv.Stdout, cliui.ExternalAuthOptions{ Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) { return client.TemplateVersionExternalAuth(ctx, templateVersion.ID) }, diff --git a/cli/server.go b/cli/server.go index 0cc065998b7b5..db61e98dc1d78 100644 --- a/cli/server.go +++ b/cli/server.go @@ -101,11 +101,11 @@ import ( // ReadGitAuthProvidersFromEnv is provided for compatibility purposes with the // viper CLI. // DEPRECATED -func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.GitAuthConfig, error) { +func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.ExternalAuthConfig, error) { // The index numbers must be in-order. sort.Strings(environ) - var providers []codersdk.GitAuthConfig + var providers []codersdk.ExternalAuthConfig for _, v := range clibase.ParseEnviron(environ, "CODER_GITAUTH_") { tokens := strings.SplitN(v.Name, "_", 2) if len(tokens) != 2 { @@ -117,7 +117,7 @@ func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.GitAuthConfig, er return nil, xerrors.Errorf("parse number: %s", v.Name) } - var provider codersdk.GitAuthConfig + var provider codersdk.ExternalAuthConfig switch { case len(providers) < providerNum: return nil, xerrors.Errorf( @@ -820,7 +820,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. if vals.Telemetry.Enable { gitAuth := make([]telemetry.GitAuth, 0) // TODO: - var gitAuthConfigs []codersdk.GitAuthConfig + var gitAuthConfigs []codersdk.ExternalAuthConfig for _, cfg := range gitAuthConfigs { gitAuth = append(gitAuth, telemetry.GitAuth{ Type: cfg.Type, diff --git a/cmd/cliui/main.go b/cmd/cliui/main.go index 565815d9448bb..dc7fea241738a 100644 --- a/cmd/cliui/main.go +++ b/cmd/cliui/main.go @@ -331,7 +331,7 @@ func main() { // Complete the auth! gitlabAuthed.Store(true) }() - return cliui.GitAuth(inv.Context(), inv.Stdout, cliui.GitAuthOptions{ + return cliui.ExternalAuth(inv.Context(), inv.Stdout, cliui.ExternalAuthOptions{ Fetch: func(ctx context.Context) ([]codersdk.TemplateVersionExternalAuth, error) { count.Add(1) return []codersdk.TemplateVersionExternalAuth{{ diff --git a/coderd/externalauth/config.go b/coderd/externalauth/externalauth.go similarity index 57% rename from coderd/externalauth/config.go rename to coderd/externalauth/externalauth.go index 2d0c5b3bbc213..6426b2afb6f88 100644 --- a/coderd/externalauth/config.go +++ b/coderd/externalauth/externalauth.go @@ -14,6 +14,7 @@ import ( "golang.org/x/xerrors" "github.com/google/go-github/v43/github" + xgithub "golang.org/x/oauth2/github" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" @@ -246,14 +247,129 @@ func (c *Config) AppInstallations(ctx context.Context, token string) ([]codersdk return installs, true, nil } +type DeviceAuth struct { + ClientID string + TokenURL string + Scopes []string + CodeURL string +} + +// AuthorizeDevice begins the device authorization flow. +// See: https://tools.ietf.org/html/rfc8628#section-3.1 +func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.ExternalAuthDevice, error) { + if c.CodeURL == "" { + return nil, xerrors.New("oauth2: device code URL not set") + } + codeURL, err := c.formatDeviceCodeURL() + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, codeURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + var r struct { + codersdk.ExternalAuthDevice + ErrorDescription string `json:"error_description"` + } + err = json.NewDecoder(resp.Body).Decode(&r) + if err != nil { + return nil, err + } + if r.ErrorDescription != "" { + return nil, xerrors.New(r.ErrorDescription) + } + return &r.ExternalAuthDevice, nil +} + +type ExchangeDeviceCodeResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +// ExchangeDeviceCode exchanges a device code for an access token. +// The boolean returned indicates whether the device code is still pending +// and the caller should try again. +func (c *DeviceAuth) ExchangeDeviceCode(ctx context.Context, deviceCode string) (*oauth2.Token, error) { + if c.TokenURL == "" { + return nil, xerrors.New("oauth2: token URL not set") + } + tokenURL, err := c.formatDeviceTokenURL(deviceCode) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, codersdk.ReadBodyAsError(resp) + } + var body ExchangeDeviceCodeResponse + err = json.NewDecoder(resp.Body).Decode(&body) + if err != nil { + return nil, err + } + if body.Error != "" { + return nil, xerrors.New(body.Error) + } + return &oauth2.Token{ + AccessToken: body.AccessToken, + RefreshToken: body.RefreshToken, + Expiry: dbtime.Now().Add(time.Duration(body.ExpiresIn) * time.Second), + }, nil +} + +func (c *DeviceAuth) formatDeviceTokenURL(deviceCode string) (string, error) { + tok, err := url.Parse(c.TokenURL) + if err != nil { + return "", err + } + tok.RawQuery = url.Values{ + "client_id": {c.ClientID}, + "device_code": {deviceCode}, + "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, + }.Encode() + return tok.String(), nil +} + +func (c *DeviceAuth) formatDeviceCodeURL() (string, error) { + cod, err := url.Parse(c.CodeURL) + if err != nil { + return "", err + } + cod.RawQuery = url.Values{ + "client_id": {c.ClientID}, + "scope": c.Scopes, + }.Encode() + return cod.String(), nil +} + // ConvertConfig converts the SDK configuration entry format // to the parsed and ready-to-consume in coderd provider type. -func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Config, error) { +func ConvertConfig(entries []codersdk.ExternalAuthConfig, accessURL *url.URL) ([]*Config, error) { ids := map[string]struct{}{} configs := []*Config{} for _, entry := range entries { var typ codersdk.ExternalAuthProvider switch codersdk.ExternalAuthProvider(entry.Type) { + case codersdk.ExternalAuthProviderOpenIDConnect: + typ = codersdk.ExternalAuthProviderOpenIDConnect case codersdk.ExternalAuthProviderAzureDevops: typ = codersdk.ExternalAuthProviderAzureDevops case codersdk.ExternalAuthProviderBitBucket: @@ -262,36 +378,41 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con typ = codersdk.ExternalAuthProviderGitHub case codersdk.ExternalAuthProviderGitLab: typ = codersdk.ExternalAuthProviderGitLab - case codersdk.ExternalAuthProviderOpenIDConnect: - typ = codersdk.ExternalAuthProviderOpenIDConnect default: - return nil, xerrors.Errorf("unknown git provider type: %q", entry.Type) + return nil, xerrors.Errorf("unknown external auth provider type: %q", entry.Type) } - if entry.ID == "" { - // Default to the type. - entry.ID = string(typ) - } - if valid := httpapi.NameValid(entry.ID); valid != nil { + + // Applies defaults to the config entry. + // This allows users to very simply state that they type is "GitHub", + // apply their client secret and ID, and have the UI appear nicely. + applyDefaultsToConfig(typ, &entry) + + valid := httpapi.NameValid(entry.ID) + if valid != nil { return nil, xerrors.Errorf("external auth provider %q doesn't have a valid id: %w", entry.ID, valid) } + if entry.ClientID == "" { + return nil, xerrors.Errorf("%q external auth provider: client_id must be provided", entry.ID) + } + if entry.ClientSecret == "" { + return nil, xerrors.Errorf("%q external auth provider: client_secret must be provided", entry.ID) + } _, exists := ids[entry.ID] if exists { if entry.ID == string(typ) { return nil, xerrors.Errorf("multiple %s external auth providers provided. you must specify a unique id for each", typ) } - return nil, xerrors.Errorf("multiple git providers exist with the id %q. specify a unique id for each", entry.ID) + return nil, xerrors.Errorf("multiple external auth providers exist with the id %q. specify a unique id for each", entry.ID) } ids[entry.ID] = struct{}{} - if entry.ClientID == "" { - return nil, xerrors.Errorf("%q external auth provider: client_id must be provided", entry.ID) - } authRedirect, err := accessURL.Parse(fmt.Sprintf("/externalauth/%s/callback", entry.ID)) if err != nil { return nil, xerrors.Errorf("parse externalauth callback url: %w", err) } - regex := regex[typ] + + var regex *regexp.Regexp if entry.Regex != "" { regex, err = regexp.Compile(entry.Regex) if err != nil { @@ -302,25 +423,12 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con oc := &oauth2.Config{ ClientID: entry.ClientID, ClientSecret: entry.ClientSecret, - Endpoint: endpoint[typ], - RedirectURL: authRedirect.String(), - Scopes: scope[typ], - } - - if entry.AuthURL != "" { - oc.Endpoint.AuthURL = entry.AuthURL - } - if entry.TokenURL != "" { - oc.Endpoint.TokenURL = entry.TokenURL - } - if entry.Scopes != nil && len(entry.Scopes) > 0 { - oc.Scopes = entry.Scopes - } - if entry.ValidateURL == "" { - entry.ValidateURL = validateURL[typ] - } - if entry.AppInstallationsURL == "" { - entry.AppInstallationsURL = appInstallationsURL[typ] + Endpoint: oauth2.Endpoint{ + AuthURL: entry.AuthURL, + TokenURL: entry.TokenURL, + }, + RedirectURL: authRedirect.String(), + Scopes: entry.Scopes, } var oauthConfig OAuth2Config = oc @@ -342,14 +450,7 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con DisplayIcon: entry.DisplayIcon, } - if cfg.DisplayName == "" { - cfg.DisplayName = typ.Pretty() - } - if entry.DeviceFlow { - if entry.DeviceCodeURL == "" { - entry.DeviceCodeURL = deviceAuthURL[typ] - } if entry.DeviceCodeURL == "" { return nil, xerrors.Errorf("external auth provider %q: device auth url must be provided", entry.ID) } @@ -365,3 +466,121 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con } return configs, nil } + +// applyDefaultsToConfig applies defaults to the config entry. +func applyDefaultsToConfig(typ codersdk.ExternalAuthProvider, config *codersdk.ExternalAuthConfig) { + defaults := defaults[typ] + if config.AuthURL == "" { + config.AuthURL = defaults.AuthURL + } + if config.TokenURL == "" { + config.TokenURL = defaults.TokenURL + } + if config.ValidateURL == "" { + config.ValidateURL = defaults.ValidateURL + } + if config.AppInstallURL == "" { + config.AppInstallURL = defaults.AppInstallURL + } + if config.AppInstallationsURL == "" { + config.AppInstallationsURL = defaults.AppInstallationsURL + } + if config.Regex == "" { + config.Regex = defaults.Regex + } + if config.Scopes == nil || len(config.Scopes) == 0 { + config.Scopes = defaults.Scopes + } + if config.DeviceCodeURL == "" { + config.DeviceCodeURL = defaults.DeviceCodeURL + } + if config.DisplayName == "" { + config.DisplayName = defaults.DisplayName + } + if config.DisplayIcon == "" { + config.DisplayIcon = defaults.DisplayIcon + } + + // Apply defaults if it's still empty... + if config.ID == "" { + config.ID = string(typ) + } +} + +var defaults = map[codersdk.ExternalAuthProvider]codersdk.ExternalAuthConfig{ + codersdk.ExternalAuthProviderAzureDevops: { + AuthURL: "https://app.vssps.visualstudio.com/oauth2/authorize", + TokenURL: "https://app.vssps.visualstudio.com/oauth2/token", + DisplayName: "Azure DevOps", + DisplayIcon: "/icon/azure-devops.svg", + Regex: `^(https?://)?dev\.azure\.com(/.*)?$`, + Scopes: []string{"vso.code_write"}, + }, + codersdk.ExternalAuthProviderBitBucket: { + AuthURL: "https://bitbucket.org/site/oauth2/authorize", + TokenURL: "https://bitbucket.org/site/oauth2/access_token", + ValidateURL: "https://api.bitbucket.org/2.0/user", + DisplayName: "BitBucket", + DisplayIcon: "/icon/bitbucket.svg", + Regex: `^(https?://)?bitbucket\.org(/.*)?$`, + Scopes: []string{"account", "repository:write"}, + }, + codersdk.ExternalAuthProviderGitLab: { + AuthURL: "https://gitlab.com/oauth/authorize", + TokenURL: "https://gitlab.com/oauth/token", + ValidateURL: "https://gitlab.com/oauth/token/info", + DisplayName: "GitLab", + DisplayIcon: "/icon/gitlab.svg", + Regex: `^(https?://)?gitlab\.com(/.*)?$`, + Scopes: []string{"write_repository"}, + }, + codersdk.ExternalAuthProviderGitHub: { + AuthURL: xgithub.Endpoint.AuthURL, + TokenURL: xgithub.Endpoint.TokenURL, + ValidateURL: "https://api.github.com/user", + DisplayName: "GitHub", + DisplayIcon: "/icon/github.svg", + Regex: `^(https?://)?github\.com(/.*)?$`, + // "workflow" is required for managing GitHub Actions in a repository. + Scopes: []string{"repo", "workflow"}, + DeviceCodeURL: "https://github.com/login/device/code", + AppInstallationsURL: "https://api.github.com/user/installations", + }, + codersdk.ExternalAuthProviderOpenIDConnect: { + DisplayName: "OpenID Connect", + // This is a key emoji. + DisplayIcon: "/emojis/1f511.png", + }, +} + +// jwtConfig is a new OAuth2 config that uses a custom +// assertion method that works with Azure Devops. See: +// https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops +type jwtConfig struct { + *oauth2.Config +} + +func (c *jwtConfig) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { + return c.Config.AuthCodeURL(state, append(opts, oauth2.SetAuthURLParam("response_type", "Assertion"))...) +} + +func (c *jwtConfig) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { + v := url.Values{ + "client_assertion_type": {}, + "client_assertion": {c.ClientSecret}, + "assertion": {code}, + "grant_type": {}, + } + if c.RedirectURL != "" { + v.Set("redirect_uri", c.RedirectURL) + } + return c.Config.Exchange(ctx, code, + append(opts, + oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), + oauth2.SetAuthURLParam("client_assertion", c.ClientSecret), + oauth2.SetAuthURLParam("assertion", code), + oauth2.SetAuthURLParam("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"), + oauth2.SetAuthURLParam("code", ""), + )..., + ) +} diff --git a/coderd/externalauth/config_test.go b/coderd/externalauth/externalauth_test.go similarity index 95% rename from coderd/externalauth/config_test.go rename to coderd/externalauth/externalauth_test.go index 4bd9e0b1628c3..16d1c40e54b95 100644 --- a/coderd/externalauth/config_test.go +++ b/coderd/externalauth/externalauth_test.go @@ -266,41 +266,43 @@ func TestConvertYAML(t *testing.T) { t.Parallel() for _, tc := range []struct { Name string - Input []codersdk.GitAuthConfig + Input []codersdk.ExternalAuthConfig Output []*externalauth.Config Error string }{{ Name: "InvalidType", - Input: []codersdk.GitAuthConfig{{ + Input: []codersdk.ExternalAuthConfig{{ Type: "moo", }}, - Error: "unknown git provider type", + Error: "unknown external auth provider type", }, { Name: "InvalidID", - Input: []codersdk.GitAuthConfig{{ + Input: []codersdk.ExternalAuthConfig{{ Type: string(codersdk.ExternalAuthProviderGitHub), ID: "$hi$", }}, Error: "doesn't have a valid id", }, { Name: "NoClientID", - Input: []codersdk.GitAuthConfig{{ + Input: []codersdk.ExternalAuthConfig{{ Type: string(codersdk.ExternalAuthProviderGitHub), }}, Error: "client_id must be provided", }, { Name: "DuplicateType", - Input: []codersdk.GitAuthConfig{{ + Input: []codersdk.ExternalAuthConfig{{ Type: string(codersdk.ExternalAuthProviderGitHub), ClientID: "example", ClientSecret: "example", }, { - Type: string(codersdk.ExternalAuthProviderGitHub), + Type: string(codersdk.ExternalAuthProviderGitHub), + ClientID: "example-2", + ClientSecret: "example-2", }}, Error: "multiple github external auth providers provided", }, { Name: "InvalidRegex", - Input: []codersdk.GitAuthConfig{{ + Input: []codersdk.ExternalAuthConfig{{ Type: string(codersdk.ExternalAuthProviderGitHub), ClientID: "example", ClientSecret: "example", @@ -309,7 +311,7 @@ func TestConvertYAML(t *testing.T) { Error: "compile regex for external auth provider", }, { Name: "NoDeviceURL", - Input: []codersdk.GitAuthConfig{{ + Input: []codersdk.ExternalAuthConfig{{ Type: string(codersdk.ExternalAuthProviderGitLab), ClientID: "example", ClientSecret: "example", @@ -332,7 +334,7 @@ func TestConvertYAML(t *testing.T) { t.Run("CustomScopesAndEndpoint", func(t *testing.T) { t.Parallel() - config, err := externalauth.ConvertConfig([]codersdk.GitAuthConfig{{ + config, err := externalauth.ConvertConfig([]codersdk.ExternalAuthConfig{{ Type: string(codersdk.ExternalAuthProviderGitLab), ClientID: "id", ClientSecret: "secret", diff --git a/coderd/externalauth/oauth.go b/coderd/externalauth/oauth.go deleted file mode 100644 index 0f679e8fe050e..0000000000000 --- a/coderd/externalauth/oauth.go +++ /dev/null @@ -1,212 +0,0 @@ -package externalauth - -import ( - "context" - "encoding/json" - "net/http" - "net/url" - "regexp" - "time" - - "golang.org/x/oauth2" - "golang.org/x/oauth2/github" - "golang.org/x/xerrors" - - "github.com/coder/coder/v2/coderd/database/dbtime" - "github.com/coder/coder/v2/codersdk" -) - -// endpoint contains default SaaS URLs for each Git provider. -var endpoint = map[codersdk.ExternalAuthProvider]oauth2.Endpoint{ - codersdk.ExternalAuthProviderAzureDevops: { - AuthURL: "https://app.vssps.visualstudio.com/oauth2/authorize", - TokenURL: "https://app.vssps.visualstudio.com/oauth2/token", - }, - codersdk.ExternalAuthProviderBitBucket: { - AuthURL: "https://bitbucket.org/site/oauth2/authorize", - TokenURL: "https://bitbucket.org/site/oauth2/access_token", - }, - codersdk.ExternalAuthProviderGitLab: { - AuthURL: "https://gitlab.com/oauth/authorize", - TokenURL: "https://gitlab.com/oauth/token", - }, - codersdk.ExternalAuthProviderGitHub: github.Endpoint, -} - -// validateURL contains defaults for each provider. -var validateURL = map[codersdk.ExternalAuthProvider]string{ - codersdk.ExternalAuthProviderGitHub: "https://api.github.com/user", - codersdk.ExternalAuthProviderGitLab: "https://gitlab.com/oauth/token/info", - codersdk.ExternalAuthProviderBitBucket: "https://api.bitbucket.org/2.0/user", -} - -var deviceAuthURL = map[codersdk.ExternalAuthProvider]string{ - codersdk.ExternalAuthProviderGitHub: "https://github.com/login/device/code", -} - -var appInstallationsURL = map[codersdk.ExternalAuthProvider]string{ - codersdk.ExternalAuthProviderGitHub: "https://api.github.com/user/installations", -} - -// scope contains defaults for each Git provider. -var scope = map[codersdk.ExternalAuthProvider][]string{ - codersdk.ExternalAuthProviderAzureDevops: {"vso.code_write"}, - codersdk.ExternalAuthProviderBitBucket: {"account", "repository:write"}, - codersdk.ExternalAuthProviderGitLab: {"write_repository"}, - // "workflow" is required for managing GitHub Actions in a repository. - codersdk.ExternalAuthProviderGitHub: {"repo", "workflow"}, -} - -// regex provides defaults for each Git provider to match their SaaS host URL. -// This is configurable by each provider. -var regex = map[codersdk.ExternalAuthProvider]*regexp.Regexp{ - codersdk.ExternalAuthProviderAzureDevops: regexp.MustCompile(`^(https?://)?dev\.azure\.com(/.*)?$`), - codersdk.ExternalAuthProviderBitBucket: regexp.MustCompile(`^(https?://)?bitbucket\.org(/.*)?$`), - codersdk.ExternalAuthProviderGitLab: regexp.MustCompile(`^(https?://)?gitlab\.com(/.*)?$`), - codersdk.ExternalAuthProviderGitHub: regexp.MustCompile(`^(https?://)?github\.com(/.*)?$`), -} - -// jwtConfig is a new OAuth2 config that uses a custom -// assertion method that works with Azure Devops. See: -// https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops -type jwtConfig struct { - *oauth2.Config -} - -func (c *jwtConfig) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string { - return c.Config.AuthCodeURL(state, append(opts, oauth2.SetAuthURLParam("response_type", "Assertion"))...) -} - -func (c *jwtConfig) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) { - v := url.Values{ - "client_assertion_type": {}, - "client_assertion": {c.ClientSecret}, - "assertion": {code}, - "grant_type": {}, - } - if c.RedirectURL != "" { - v.Set("redirect_uri", c.RedirectURL) - } - return c.Config.Exchange(ctx, code, - append(opts, - oauth2.SetAuthURLParam("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), - oauth2.SetAuthURLParam("client_assertion", c.ClientSecret), - oauth2.SetAuthURLParam("assertion", code), - oauth2.SetAuthURLParam("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"), - oauth2.SetAuthURLParam("code", ""), - )..., - ) -} - -type DeviceAuth struct { - ClientID string - TokenURL string - Scopes []string - CodeURL string -} - -// AuthorizeDevice begins the device authorization flow. -// See: https://tools.ietf.org/html/rfc8628#section-3.1 -func (c *DeviceAuth) AuthorizeDevice(ctx context.Context) (*codersdk.ExternalAuthDevice, error) { - if c.CodeURL == "" { - return nil, xerrors.New("oauth2: device code URL not set") - } - codeURL, err := c.formatDeviceCodeURL() - if err != nil { - return nil, err - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, codeURL, nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - var r struct { - codersdk.ExternalAuthDevice - ErrorDescription string `json:"error_description"` - } - err = json.NewDecoder(resp.Body).Decode(&r) - if err != nil { - return nil, err - } - if r.ErrorDescription != "" { - return nil, xerrors.New(r.ErrorDescription) - } - return &r.ExternalAuthDevice, nil -} - -type ExchangeDeviceCodeResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int `json:"expires_in"` - Error string `json:"error"` - ErrorDescription string `json:"error_description"` -} - -// ExchangeDeviceCode exchanges a device code for an access token. -// The boolean returned indicates whether the device code is still pending -// and the caller should try again. -func (c *DeviceAuth) ExchangeDeviceCode(ctx context.Context, deviceCode string) (*oauth2.Token, error) { - if c.TokenURL == "" { - return nil, xerrors.New("oauth2: token URL not set") - } - tokenURL, err := c.formatDeviceTokenURL(deviceCode) - if err != nil { - return nil, err - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, nil) - if err != nil { - return nil, err - } - req.Header.Set("Accept", "application/json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, codersdk.ReadBodyAsError(resp) - } - var body ExchangeDeviceCodeResponse - err = json.NewDecoder(resp.Body).Decode(&body) - if err != nil { - return nil, err - } - if body.Error != "" { - return nil, xerrors.New(body.Error) - } - return &oauth2.Token{ - AccessToken: body.AccessToken, - RefreshToken: body.RefreshToken, - Expiry: dbtime.Now().Add(time.Duration(body.ExpiresIn) * time.Second), - }, nil -} - -func (c *DeviceAuth) formatDeviceTokenURL(deviceCode string) (string, error) { - tok, err := url.Parse(c.TokenURL) - if err != nil { - return "", err - } - tok.RawQuery = url.Values{ - "client_id": {c.ClientID}, - "device_code": {deviceCode}, - "grant_type": {"urn:ietf:params:oauth:grant-type:device_code"}, - }.Encode() - return tok.String(), nil -} - -func (c *DeviceAuth) formatDeviceCodeURL() (string, error) { - cod, err := url.Parse(c.CodeURL) - if err != nil { - return "", err - } - cod.RawQuery = url.Values{ - "client_id": {c.ClientID}, - "scope": c.Scopes, - }.Encode() - return cod.String(), nil -} diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 5b3de8790fcfe..a0cc1bb776e86 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -134,52 +134,52 @@ type DeploymentValues struct { DocsURL clibase.URL `json:"docs_url,omitempty"` RedirectToAccessURL clibase.Bool `json:"redirect_to_access_url,omitempty"` // HTTPAddress is a string because it may be set to zero to disable. - HTTPAddress clibase.String `json:"http_address,omitempty" typescript:",notnull"` - AutobuildPollInterval clibase.Duration `json:"autobuild_poll_interval,omitempty"` - JobHangDetectorInterval clibase.Duration `json:"job_hang_detector_interval,omitempty"` - DERP DERP `json:"derp,omitempty" typescript:",notnull"` - Prometheus PrometheusConfig `json:"prometheus,omitempty" typescript:",notnull"` - Pprof PprofConfig `json:"pprof,omitempty" typescript:",notnull"` - ProxyTrustedHeaders clibase.StringArray `json:"proxy_trusted_headers,omitempty" typescript:",notnull"` - ProxyTrustedOrigins clibase.StringArray `json:"proxy_trusted_origins,omitempty" typescript:",notnull"` - CacheDir clibase.String `json:"cache_directory,omitempty" typescript:",notnull"` - InMemoryDatabase clibase.Bool `json:"in_memory_database,omitempty" typescript:",notnull"` - PostgresURL clibase.String `json:"pg_connection_url,omitempty" typescript:",notnull"` - OAuth2 OAuth2Config `json:"oauth2,omitempty" typescript:",notnull"` - OIDC OIDCConfig `json:"oidc,omitempty" typescript:",notnull"` - Telemetry TelemetryConfig `json:"telemetry,omitempty" typescript:",notnull"` - TLS TLSConfig `json:"tls,omitempty" typescript:",notnull"` - Trace TraceConfig `json:"trace,omitempty" typescript:",notnull"` - SecureAuthCookie clibase.Bool `json:"secure_auth_cookie,omitempty" typescript:",notnull"` - StrictTransportSecurity clibase.Int64 `json:"strict_transport_security,omitempty" typescript:",notnull"` - StrictTransportSecurityOptions clibase.StringArray `json:"strict_transport_security_options,omitempty" typescript:",notnull"` - SSHKeygenAlgorithm clibase.String `json:"ssh_keygen_algorithm,omitempty" typescript:",notnull"` - MetricsCacheRefreshInterval clibase.Duration `json:"metrics_cache_refresh_interval,omitempty" typescript:",notnull"` - AgentStatRefreshInterval clibase.Duration `json:"agent_stat_refresh_interval,omitempty" typescript:",notnull"` - AgentFallbackTroubleshootingURL clibase.URL `json:"agent_fallback_troubleshooting_url,omitempty" typescript:",notnull"` - BrowserOnly clibase.Bool `json:"browser_only,omitempty" typescript:",notnull"` - SCIMAPIKey clibase.String `json:"scim_api_key,omitempty" typescript:",notnull"` - ExternalTokenEncryptionKeys clibase.StringArray `json:"external_token_encryption_keys,omitempty" typescript:",notnull"` - Provisioner ProvisionerConfig `json:"provisioner,omitempty" typescript:",notnull"` - RateLimit RateLimitConfig `json:"rate_limit,omitempty" typescript:",notnull"` - Experiments clibase.StringArray `json:"experiments,omitempty" typescript:",notnull"` - UpdateCheck clibase.Bool `json:"update_check,omitempty" typescript:",notnull"` - MaxTokenLifetime clibase.Duration `json:"max_token_lifetime,omitempty" typescript:",notnull"` - Swagger SwaggerConfig `json:"swagger,omitempty" typescript:",notnull"` - Logging LoggingConfig `json:"logging,omitempty" typescript:",notnull"` - Dangerous DangerousConfig `json:"dangerous,omitempty" typescript:",notnull"` - DisablePathApps clibase.Bool `json:"disable_path_apps,omitempty" typescript:",notnull"` - SessionDuration clibase.Duration `json:"max_session_expiry,omitempty" typescript:",notnull"` - DisableSessionExpiryRefresh clibase.Bool `json:"disable_session_expiry_refresh,omitempty" typescript:",notnull"` - DisablePasswordAuth clibase.Bool `json:"disable_password_auth,omitempty" typescript:",notnull"` - Support SupportConfig `json:"support,omitempty" typescript:",notnull"` - GitAuthProviders clibase.Struct[[]GitAuthConfig] `json:"git_auth,omitempty" typescript:",notnull"` - SSHConfig SSHConfig `json:"config_ssh,omitempty" typescript:",notnull"` - WgtunnelHost clibase.String `json:"wgtunnel_host,omitempty" typescript:",notnull"` - DisableOwnerWorkspaceExec clibase.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"` - ProxyHealthStatusInterval clibase.Duration `json:"proxy_health_status_interval,omitempty" typescript:",notnull"` - EnableTerraformDebugMode clibase.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"` - UserQuietHoursSchedule UserQuietHoursScheduleConfig `json:"user_quiet_hours_schedule,omitempty" typescript:",notnull"` + HTTPAddress clibase.String `json:"http_address,omitempty" typescript:",notnull"` + AutobuildPollInterval clibase.Duration `json:"autobuild_poll_interval,omitempty"` + JobHangDetectorInterval clibase.Duration `json:"job_hang_detector_interval,omitempty"` + DERP DERP `json:"derp,omitempty" typescript:",notnull"` + Prometheus PrometheusConfig `json:"prometheus,omitempty" typescript:",notnull"` + Pprof PprofConfig `json:"pprof,omitempty" typescript:",notnull"` + ProxyTrustedHeaders clibase.StringArray `json:"proxy_trusted_headers,omitempty" typescript:",notnull"` + ProxyTrustedOrigins clibase.StringArray `json:"proxy_trusted_origins,omitempty" typescript:",notnull"` + CacheDir clibase.String `json:"cache_directory,omitempty" typescript:",notnull"` + InMemoryDatabase clibase.Bool `json:"in_memory_database,omitempty" typescript:",notnull"` + PostgresURL clibase.String `json:"pg_connection_url,omitempty" typescript:",notnull"` + OAuth2 OAuth2Config `json:"oauth2,omitempty" typescript:",notnull"` + OIDC OIDCConfig `json:"oidc,omitempty" typescript:",notnull"` + Telemetry TelemetryConfig `json:"telemetry,omitempty" typescript:",notnull"` + TLS TLSConfig `json:"tls,omitempty" typescript:",notnull"` + Trace TraceConfig `json:"trace,omitempty" typescript:",notnull"` + SecureAuthCookie clibase.Bool `json:"secure_auth_cookie,omitempty" typescript:",notnull"` + StrictTransportSecurity clibase.Int64 `json:"strict_transport_security,omitempty" typescript:",notnull"` + StrictTransportSecurityOptions clibase.StringArray `json:"strict_transport_security_options,omitempty" typescript:",notnull"` + SSHKeygenAlgorithm clibase.String `json:"ssh_keygen_algorithm,omitempty" typescript:",notnull"` + MetricsCacheRefreshInterval clibase.Duration `json:"metrics_cache_refresh_interval,omitempty" typescript:",notnull"` + AgentStatRefreshInterval clibase.Duration `json:"agent_stat_refresh_interval,omitempty" typescript:",notnull"` + AgentFallbackTroubleshootingURL clibase.URL `json:"agent_fallback_troubleshooting_url,omitempty" typescript:",notnull"` + BrowserOnly clibase.Bool `json:"browser_only,omitempty" typescript:",notnull"` + SCIMAPIKey clibase.String `json:"scim_api_key,omitempty" typescript:",notnull"` + ExternalTokenEncryptionKeys clibase.StringArray `json:"external_token_encryption_keys,omitempty" typescript:",notnull"` + Provisioner ProvisionerConfig `json:"provisioner,omitempty" typescript:",notnull"` + RateLimit RateLimitConfig `json:"rate_limit,omitempty" typescript:",notnull"` + Experiments clibase.StringArray `json:"experiments,omitempty" typescript:",notnull"` + UpdateCheck clibase.Bool `json:"update_check,omitempty" typescript:",notnull"` + MaxTokenLifetime clibase.Duration `json:"max_token_lifetime,omitempty" typescript:",notnull"` + Swagger SwaggerConfig `json:"swagger,omitempty" typescript:",notnull"` + Logging LoggingConfig `json:"logging,omitempty" typescript:",notnull"` + Dangerous DangerousConfig `json:"dangerous,omitempty" typescript:",notnull"` + DisablePathApps clibase.Bool `json:"disable_path_apps,omitempty" typescript:",notnull"` + SessionDuration clibase.Duration `json:"max_session_expiry,omitempty" typescript:",notnull"` + DisableSessionExpiryRefresh clibase.Bool `json:"disable_session_expiry_refresh,omitempty" typescript:",notnull"` + DisablePasswordAuth clibase.Bool `json:"disable_password_auth,omitempty" typescript:",notnull"` + Support SupportConfig `json:"support,omitempty" typescript:",notnull"` + GitAuthProviders clibase.Struct[[]ExternalAuthConfig] `json:"git_auth,omitempty" typescript:",notnull"` + SSHConfig SSHConfig `json:"config_ssh,omitempty" typescript:",notnull"` + WgtunnelHost clibase.String `json:"wgtunnel_host,omitempty" typescript:",notnull"` + DisableOwnerWorkspaceExec clibase.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"` + ProxyHealthStatusInterval clibase.Duration `json:"proxy_health_status_interval,omitempty" typescript:",notnull"` + EnableTerraformDebugMode clibase.Bool `json:"enable_terraform_debug_mode,omitempty" typescript:",notnull"` + UserQuietHoursSchedule UserQuietHoursScheduleConfig `json:"user_quiet_hours_schedule,omitempty" typescript:",notnull"` Config clibase.YAMLConfigPath `json:"config,omitempty" typescript:",notnull"` WriteConfig clibase.Bool `json:"write_config,omitempty" typescript:",notnull"` @@ -321,7 +321,7 @@ type TraceConfig struct { DataDog clibase.Bool `json:"data_dog" typescript:",notnull"` } -type GitAuthConfig struct { +type ExternalAuthConfig struct { ID string `json:"id"` Type string `json:"type"` ClientID string `json:"client_id"` diff --git a/codersdk/workspaceagents.go b/codersdk/workspaceagents.go index 9c26f8e3746e8..904158b23de3a 100644 --- a/codersdk/workspaceagents.go +++ b/codersdk/workspaceagents.go @@ -748,23 +748,6 @@ func (c *Client) WorkspaceAgentLogsAfter(ctx context.Context, agentID uuid.UUID, // type of providers that are supported within Coder. type ExternalAuthProvider string -func (g ExternalAuthProvider) Pretty() string { - switch g { - case ExternalAuthProviderAzureDevops: - return "Azure DevOps" - case ExternalAuthProviderGitHub: - return "GitHub" - case ExternalAuthProviderGitLab: - return "GitLab" - case ExternalAuthProviderBitBucket: - return "Bitbucket" - case ExternalAuthProviderOpenIDConnect: - return "OpenID Connect" - default: - return string(g) - } -} - const ( ExternalAuthProviderAzureDevops ExternalAuthProvider = "azure-devops" ExternalAuthProviderGitHub ExternalAuthProvider = "github" From c4b0fa02e299b0eee44c1a20f9ea1fcfb83c952c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 2 Oct 2023 16:07:06 +0000 Subject: [PATCH 03/19] Add support for new config properties --- cli/server.go | 189 ++++++++------- cli/server_test.go | 47 +++- coderd/apidoc/docs.go | 121 +++++----- coderd/apidoc/swagger.json | 121 +++++----- coderd/database/dump.sql | 2 +- .../migrations/000158_external_auth.up.sql | 2 + coderd/database/models.go | 2 +- coderd/database/queries.sql.go | 226 +++++++++--------- codersdk/deployment.go | 31 ++- codersdk/deployment_test.go | 6 +- docs/api/general.md | 4 +- docs/api/schemas.md | 213 +++++++++-------- site/src/api/typesGenerated.ts | 40 ++-- 13 files changed, 541 insertions(+), 463 deletions(-) diff --git a/cli/server.go b/cli/server.go index db61e98dc1d78..deb21e549aa82 100644 --- a/cli/server.go +++ b/cli/server.go @@ -98,89 +98,6 @@ import ( "github.com/coder/wgtunnel/tunnelsdk" ) -// ReadGitAuthProvidersFromEnv is provided for compatibility purposes with the -// viper CLI. -// DEPRECATED -func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.ExternalAuthConfig, error) { - // The index numbers must be in-order. - sort.Strings(environ) - - var providers []codersdk.ExternalAuthConfig - for _, v := range clibase.ParseEnviron(environ, "CODER_GITAUTH_") { - tokens := strings.SplitN(v.Name, "_", 2) - if len(tokens) != 2 { - return nil, xerrors.Errorf("invalid env var: %s", v.Name) - } - - providerNum, err := strconv.Atoi(tokens[0]) - if err != nil { - return nil, xerrors.Errorf("parse number: %s", v.Name) - } - - var provider codersdk.ExternalAuthConfig - switch { - case len(providers) < providerNum: - return nil, xerrors.Errorf( - "provider num %v skipped: %s", - len(providers), - v.Name, - ) - case len(providers) == providerNum: - // At the next next provider. - providers = append(providers, provider) - case len(providers) == providerNum+1: - // At the current provider. - provider = providers[providerNum] - } - - key := tokens[1] - switch key { - case "ID": - provider.ID = v.Value - case "TYPE": - provider.Type = v.Value - case "CLIENT_ID": - provider.ClientID = v.Value - case "CLIENT_SECRET": - provider.ClientSecret = v.Value - case "AUTH_URL": - provider.AuthURL = v.Value - case "TOKEN_URL": - provider.TokenURL = v.Value - case "VALIDATE_URL": - provider.ValidateURL = v.Value - case "REGEX": - provider.Regex = v.Value - case "DEVICE_FLOW": - b, err := strconv.ParseBool(v.Value) - if err != nil { - return nil, xerrors.Errorf("parse bool: %s", v.Value) - } - provider.DeviceFlow = b - case "DEVICE_CODE_URL": - provider.DeviceCodeURL = v.Value - case "NO_REFRESH": - b, err := strconv.ParseBool(v.Value) - if err != nil { - return nil, xerrors.Errorf("parse bool: %s", v.Value) - } - provider.NoRefresh = b - case "SCOPES": - provider.Scopes = strings.Split(v.Value, " ") - case "APP_INSTALL_URL": - provider.AppInstallURL = v.Value - case "APP_INSTALLATIONS_URL": - provider.AppInstallationsURL = v.Value - case "DISPLAY_NAME": - provider.DisplayName = v.Value - case "DISPLAY_ICON": - provider.DisplayIcon = v.Value - } - providers[providerNum] = provider - } - return providers, nil -} - func createOIDCConfig(ctx context.Context, vals *codersdk.DeploymentValues) (*coderd.OIDCConfig, error) { if vals.OIDC.ClientID == "" { return nil, xerrors.Errorf("OIDC client ID must be set!") @@ -572,14 +489,14 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. } } - gitAuthEnv, err := ReadGitAuthProvidersFromEnv(os.Environ()) + extAuthEnv, err := ReadExternalAuthProvidersFromEnv(os.Environ()) if err != nil { - return xerrors.Errorf("read git auth providers from env: %w", err) + return xerrors.Errorf("read external auth providers from env: %w", err) } - vals.GitAuthProviders.Value = append(vals.GitAuthProviders.Value, gitAuthEnv...) + vals.ExternalAuthConfigs.Value = append(vals.ExternalAuthConfigs.Value, extAuthEnv...) externalAuthConfigs, err := externalauth.ConvertConfig( - vals.GitAuthProviders.Value, + vals.ExternalAuthConfigs.Value, vals.AccessURL.Value(), ) if err != nil { @@ -2246,3 +2163,101 @@ func ConfigureHTTPServers(inv *clibase.Invocation, cfg *codersdk.DeploymentValue return httpServers, nil } + +// ReadExternalAuthProvidersFromEnv is provided for compatibility purposes with +// the viper CLI. +func ReadExternalAuthProvidersFromEnv(environ []string) ([]codersdk.ExternalAuthConfig, error) { + providers, err := readExternalAuthProvidersFromEnv("CODER_EXTERNAL_AUTH_", environ) + if err != nil { + return nil, err + } + // Deprecated: To support legacy git auth! + gitProviders, err := readExternalAuthProvidersFromEnv("CODER_GITAUTH_", environ) + if err != nil { + return nil, err + } + return append(providers, gitProviders...), nil +} + +// readExternalAuthProvidersFromEnv consumes environment variables to parse +// external auth providers. A prefix is provided to support the legacy +// parsing of `GITAUTH` environment variables. +func readExternalAuthProvidersFromEnv(prefix string, environ []string) ([]codersdk.ExternalAuthConfig, error) { + // The index numbers must be in-order. + sort.Strings(environ) + + var providers []codersdk.ExternalAuthConfig + for _, v := range clibase.ParseEnviron(environ, prefix) { + tokens := strings.SplitN(v.Name, "_", 2) + if len(tokens) != 2 { + return nil, xerrors.Errorf("invalid env var: %s", v.Name) + } + + providerNum, err := strconv.Atoi(tokens[0]) + if err != nil { + return nil, xerrors.Errorf("parse number: %s", v.Name) + } + + var provider codersdk.ExternalAuthConfig + switch { + case len(providers) < providerNum: + return nil, xerrors.Errorf( + "provider num %v skipped: %s", + len(providers), + v.Name, + ) + case len(providers) == providerNum: + // At the next next provider. + providers = append(providers, provider) + case len(providers) == providerNum+1: + // At the current provider. + provider = providers[providerNum] + } + + key := tokens[1] + switch key { + case "ID": + provider.ID = v.Value + case "TYPE": + provider.Type = v.Value + case "CLIENT_ID": + provider.ClientID = v.Value + case "CLIENT_SECRET": + provider.ClientSecret = v.Value + case "AUTH_URL": + provider.AuthURL = v.Value + case "TOKEN_URL": + provider.TokenURL = v.Value + case "VALIDATE_URL": + provider.ValidateURL = v.Value + case "REGEX": + provider.Regex = v.Value + case "DEVICE_FLOW": + b, err := strconv.ParseBool(v.Value) + if err != nil { + return nil, xerrors.Errorf("parse bool: %s", v.Value) + } + provider.DeviceFlow = b + case "DEVICE_CODE_URL": + provider.DeviceCodeURL = v.Value + case "NO_REFRESH": + b, err := strconv.ParseBool(v.Value) + if err != nil { + return nil, xerrors.Errorf("parse bool: %s", v.Value) + } + provider.NoRefresh = b + case "SCOPES": + provider.Scopes = strings.Split(v.Value, " ") + case "APP_INSTALL_URL": + provider.AppInstallURL = v.Value + case "APP_INSTALLATIONS_URL": + provider.AppInstallationsURL = v.Value + case "DISPLAY_NAME": + provider.DisplayName = v.Value + case "DISPLAY_ICON": + provider.DisplayIcon = v.Value + } + providers[providerNum] = provider + } + return providers, nil +} diff --git a/cli/server_test.go b/cli/server_test.go index dbbff56d9ac3d..7034f2fa33d33 100644 --- a/cli/server_test.go +++ b/cli/server_test.go @@ -49,11 +49,50 @@ import ( "github.com/coder/coder/v2/testutil" ) +func TestReadExternalAuthProvidersFromEnv(t *testing.T) { + t.Parallel() + t.Run("Valid", func(t *testing.T) { + t.Parallel() + providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{ + "CODER_EXTERNAL_AUTH_0_ID=1", + "CODER_EXTERNAL_AUTH_0_TYPE=gitlab", + "CODER_EXTERNAL_AUTH_1_ID=2", + "CODER_EXTERNAL_AUTH_1_CLIENT_ID=sid", + "CODER_EXTERNAL_AUTH_1_CLIENT_SECRET=hunter12", + "CODER_EXTERNAL_AUTH_1_TOKEN_URL=google.com", + "CODER_EXTERNAL_AUTH_1_VALIDATE_URL=bing.com", + "CODER_EXTERNAL_AUTH_1_SCOPES=repo:read repo:write", + "CODER_EXTERNAL_AUTH_1_NO_REFRESH=true", + "CODER_EXTERNAL_AUTH_1_DISPLAY_NAME=Google", + "CODER_EXTERNAL_AUTH_1_DISPLAY_ICON=/icon/google.svg", + }) + require.NoError(t, err) + require.Len(t, providers, 2) + + // Validate the first provider. + assert.Equal(t, "1", providers[0].ID) + assert.Equal(t, "gitlab", providers[0].Type) + + // Validate the second provider. + assert.Equal(t, "2", providers[1].ID) + assert.Equal(t, "sid", providers[1].ClientID) + assert.Equal(t, "hunter12", providers[1].ClientSecret) + assert.Equal(t, "google.com", providers[1].TokenURL) + assert.Equal(t, "bing.com", providers[1].ValidateURL) + assert.Equal(t, []string{"repo:read", "repo:write"}, providers[1].Scopes) + assert.Equal(t, true, providers[1].NoRefresh) + assert.Equal(t, "Google", providers[1].DisplayName) + assert.Equal(t, "/icon/google.svg", providers[1].DisplayIcon) + }) +} + +// TestReadGitAuthProvidersFromEnv ensures that the deprecated `CODER_GITAUTH_` +// environment variables are still supported. func TestReadGitAuthProvidersFromEnv(t *testing.T) { t.Parallel() t.Run("Empty", func(t *testing.T) { t.Parallel() - providers, err := cli.ReadGitAuthProvidersFromEnv([]string{ + providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{ "HOME=/home/frodo", }) require.NoError(t, err) @@ -61,7 +100,7 @@ func TestReadGitAuthProvidersFromEnv(t *testing.T) { }) t.Run("InvalidKey", func(t *testing.T) { t.Parallel() - providers, err := cli.ReadGitAuthProvidersFromEnv([]string{ + providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{ "CODER_GITAUTH_XXX=invalid", }) require.Error(t, err, "providers: %+v", providers) @@ -69,7 +108,7 @@ func TestReadGitAuthProvidersFromEnv(t *testing.T) { }) t.Run("SkipKey", func(t *testing.T) { t.Parallel() - providers, err := cli.ReadGitAuthProvidersFromEnv([]string{ + providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{ "CODER_GITAUTH_0_ID=invalid", "CODER_GITAUTH_2_ID=invalid", }) @@ -78,7 +117,7 @@ func TestReadGitAuthProvidersFromEnv(t *testing.T) { }) t.Run("Valid", func(t *testing.T) { t.Parallel() - providers, err := cli.ReadGitAuthProvidersFromEnv([]string{ + providers, err := cli.ReadExternalAuthProvidersFromEnv([]string{ "CODER_GITAUTH_0_ID=1", "CODER_GITAUTH_0_TYPE=gitlab", "CODER_GITAUTH_1_ID=2", diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 69c4863a92f09..70d0ecd4eaaef 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -6725,13 +6725,13 @@ const docTemplate = `{ "clibase.Regexp": { "type": "object" }, - "clibase.Struct-array_codersdk_GitAuthConfig": { + "clibase.Struct-array_codersdk_ExternalAuthConfig": { "type": "object", "properties": { "value": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.GitAuthConfig" + "$ref": "#/definitions/codersdk.ExternalAuthConfig" } } } @@ -7978,15 +7978,15 @@ const docTemplate = `{ "type": "string" } }, + "external_auth": { + "$ref": "#/definitions/clibase.Struct-array_codersdk_ExternalAuthConfig" + }, "external_token_encryption_keys": { "type": "array", "items": { "type": "string" } }, - "git_auth": { - "$ref": "#/definitions/clibase.Struct-array_codersdk_GitAuthConfig" - }, "http_address": { "description": "HTTPAddress is a string because it may be set to zero to disable.", "type": "string" @@ -8237,6 +8237,64 @@ const docTemplate = `{ } } }, + "codersdk.ExternalAuthConfig": { + "type": "object", + "properties": { + "app_install_url": { + "type": "string" + }, + "app_installations_url": { + "type": "string" + }, + "auth_url": { + "type": "string" + }, + "client_id": { + "type": "string" + }, + "device_code_url": { + "type": "string" + }, + "device_flow": { + "type": "boolean" + }, + "display_icon": { + "description": "DisplayIcon is a URL to an icon to display in the UI.", + "type": "string" + }, + "display_name": { + "description": "DisplayName is shown in the UI to identify the auth config.", + "type": "string" + }, + "id": { + "description": "ID is a unique identifier for the auth config.\nIt defaults to ` + "`" + `type` + "`" + ` when not provided.", + "type": "string" + }, + "no_refresh": { + "type": "boolean" + }, + "regex": { + "description": "Regex allows API requesters to match an auth config by\na string (e.g. coder.com) instead of by it's type.\n\nGit clone makes use of this by parsing the URL from:\n'Username for \"https://github.com\":'\nAnd sending it to the Coder server to match against the Regex.", + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "token_url": { + "type": "string" + }, + "type": { + "description": "Type is the type of external auth config.", + "type": "string" + }, + "validate_url": { + "type": "string" + } + } + }, "codersdk.ExternalAuthDevice": { "type": "object", "properties": { @@ -8330,59 +8388,6 @@ const docTemplate = `{ } } }, - "codersdk.GitAuthConfig": { - "type": "object", - "properties": { - "app_install_url": { - "type": "string" - }, - "app_installations_url": { - "type": "string" - }, - "auth_url": { - "type": "string" - }, - "client_id": { - "type": "string" - }, - "device_code_url": { - "type": "string" - }, - "device_flow": { - "type": "boolean" - }, - "display_icon": { - "type": "string" - }, - "display_name": { - "type": "string" - }, - "id": { - "type": "string" - }, - "no_refresh": { - "type": "boolean" - }, - "regex": { - "type": "string" - }, - "scopes": { - "type": "array", - "items": { - "type": "string" - } - }, - "token_url": { - "type": "string" - }, - "type": { - "type": "string" - }, - "validate_url": { - "type": "string" - } - } - }, "codersdk.GitSSHKey": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index eacd7b96075ee..87d0d8d7e1c00 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -5961,13 +5961,13 @@ "clibase.Regexp": { "type": "object" }, - "clibase.Struct-array_codersdk_GitAuthConfig": { + "clibase.Struct-array_codersdk_ExternalAuthConfig": { "type": "object", "properties": { "value": { "type": "array", "items": { - "$ref": "#/definitions/codersdk.GitAuthConfig" + "$ref": "#/definitions/codersdk.ExternalAuthConfig" } } } @@ -7130,15 +7130,15 @@ "type": "string" } }, + "external_auth": { + "$ref": "#/definitions/clibase.Struct-array_codersdk_ExternalAuthConfig" + }, "external_token_encryption_keys": { "type": "array", "items": { "type": "string" } }, - "git_auth": { - "$ref": "#/definitions/clibase.Struct-array_codersdk_GitAuthConfig" - }, "http_address": { "description": "HTTPAddress is a string because it may be set to zero to disable.", "type": "string" @@ -7385,6 +7385,64 @@ } } }, + "codersdk.ExternalAuthConfig": { + "type": "object", + "properties": { + "app_install_url": { + "type": "string" + }, + "app_installations_url": { + "type": "string" + }, + "auth_url": { + "type": "string" + }, + "client_id": { + "type": "string" + }, + "device_code_url": { + "type": "string" + }, + "device_flow": { + "type": "boolean" + }, + "display_icon": { + "description": "DisplayIcon is a URL to an icon to display in the UI.", + "type": "string" + }, + "display_name": { + "description": "DisplayName is shown in the UI to identify the auth config.", + "type": "string" + }, + "id": { + "description": "ID is a unique identifier for the auth config.\nIt defaults to `type` when not provided.", + "type": "string" + }, + "no_refresh": { + "type": "boolean" + }, + "regex": { + "description": "Regex allows API requesters to match an auth config by\na string (e.g. coder.com) instead of by it's type.\n\nGit clone makes use of this by parsing the URL from:\n'Username for \"https://github.com\":'\nAnd sending it to the Coder server to match against the Regex.", + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "token_url": { + "type": "string" + }, + "type": { + "description": "Type is the type of external auth config.", + "type": "string" + }, + "validate_url": { + "type": "string" + } + } + }, "codersdk.ExternalAuthDevice": { "type": "object", "properties": { @@ -7472,59 +7530,6 @@ } } }, - "codersdk.GitAuthConfig": { - "type": "object", - "properties": { - "app_install_url": { - "type": "string" - }, - "app_installations_url": { - "type": "string" - }, - "auth_url": { - "type": "string" - }, - "client_id": { - "type": "string" - }, - "device_code_url": { - "type": "string" - }, - "device_flow": { - "type": "boolean" - }, - "display_icon": { - "type": "string" - }, - "display_name": { - "type": "string" - }, - "id": { - "type": "string" - }, - "no_refresh": { - "type": "boolean" - }, - "regex": { - "type": "string" - }, - "scopes": { - "type": "array", - "items": { - "type": "string" - } - }, - "token_url": { - "type": "string" - }, - "type": { - "type": "string" - }, - "validate_url": { - "type": "string" - } - } - }, "codersdk.GitSSHKey": { "type": "object", "properties": { diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 27fc6815a0d40..4c5aef4ac88c9 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -643,7 +643,7 @@ CREATE TABLE template_versions ( message character varying(1048576) DEFAULT ''::character varying NOT NULL ); -COMMENT ON COLUMN template_versions.external_auth_providers IS 'IDs of Git auth providers for a specific template version'; +COMMENT ON COLUMN template_versions.external_auth_providers IS 'IDs of External auth providers for a specific template version'; COMMENT ON COLUMN template_versions.message IS 'Message describing the changes in this version of the template, similar to a Git commit message. Like a commit message, this should be a short, high-level description of the changes in this version of the template. This message is immutable and should not be updated after the fact.'; diff --git a/coderd/database/migrations/000158_external_auth.up.sql b/coderd/database/migrations/000158_external_auth.up.sql index 3c9b787b232b6..52fc1977e376c 100644 --- a/coderd/database/migrations/000158_external_auth.up.sql +++ b/coderd/database/migrations/000158_external_auth.up.sql @@ -22,4 +22,6 @@ FROM COMMENT ON VIEW template_version_with_user IS 'Joins in the username + avatar url of the created by user.'; +COMMENT ON COLUMN template_versions.external_auth_providers IS 'IDs of External auth providers for a specific template version'; + COMMIT; diff --git a/coderd/database/models.go b/coderd/database/models.go index dda46ce282462..ab6d8861ee434 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1857,7 +1857,7 @@ type TemplateVersionTable struct { Readme string `db:"readme" json:"readme"` JobID uuid.UUID `db:"job_id" json:"job_id"` CreatedBy uuid.UUID `db:"created_by" json:"created_by"` - // IDs of Git auth providers for a specific template version + // IDs of External auth providers for a specific template version ExternalAuthProviders []string `db:"external_auth_providers" json:"external_auth_providers"` // Message describing the changes in this version of the template, similar to a Git commit message. Like a commit message, this should be a short, high-level description of the changes in this version of the template. This message is immutable and should not be updated after the fact. Message string `db:"message" json:"message"` diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 40b7ee7e72715..f1ea7719dfdaf 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9554,6 +9554,119 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } +const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many +SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many +INSERT INTO + workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) +SELECT + $1 :: uuid AS workspace_agent_id, + $2 :: timestamptz AS created_at, + unnest($3 :: uuid [ ]) AS log_source_id, + unnest($4 :: text [ ]) AS log_path, + unnest($5 :: text [ ]) AS script, + unnest($6 :: text [ ]) AS cron, + unnest($7 :: boolean [ ]) AS start_blocks_login, + unnest($8 :: boolean [ ]) AS run_on_start, + unnest($9 :: boolean [ ]) AS run_on_stop, + unnest($10 :: integer [ ]) AS timeout_seconds +RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds +` + +type InsertWorkspaceAgentScriptsParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` + LogPath []string `db:"log_path" json:"log_path"` + Script []string `db:"script" json:"script"` + Cron []string `db:"cron" json:"cron"` + StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` + RunOnStart []bool `db:"run_on_start" json:"run_on_start"` + RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` + TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, + arg.WorkspaceAgentID, + arg.CreatedAt, + pq.Array(arg.LogSourceID), + pq.Array(arg.LogPath), + pq.Array(arg.Script), + pq.Array(arg.Cron), + pq.Array(arg.StartBlocksLogin), + pq.Array(arg.RunOnStart), + pq.Array(arg.RunOnStop), + pq.Array(arg.TimeoutSeconds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT @@ -10502,116 +10615,3 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } - -const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many -SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many -INSERT INTO - workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) -SELECT - $1 :: uuid AS workspace_agent_id, - $2 :: timestamptz AS created_at, - unnest($3 :: uuid [ ]) AS log_source_id, - unnest($4 :: text [ ]) AS log_path, - unnest($5 :: text [ ]) AS script, - unnest($6 :: text [ ]) AS cron, - unnest($7 :: boolean [ ]) AS start_blocks_login, - unnest($8 :: boolean [ ]) AS run_on_start, - unnest($9 :: boolean [ ]) AS run_on_stop, - unnest($10 :: integer [ ]) AS timeout_seconds -RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds -` - -type InsertWorkspaceAgentScriptsParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` - LogPath []string `db:"log_path" json:"log_path"` - Script []string `db:"script" json:"script"` - Cron []string `db:"cron" json:"cron"` - StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` - RunOnStart []bool `db:"run_on_start" json:"run_on_start"` - RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` - TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, - arg.WorkspaceAgentID, - arg.CreatedAt, - pq.Array(arg.LogSourceID), - pq.Array(arg.LogPath), - pq.Array(arg.Script), - pq.Array(arg.Cron), - pq.Array(arg.StartBlocksLogin), - pq.Array(arg.RunOnStart), - pq.Array(arg.RunOnStop), - pq.Array(arg.TimeoutSeconds), - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/codersdk/deployment.go b/codersdk/deployment.go index a0cc1bb776e86..b20d052eb4a9d 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -173,7 +173,7 @@ type DeploymentValues struct { DisableSessionExpiryRefresh clibase.Bool `json:"disable_session_expiry_refresh,omitempty" typescript:",notnull"` DisablePasswordAuth clibase.Bool `json:"disable_password_auth,omitempty" typescript:",notnull"` Support SupportConfig `json:"support,omitempty" typescript:",notnull"` - GitAuthProviders clibase.Struct[[]ExternalAuthConfig] `json:"git_auth,omitempty" typescript:",notnull"` + ExternalAuthConfigs clibase.Struct[[]ExternalAuthConfig] `json:"external_auth,omitempty" typescript:",notnull"` SSHConfig SSHConfig `json:"config_ssh,omitempty" typescript:",notnull"` WgtunnelHost clibase.String `json:"wgtunnel_host,omitempty" typescript:",notnull"` DisableOwnerWorkspaceExec clibase.Bool `json:"disable_owner_workspace_exec,omitempty" typescript:",notnull"` @@ -322,22 +322,33 @@ type TraceConfig struct { } type ExternalAuthConfig struct { + // Type is the type of external auth config. + Type string `json:"type"` + ClientID string `json:"client_id"` + ClientSecret string `json:"-" yaml:"client_secret"` + // ID is a unique identifier for the auth config. + // It defaults to `type` when not provided. ID string `json:"id"` - Type string `json:"type"` - ClientID string `json:"client_id"` - ClientSecret string `json:"-" yaml:"client_secret"` AuthURL string `json:"auth_url"` TokenURL string `json:"token_url"` ValidateURL string `json:"validate_url"` AppInstallURL string `json:"app_install_url"` AppInstallationsURL string `json:"app_installations_url"` - Regex string `json:"regex"` NoRefresh bool `json:"no_refresh"` Scopes []string `json:"scopes"` DeviceFlow bool `json:"device_flow"` DeviceCodeURL string `json:"device_code_url"` - DisplayName string `json:"display_name"` - DisplayIcon string `json:"display_icon"` + // Regex allows API requesters to match an auth config by + // a string (e.g. coder.com) instead of by it's type. + // + // Git clone makes use of this by parsing the URL from: + // 'Username for "https://github.com":' + // And sending it to the Coder server to match against the Regex. + Regex string `json:"regex"` + // DisplayName is shown in the UI to identify the auth config. + DisplayName string `json:"display_name"` + // DisplayIcon is a URL to an icon to display in the UI. + DisplayIcon string `json:"display_icon"` } type ProvisionerConfig struct { @@ -1712,12 +1723,12 @@ Write out the current server config as YAML to stdout.`, }, { // Env handling is done in cli.ReadGitAuthFromEnvironment - Name: "Git Auth Providers", - Description: "Git Authentication providers.", + Name: "External Auth Providers", + Description: "External Authentication providers.", // We need extra scrutiny to ensure this works, is documented, and // tested before enabling. // YAML: "gitAuthProviders", - Value: &c.GitAuthProviders, + Value: &c.ExternalAuthConfigs, Hidden: true, }, { diff --git a/codersdk/deployment_test.go b/codersdk/deployment_test.go index 287e34c741226..7cecc288512ca 100644 --- a/codersdk/deployment_test.go +++ b/codersdk/deployment_test.go @@ -65,9 +65,9 @@ func TestDeploymentValues_HighlyConfigurable(t *testing.T) { flag: true, env: true, }, - "Git Auth Providers": { - // Technically Git Auth Providers can be provided through the env, - // but bypassing clibase. See cli.ReadGitAuthProvidersFromEnv. + "External Auth Providers": { + // Technically External Auth Providers can be provided through the env, + // but bypassing clibase. See cli.ReadExternalAuthProvidersFromEnv. flag: true, env: true, }, diff --git a/docs/api/general.md b/docs/api/general.md index 0bb3dc48d40e6..ad2dcf67f05ca 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -212,8 +212,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ }, "enable_terraform_debug_mode": true, "experiments": ["string"], - "external_token_encryption_keys": ["string"], - "git_auth": { + "external_auth": { "value": [ { "app_install_url": "string", @@ -234,6 +233,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ } ] }, + "external_token_encryption_keys": ["string"], "http_address": "string", "in_memory_database": true, "job_hang_detector_interval": 0, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index f6cfb604254ae..18461c44aa8a9 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -620,7 +620,7 @@ _None_ -## clibase.Struct-array_codersdk_GitAuthConfig +## clibase.Struct-array_codersdk_ExternalAuthConfig ```json { @@ -648,9 +648,9 @@ _None_ ### Properties -| Name | Type | Required | Restrictions | Description | -| ------- | --------------------------------------------------------- | -------- | ------------ | ----------- | -| `value` | array of [codersdk.GitAuthConfig](#codersdkgitauthconfig) | false | | | +| Name | Type | Required | Restrictions | Description | +| ------- | ------------------------------------------------------------------- | -------- | ------------ | ----------- | +| `value` | array of [codersdk.ExternalAuthConfig](#codersdkexternalauthconfig) | false | | | ## clibase.Struct-array_codersdk_LinkConfig @@ -2045,8 +2045,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in }, "enable_terraform_debug_mode": true, "experiments": ["string"], - "external_token_encryption_keys": ["string"], - "git_auth": { + "external_auth": { "value": [ { "app_install_url": "string", @@ -2067,6 +2066,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in } ] }, + "external_token_encryption_keys": ["string"], "http_address": "string", "in_memory_database": true, "job_hang_detector_interval": 0, @@ -2412,8 +2412,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in }, "enable_terraform_debug_mode": true, "experiments": ["string"], - "external_token_encryption_keys": ["string"], - "git_auth": { + "external_auth": { "value": [ { "app_install_url": "string", @@ -2434,6 +2433,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in } ] }, + "external_token_encryption_keys": ["string"], "http_address": "string", "in_memory_database": true, "job_hang_detector_interval": 0, @@ -2608,62 +2608,62 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ### Properties -| Name | Type | Required | Restrictions | Description | -| ------------------------------------ | ------------------------------------------------------------------------------------------ | -------- | ------------ | ------------------------------------------------------------------ | -| `access_url` | [clibase.URL](#clibaseurl) | false | | | -| `address` | [clibase.HostPort](#clibasehostport) | false | | Address Use HTTPAddress or TLS.Address instead. | -| `agent_fallback_troubleshooting_url` | [clibase.URL](#clibaseurl) | false | | | -| `agent_stat_refresh_interval` | integer | false | | | -| `autobuild_poll_interval` | integer | false | | | -| `browser_only` | boolean | false | | | -| `cache_directory` | string | false | | | -| `config` | string | false | | | -| `config_ssh` | [codersdk.SSHConfig](#codersdksshconfig) | false | | | -| `dangerous` | [codersdk.DangerousConfig](#codersdkdangerousconfig) | false | | | -| `derp` | [codersdk.DERP](#codersdkderp) | false | | | -| `disable_owner_workspace_exec` | boolean | false | | | -| `disable_password_auth` | boolean | false | | | -| `disable_path_apps` | boolean | false | | | -| `disable_session_expiry_refresh` | boolean | false | | | -| `docs_url` | [clibase.URL](#clibaseurl) | false | | | -| `enable_terraform_debug_mode` | boolean | false | | | -| `experiments` | array of string | false | | | -| `external_token_encryption_keys` | array of string | false | | | -| `git_auth` | [clibase.Struct-array_codersdk_GitAuthConfig](#clibasestruct-array_codersdk_gitauthconfig) | false | | | -| `http_address` | string | false | | Http address is a string because it may be set to zero to disable. | -| `in_memory_database` | boolean | false | | | -| `job_hang_detector_interval` | integer | false | | | -| `logging` | [codersdk.LoggingConfig](#codersdkloggingconfig) | false | | | -| `max_session_expiry` | integer | false | | | -| `max_token_lifetime` | integer | false | | | -| `metrics_cache_refresh_interval` | integer | false | | | -| `oauth2` | [codersdk.OAuth2Config](#codersdkoauth2config) | false | | | -| `oidc` | [codersdk.OIDCConfig](#codersdkoidcconfig) | false | | | -| `pg_connection_url` | string | false | | | -| `pprof` | [codersdk.PprofConfig](#codersdkpprofconfig) | false | | | -| `prometheus` | [codersdk.PrometheusConfig](#codersdkprometheusconfig) | false | | | -| `provisioner` | [codersdk.ProvisionerConfig](#codersdkprovisionerconfig) | false | | | -| `proxy_health_status_interval` | integer | false | | | -| `proxy_trusted_headers` | array of string | false | | | -| `proxy_trusted_origins` | array of string | false | | | -| `rate_limit` | [codersdk.RateLimitConfig](#codersdkratelimitconfig) | false | | | -| `redirect_to_access_url` | boolean | false | | | -| `scim_api_key` | string | false | | | -| `secure_auth_cookie` | boolean | false | | | -| `ssh_keygen_algorithm` | string | false | | | -| `strict_transport_security` | integer | false | | | -| `strict_transport_security_options` | array of string | false | | | -| `support` | [codersdk.SupportConfig](#codersdksupportconfig) | false | | | -| `swagger` | [codersdk.SwaggerConfig](#codersdkswaggerconfig) | false | | | -| `telemetry` | [codersdk.TelemetryConfig](#codersdktelemetryconfig) | false | | | -| `tls` | [codersdk.TLSConfig](#codersdktlsconfig) | false | | | -| `trace` | [codersdk.TraceConfig](#codersdktraceconfig) | false | | | -| `update_check` | boolean | false | | | -| `user_quiet_hours_schedule` | [codersdk.UserQuietHoursScheduleConfig](#codersdkuserquiethoursscheduleconfig) | false | | | -| `verbose` | boolean | false | | | -| `wgtunnel_host` | string | false | | | -| `wildcard_access_url` | [clibase.URL](#clibaseurl) | false | | | -| `write_config` | boolean | false | | | +| Name | Type | Required | Restrictions | Description | +| ------------------------------------ | ---------------------------------------------------------------------------------------------------- | -------- | ------------ | ------------------------------------------------------------------ | +| `access_url` | [clibase.URL](#clibaseurl) | false | | | +| `address` | [clibase.HostPort](#clibasehostport) | false | | Address Use HTTPAddress or TLS.Address instead. | +| `agent_fallback_troubleshooting_url` | [clibase.URL](#clibaseurl) | false | | | +| `agent_stat_refresh_interval` | integer | false | | | +| `autobuild_poll_interval` | integer | false | | | +| `browser_only` | boolean | false | | | +| `cache_directory` | string | false | | | +| `config` | string | false | | | +| `config_ssh` | [codersdk.SSHConfig](#codersdksshconfig) | false | | | +| `dangerous` | [codersdk.DangerousConfig](#codersdkdangerousconfig) | false | | | +| `derp` | [codersdk.DERP](#codersdkderp) | false | | | +| `disable_owner_workspace_exec` | boolean | false | | | +| `disable_password_auth` | boolean | false | | | +| `disable_path_apps` | boolean | false | | | +| `disable_session_expiry_refresh` | boolean | false | | | +| `docs_url` | [clibase.URL](#clibaseurl) | false | | | +| `enable_terraform_debug_mode` | boolean | false | | | +| `experiments` | array of string | false | | | +| `external_auth` | [clibase.Struct-array_codersdk_ExternalAuthConfig](#clibasestruct-array_codersdk_externalauthconfig) | false | | | +| `external_token_encryption_keys` | array of string | false | | | +| `http_address` | string | false | | Http address is a string because it may be set to zero to disable. | +| `in_memory_database` | boolean | false | | | +| `job_hang_detector_interval` | integer | false | | | +| `logging` | [codersdk.LoggingConfig](#codersdkloggingconfig) | false | | | +| `max_session_expiry` | integer | false | | | +| `max_token_lifetime` | integer | false | | | +| `metrics_cache_refresh_interval` | integer | false | | | +| `oauth2` | [codersdk.OAuth2Config](#codersdkoauth2config) | false | | | +| `oidc` | [codersdk.OIDCConfig](#codersdkoidcconfig) | false | | | +| `pg_connection_url` | string | false | | | +| `pprof` | [codersdk.PprofConfig](#codersdkpprofconfig) | false | | | +| `prometheus` | [codersdk.PrometheusConfig](#codersdkprometheusconfig) | false | | | +| `provisioner` | [codersdk.ProvisionerConfig](#codersdkprovisionerconfig) | false | | | +| `proxy_health_status_interval` | integer | false | | | +| `proxy_trusted_headers` | array of string | false | | | +| `proxy_trusted_origins` | array of string | false | | | +| `rate_limit` | [codersdk.RateLimitConfig](#codersdkratelimitconfig) | false | | | +| `redirect_to_access_url` | boolean | false | | | +| `scim_api_key` | string | false | | | +| `secure_auth_cookie` | boolean | false | | | +| `ssh_keygen_algorithm` | string | false | | | +| `strict_transport_security` | integer | false | | | +| `strict_transport_security_options` | array of string | false | | | +| `support` | [codersdk.SupportConfig](#codersdksupportconfig) | false | | | +| `swagger` | [codersdk.SwaggerConfig](#codersdkswaggerconfig) | false | | | +| `telemetry` | [codersdk.TelemetryConfig](#codersdktelemetryconfig) | false | | | +| `tls` | [codersdk.TLSConfig](#codersdktlsconfig) | false | | | +| `trace` | [codersdk.TraceConfig](#codersdktraceconfig) | false | | | +| `update_check` | boolean | false | | | +| `user_quiet_hours_schedule` | [codersdk.UserQuietHoursScheduleConfig](#codersdkuserquiethoursscheduleconfig) | false | | | +| `verbose` | boolean | false | | | +| `wgtunnel_host` | string | false | | | +| `wildcard_access_url` | [clibase.URL](#clibaseurl) | false | | | +| `write_config` | boolean | false | | | ## codersdk.DisplayApp @@ -2823,6 +2823,49 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `configure_url` | string | false | | | | `id` | integer | false | | | +## codersdk.ExternalAuthConfig + +```json +{ + "app_install_url": "string", + "app_installations_url": "string", + "auth_url": "string", + "client_id": "string", + "device_code_url": "string", + "device_flow": true, + "display_icon": "string", + "display_name": "string", + "id": "string", + "no_refresh": true, + "regex": "string", + "scopes": ["string"], + "token_url": "string", + "type": "string", + "validate_url": "string" +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +| -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------- | ------------ | --------------------------------------------------------------------------------------------------------- | +| `app_install_url` | string | false | | | +| `app_installations_url` | string | false | | | +| `auth_url` | string | false | | | +| `client_id` | string | false | | | +| `device_code_url` | string | false | | | +| `device_flow` | boolean | false | | | +| `display_icon` | string | false | | Display icon is a URL to an icon to display in the UI. | +| `display_name` | string | false | | Display name is shown in the UI to identify the auth config. | +| `id` | string | false | | ID is a unique identifier for the auth config. It defaults to `type` when not provided. | +| `no_refresh` | boolean | false | | | +| `regex` | string | false | | Regex allows API requesters to match an auth config by a string (e.g. coder.com) instead of by it's type. | +| Git clone makes use of this by parsing the URL from: 'Username for "https://github.com":' And sending it to the Coder server to match against the Regex. | +| `scopes` | array of string | false | | | +| `token_url` | string | false | | | +| `type` | string | false | | Type is the type of external auth config. | +| `validate_url` | string | false | | | + ## codersdk.ExternalAuthDevice ```json @@ -2951,48 +2994,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `count` | integer | false | | | | `users` | array of [codersdk.User](#codersdkuser) | false | | | -## codersdk.GitAuthConfig - -```json -{ - "app_install_url": "string", - "app_installations_url": "string", - "auth_url": "string", - "client_id": "string", - "device_code_url": "string", - "device_flow": true, - "display_icon": "string", - "display_name": "string", - "id": "string", - "no_refresh": true, - "regex": "string", - "scopes": ["string"], - "token_url": "string", - "type": "string", - "validate_url": "string" -} -``` - -### Properties - -| Name | Type | Required | Restrictions | Description | -| ----------------------- | --------------- | -------- | ------------ | ----------- | -| `app_install_url` | string | false | | | -| `app_installations_url` | string | false | | | -| `auth_url` | string | false | | | -| `client_id` | string | false | | | -| `device_code_url` | string | false | | | -| `device_flow` | boolean | false | | | -| `display_icon` | string | false | | | -| `display_name` | string | false | | | -| `id` | string | false | | | -| `no_refresh` | boolean | false | | | -| `regex` | string | false | | | -| `scopes` | array of string | false | | | -| `token_url` | string | false | | | -| `type` | string | false | | | -| `validate_url` | string | false | | | - ## codersdk.GitSSHKey ```json diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 5c70b6b2b43cc..c98e74dc9c4e1 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -396,7 +396,7 @@ export interface DeploymentValues { readonly disable_session_expiry_refresh?: boolean; readonly disable_password_auth?: boolean; readonly support?: SupportConfig; - readonly git_auth?: GitAuthConfig[]; + readonly external_auth?: ExternalAuthConfig[]; readonly config_ssh?: SSHConfig; readonly wgtunnel_host?: string; readonly disable_owner_workspace_exec?: boolean; @@ -440,6 +440,25 @@ export interface ExternalAuthAppInstallation { readonly configure_url: string; } +// From codersdk/deployment.go +export interface ExternalAuthConfig { + readonly type: string; + readonly client_id: string; + readonly id: string; + readonly auth_url: string; + readonly token_url: string; + readonly validate_url: string; + readonly app_install_url: string; + readonly app_installations_url: string; + readonly no_refresh: boolean; + readonly scopes: string[]; + readonly device_flow: boolean; + readonly device_code_url: string; + readonly regex: string; + readonly display_name: string; + readonly display_icon: string; +} + // From codersdk/externalauth.go export interface ExternalAuthDevice { readonly device_code: string; @@ -481,25 +500,6 @@ export interface GetUsersResponse { readonly count: number; } -// From codersdk/deployment.go -export interface GitAuthConfig { - readonly id: string; - readonly type: string; - readonly client_id: string; - readonly auth_url: string; - readonly token_url: string; - readonly validate_url: string; - readonly app_install_url: string; - readonly app_installations_url: string; - readonly regex: string; - readonly no_refresh: boolean; - readonly scopes: string[]; - readonly device_flow: boolean; - readonly device_code_url: string; - readonly display_name: string; - readonly display_icon: string; -} - // From codersdk/gitsshkey.go export interface GitSSHKey { readonly user_id: string; From e6c047c043040c1d7bd437212aacc5bad1cfed6e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 2 Oct 2023 16:18:11 +0000 Subject: [PATCH 04/19] Change the name of external auth --- .../{git-providers.md => external-auth.md} | 84 +++++++++--------- docs/manifest.json | 6 +- site/src/AppRouter.tsx | 9 +- .../ExternalAuthSettingsPage.tsx} | 10 +-- .../ExternalAuthSettingsPageView.stories.tsx} | 12 +-- .../ExternalAuthSettingsPageView.tsx} | 24 ++--- .../ExternalAuthPage/ExternalAuthPageView.tsx | 4 +- .../static/{gitauth.mp4 => external-auth.mp4} | Bin 8 files changed, 80 insertions(+), 69 deletions(-) rename docs/admin/{git-providers.md => external-auth.md} (63%) rename site/src/pages/DeploySettingsPage/{GitAuthSettingsPage/GitAuthSettingsPage.tsx => ExternalAuthSettingsPage/ExternalAuthSettingsPage.tsx} (52%) rename site/src/pages/DeploySettingsPage/{GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx => ExternalAuthSettingsPage/ExternalAuthSettingsPageView.stories.tsx} (67%) rename site/src/pages/DeploySettingsPage/{GitAuthSettingsPage/GitAuthSettingsPageView.tsx => ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx} (77%) rename site/static/{gitauth.mp4 => external-auth.mp4} (100%) diff --git a/docs/admin/git-providers.md b/docs/admin/external-auth.md similarity index 63% rename from docs/admin/git-providers.md rename to docs/admin/external-auth.md index 0cbd0e00c94fa..b5a496ef77219 100644 --- a/docs/admin/git-providers.md +++ b/docs/admin/external-auth.md @@ -1,41 +1,45 @@ -# Git Providers +# External Authentication -Coder integrates with git providers to automate away the need for developers to -authenticate with repositories within their workspace. +Coder integrates with Git and OpenID Connect to automate away the need for +developers to authenticate with external services within their workspace. -## How it works +## Git Providers When developers use `git` inside their workspace, they are prompted to authenticate. After that, Coder will store and refresh tokens for future operations. ## Configuration -To add a git provider, you'll need to create an OAuth application. The following -providers are supported: +To add an external authentication provider, you'll need to create an OAuth +application. The following providers are supported: -- [GitHub](#github-app) +- [GitHub](#github) - [GitLab](https://docs.gitlab.com/ee/integration/oauth_provider.html) - [BitBucket](https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/) - [Azure DevOps](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/oauth?view=azure-devops) Example callback URL: -`https://coder.example.com/gitauth/primary-github/callback`. Use an arbitrary ID -for your provider (e.g. `primary-github`). +`https://coder.example.com/external-auth/primary-github/callback`. Use an +arbitrary ID for your provider (e.g. `primary-github`). Set the following environment variables to [configure the Coder server](./configure.md): ```env -CODER_GITAUTH_0_ID="primary-github" -CODER_GITAUTH_0_TYPE=github|gitlab|azure-devops|bitbucket -CODER_GITAUTH_0_CLIENT_ID=xxxxxx -CODER_GITAUTH_0_CLIENT_SECRET=xxxxxxx +CODER_EXTERNAL_AUTH_0_ID="primary-github" +CODER_EXTERNAL_AUTH_0_TYPE=github|gitlab|azure-devops|bitbucket|oidc +CODER_EXTERNAL_AUTH_0_CLIENT_ID=xxxxxx +CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=xxxxxxx + +# Optionally, configure a custom display name and icon +CODER_EXTERNAL_AUTH_0_DISPLAY_NAME="Google Calendar" +CODER_EXTERNAL_AUTH_0_DISPLAY_ICON="https://mycustomicon.com/google.svg" ``` ### GitHub @@ -69,9 +73,9 @@ CODER_GITAUTH_0_CLIENT_SECRET=xxxxxxx GitHub Enterprise requires the following authentication and token URLs: ```env -CODER_GITAUTH_0_VALIDATE_URL="https://github.example.com/login/oauth/access_token/info" -CODER_GITAUTH_0_AUTH_URL="https://github.example.com/login/oauth/authorize" -CODER_GITAUTH_0_TOKEN_URL="https://github.example.com/login/oauth/access_token" +CODER_EXTERNAL_AUTH_0_VALIDATE_URL="https://github.example.com/login/oauth/access_token/info" +CODER_EXTERNAL_AUTH_0_AUTH_URL="https://github.example.com/login/oauth/authorize" +CODER_EXTERNAL_AUTH_0_TOKEN_URL="https://github.example.com/login/oauth/access_token" ``` ### Azure DevOps @@ -79,13 +83,13 @@ CODER_GITAUTH_0_TOKEN_URL="https://github.example.com/login/oauth/access_token" Azure DevOps requires the following environment variables: ```env -CODER_GITAUTH_0_ID="primary-azure-devops" -CODER_GITAUTH_0_TYPE=azure-devops -CODER_GITAUTH_0_CLIENT_ID=xxxxxx +CODER_EXTERNAL_AUTH_0_ID="primary-azure-devops" +CODER_EXTERNAL_AUTH_0_TYPE=azure-devops +CODER_EXTERNAL_AUTH_0_CLIENT_ID=xxxxxx # Ensure this value is your "Client Secret", not "App Secret" -CODER_GITAUTH_0_CLIENT_SECRET=xxxxxxx -CODER_GITAUTH_0_AUTH_URL="https://app.vssps.visualstudio.com/oauth2/authorize" -CODER_GITAUTH_0_TOKEN_URL="https://app.vssps.visualstudio.com/oauth2/token" +CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=xxxxxxx +CODER_EXTERNAL_AUTH_0_AUTH_URL="https://app.vssps.visualstudio.com/oauth2/authorize" +CODER_EXTERNAL_AUTH_0_TOKEN_URL="https://app.vssps.visualstudio.com/oauth2/token" ``` ### Self-managed git providers @@ -94,9 +98,9 @@ Custom authentication and token URLs should be used for self-managed Git provider deployments. ```env -CODER_GITAUTH_0_AUTH_URL="https://github.example.com/oauth/authorize" -CODER_GITAUTH_0_TOKEN_URL="https://github.example.com/oauth/token" -CODER_GITAUTH_0_VALIDATE_URL="https://your-domain.com/oauth/token/info" +CODER_EXTERNAL_AUTH_0_AUTH_URL="https://github.example.com/oauth/authorize" +CODER_EXTERNAL_AUTH_0_TOKEN_URL="https://github.example.com/oauth/token" +CODER_EXTERNAL_AUTH_0_VALIDATE_URL="https://your-domain.com/oauth/token/info" ``` ### Custom scopes @@ -104,7 +108,7 @@ CODER_GITAUTH_0_VALIDATE_URL="https://your-domain.com/oauth/token/info" Optionally, you can request custom scopes: ```env -CODER_GITAUTH_0_SCOPES="repo:read repo:write write:gpg_key" +CODER_EXTERNAL_AUTH_0_SCOPES="repo:read repo:write write:gpg_key" ``` ### Multiple git providers (enterprise) @@ -116,21 +120,21 @@ limit auth scope. Here's a sample config: ```env # Provider 1) github.com -CODER_GITAUTH_0_ID=primary-github -CODER_GITAUTH_0_TYPE=github -CODER_GITAUTH_0_CLIENT_ID=xxxxxx -CODER_GITAUTH_0_CLIENT_SECRET=xxxxxxx -CODER_GITAUTH_0_REGEX=github.com/orgname +CODER_EXTERNAL_AUTH_0_ID=primary-github +CODER_EXTERNAL_AUTH_0_TYPE=github +CODER_EXTERNAL_AUTH_0_CLIENT_ID=xxxxxx +CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=xxxxxxx +CODER_EXTERNAL_AUTH_0_REGEX=github.com/orgname # Provider 2) github.example.com -CODER_GITAUTH_1_ID=secondary-github -CODER_GITAUTH_1_TYPE=github -CODER_GITAUTH_1_CLIENT_ID=xxxxxx -CODER_GITAUTH_1_CLIENT_SECRET=xxxxxxx -CODER_GITAUTH_1_REGEX=github.example.com -CODER_GITAUTH_1_AUTH_URL="https://github.example.com/login/oauth/authorize" -CODER_GITAUTH_1_TOKEN_URL="https://github.example.com/login/oauth/access_token" -CODER_GITAUTH_1_VALIDATE_URL="https://github.example.com/login/oauth/access_token/info" +CODER_EXTERNAL_AUTH_1_ID=secondary-github +CODER_EXTERNAL_AUTH_1_TYPE=github +CODER_EXTERNAL_AUTH_1_CLIENT_ID=xxxxxx +CODER_EXTERNAL_AUTH_1_CLIENT_SECRET=xxxxxxx +CODER_EXTERNAL_AUTH_1_REGEX=github.example.com +CODER_EXTERNAL_AUTH_1_AUTH_URL="https://github.example.com/login/oauth/authorize" +CODER_EXTERNAL_AUTH_1_TOKEN_URL="https://github.example.com/login/oauth/access_token" +CODER_EXTERNAL_AUTH_1_VALIDATE_URL="https://github.example.com/login/oauth/access_token/info" ``` To support regex matching for paths (e.g. github.com/orgname), you'll need to diff --git a/docs/manifest.json b/docs/manifest.json index bfa75ad20d4ce..e6541f5634250 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -308,9 +308,9 @@ "icon_path": "./images/icons/toggle_on.svg" }, { - "title": "Git Providers", - "description": "Learn how connect Coder with external git providers", - "path": "./admin/git-providers.md", + "title": "External Auth", + "description": "Learn how connect Coder with external auth providers", + "path": "./admin/external-auth.md", "icon_path": "./images/icons/git.svg" }, { diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 84cbd8547214b..d27000daa3481 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -110,10 +110,10 @@ const UserAuthSettingsPage = lazy( "./pages/DeploySettingsPage/UserAuthSettingsPage/UserAuthSettingsPage" ), ); -const GitAuthSettingsPage = lazy( +const ExternalAuthSettingsPage = lazy( () => import( - "./pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage" + "./pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPage" ), ); const NetworkSettingsPage = lazy( @@ -292,7 +292,10 @@ export const AppRouter: FC = () => { } /> } /> } /> - } /> + } + /> } diff --git a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage.tsx b/site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPage.tsx similarity index 52% rename from site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage.tsx rename to site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPage.tsx index cc18a803ea48f..0fc166a1a30ab 100644 --- a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPage.tsx @@ -2,20 +2,20 @@ import { useDeploySettings } from "components/DeploySettingsLayout/DeploySetting import { FC } from "react"; import { Helmet } from "react-helmet-async"; import { pageTitle } from "utils/page"; -import { GitAuthSettingsPageView } from "./GitAuthSettingsPageView"; +import { ExternalAuthSettingsPageView } from "./ExternalAuthSettingsPageView"; -const GitAuthSettingsPage: FC = () => { +const ExternalAuthSettingsPage: FC = () => { const { deploymentValues: deploymentValues } = useDeploySettings(); return ( <> - {pageTitle("Git Authentication Settings")} + {pageTitle("External Authentication Settings")} - + ); }; -export default GitAuthSettingsPage; +export default ExternalAuthSettingsPage; diff --git a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.stories.tsx similarity index 67% rename from site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx rename to site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.stories.tsx index 7e3b297178180..cbcb29fd141aa 100644 --- a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.stories.tsx @@ -1,12 +1,12 @@ -import { GitAuthSettingsPageView } from "./GitAuthSettingsPageView"; +import { ExternalAuthSettingsPageView } from "./ExternalAuthSettingsPageView"; import type { Meta, StoryObj } from "@storybook/react"; -const meta: Meta = { - title: "pages/GitAuthSettingsPageView", - component: GitAuthSettingsPageView, +const meta: Meta = { + title: "pages/ExternalAuthSettingsPageView", + component: ExternalAuthSettingsPageView, args: { config: { - git_auth: [ + external_auth: [ { id: "0000-1111", type: "GitHub", @@ -30,6 +30,6 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Page: Story = {}; diff --git a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx similarity index 77% rename from site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.tsx rename to site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx index e56634a93f39f..d3172f05c97a6 100644 --- a/site/src/pages/DeploySettingsPage/GitAuthSettingsPage/GitAuthSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/ExternalAuthSettingsPage/ExternalAuthSettingsPageView.tsx @@ -5,27 +5,27 @@ import TableCell from "@mui/material/TableCell"; import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; -import { DeploymentValues, GitAuthConfig } from "api/typesGenerated"; +import { DeploymentValues, ExternalAuthConfig } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; import { EnterpriseBadge } from "components/DeploySettingsLayout/Badges"; import { Header } from "components/DeploySettingsLayout/Header"; import { docs } from "utils/docs"; -export type GitAuthSettingsPageViewProps = { +export type ExternalAuthSettingsPageViewProps = { config: DeploymentValues; }; -export const GitAuthSettingsPageView = ({ +export const ExternalAuthSettingsPageView = ({ config, -}: GitAuthSettingsPageViewProps): JSX.Element => { +}: ExternalAuthSettingsPageViewProps): JSX.Element => { const styles = useStyles(); return ( <>