mirror of https://github.com/coder/coder.git
feat: allow external services to be authable (#9996)
* feat: allow external services to be authable * Refactor external auth config structure for defaults * Add support for new config properties * Change the name of external auth * Move externalauth -> external-auth * Run gen * Fix tests * Fix MW tests * Fix git auth redirect * Fix lint * Fix name * Allow any ID * Fix invalid type test * Fix e2e tests * Fix comments * Fix colors * Allow accepting any type as string * Run gen * Fix href
This commit is contained in:
parent
f62f45a303
commit
45b53c285f
|
@ -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
|
||||
}
|
|
@ -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,12 +25,13 @@ 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",
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
DisplayName: "GitHub",
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
Authenticated: fetched.Load(),
|
||||
AuthenticateURL: "https://example.com/gitauth/github",
|
||||
}}, nil
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -613,7 +613,8 @@ func TestCreateWithGitAuth(t *testing.T) {
|
|||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
DisplayName: "GitHub",
|
||||
}},
|
||||
IncludeProvisionerDaemon: true,
|
||||
})
|
||||
|
|
187
cli/server.go
187
cli/server.go
|
@ -98,85 +98,6 @@ import (
|
|||
"github.com/coder/wgtunnel/tunnelsdk"
|
||||
)
|
||||
|
||||
// ReadGitAuthProvidersFromEnv is provided for compatibility purposes with the
|
||||
// viper CLI.
|
||||
// DEPRECATED
|
||||
func ReadGitAuthProvidersFromEnv(environ []string) ([]codersdk.GitAuthConfig, error) {
|
||||
// The index numbers must be in-order.
|
||||
sort.Strings(environ)
|
||||
|
||||
var providers []codersdk.GitAuthConfig
|
||||
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.GitAuthConfig
|
||||
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
|
||||
}
|
||||
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!")
|
||||
|
@ -568,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 {
|
||||
|
@ -816,7 +737,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,
|
||||
|
@ -2242,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 := parseExternalAuthProvidersFromEnv("CODER_EXTERNAL_AUTH_", environ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Deprecated: To support legacy git auth!
|
||||
gitProviders, err := parseExternalAuthProvidersFromEnv("CODER_GITAUTH_", environ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(providers, gitProviders...), nil
|
||||
}
|
||||
|
||||
// parseExternalAuthProvidersFromEnv consumes environment variables to parse
|
||||
// external auth providers. A prefix is provided to support the legacy
|
||||
// parsing of `GITAUTH` environment variables.
|
||||
func parseExternalAuthProvidersFromEnv(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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -331,17 +331,17 @@ 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{{
|
||||
ID: "github",
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
Authenticated: githubAuthed.Load(),
|
||||
AuthenticateURL: "https://example.com/gitauth/github?redirect=" + url.QueryEscape("/gitauth?notify"),
|
||||
}, {
|
||||
ID: "gitlab",
|
||||
Type: codersdk.ExternalAuthProviderGitLab,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitLab.String(),
|
||||
Authenticated: gitlabAuthed.Load(),
|
||||
AuthenticateURL: "https://example.com/gitauth/gitlab?redirect=" + url.QueryEscape("/gitauth?notify"),
|
||||
}}, nil
|
||||
|
|
|
@ -602,7 +602,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/externalauth/{externalauth}": {
|
||||
"/external-auth/{externalauth}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
|
@ -637,7 +637,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/externalauth/{externalauth}/device": {
|
||||
"/external-auth/{externalauth}/device": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
|
@ -2768,7 +2768,7 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/externalauth": {
|
||||
"/templateversions/{templateversion}/external-auth": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
|
@ -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"
|
||||
|
@ -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": [
|
||||
|
@ -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": {
|
||||
|
@ -8257,23 +8315,6 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuthProvider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"azure-devops",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"openid-connect"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ExternalAuthProviderAzureDevops",
|
||||
"ExternalAuthProviderGitHub",
|
||||
"ExternalAuthProviderGitLab",
|
||||
"ExternalAuthProviderBitBucket",
|
||||
"ExternalAuthProviderOpenIDConnect"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuthUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -8330,53 +8371,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"
|
||||
},
|
||||
"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": {
|
||||
|
@ -10018,11 +10012,17 @@ const docTemplate = `{
|
|||
"authenticated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthProvider"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -512,7 +512,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/externalauth/{externalauth}": {
|
||||
"/external-auth/{externalauth}": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
|
@ -543,7 +543,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/externalauth/{externalauth}/device": {
|
||||
"/external-auth/{externalauth}/device": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
|
@ -2430,7 +2430,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/templateversions/{templateversion}/externalauth": {
|
||||
"/templateversions/{templateversion}/external-auth": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
|
@ -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"
|
||||
|
@ -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": [
|
||||
|
@ -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": {
|
||||
|
@ -7405,23 +7463,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.ExternalAuthProvider": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"azure-devops",
|
||||
"github",
|
||||
"gitlab",
|
||||
"bitbucket",
|
||||
"openid-connect"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"ExternalAuthProviderAzureDevops",
|
||||
"ExternalAuthProviderGitHub",
|
||||
"ExternalAuthProviderGitLab",
|
||||
"ExternalAuthProviderBitBucket",
|
||||
"ExternalAuthProviderOpenIDConnect"
|
||||
]
|
||||
},
|
||||
"codersdk.ExternalAuthUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -7478,53 +7519,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"
|
||||
},
|
||||
"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": {
|
||||
|
@ -9065,11 +9059,17 @@
|
|||
"authenticated": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"display_icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"display_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/definitions/codersdk.ExternalAuthProvider"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -547,7 +547,7 @@ func New(options *Options) *API {
|
|||
|
||||
// Register callback handlers for each OAuth2 provider.
|
||||
// We must support gitauth and externalauth for backwards compatibility.
|
||||
for _, route := range []string{"gitauth", "externalauth"} {
|
||||
for _, route := range []string{"gitauth", "external-auth"} {
|
||||
r.Route("/"+route, func(r chi.Router) {
|
||||
for _, externalAuthConfig := range options.ExternalAuthConfigs {
|
||||
// We don't need to register a callback handler for device auth.
|
||||
|
@ -616,7 +616,7 @@ func New(options *Options) *API {
|
|||
r.Get("/{fileID}", api.fileByID)
|
||||
r.Post("/", api.postFile)
|
||||
})
|
||||
r.Route("/externalauth/{externalauth}", func(r chi.Router) {
|
||||
r.Route("/external-auth/{externalauth}", func(r chi.Router) {
|
||||
r.Use(
|
||||
apiKeyMiddleware,
|
||||
httpmw.ExtractExternalAuthParam(options.ExternalAuthConfigs),
|
||||
|
@ -689,7 +689,7 @@ func New(options *Options) *API {
|
|||
r.Get("/schema", templateVersionSchemaDeprecated)
|
||||
r.Get("/parameters", templateVersionParametersDeprecated)
|
||||
r.Get("/rich-parameters", api.templateVersionRichParameters)
|
||||
r.Get("/externalauth", api.templateVersionExternalAuth)
|
||||
r.Get("/external-auth", api.templateVersionExternalAuth)
|
||||
r.Get("/variables", api.templateVersionVariables)
|
||||
r.Get("/resources", api.templateVersionResources)
|
||||
r.Get("/logs", api.templateVersionLogs)
|
||||
|
|
|
@ -906,7 +906,7 @@ func RequestExternalAuthCallback(t *testing.T, providerID string, client *coders
|
|||
return http.ErrUseLastResponse
|
||||
}
|
||||
state := "somestate"
|
||||
oauthURL, err := client.URL.Parse(fmt.Sprintf("/externalauth/%s/callback?code=asd&state=%s", providerID, state))
|
||||
oauthURL, err := client.URL.Parse(fmt.Sprintf("/external-auth/%s/callback?code=asd&state=%s", providerID, state))
|
||||
require.NoError(t, err)
|
||||
req, err := http.NewRequestWithContext(context.Background(), "GET", oauthURL.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -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.';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
// @Tags Git
|
||||
// @Param externalauth path string true "Git Provider ID" format(string)
|
||||
// @Success 200 {object} codersdk.ExternalAuth
|
||||
// @Router /externalauth/{externalauth} [get]
|
||||
// @Router /external-auth/{externalauth} [get]
|
||||
func (api *API) externalAuthByID(w http.ResponseWriter, r *http.Request) {
|
||||
config := httpmw.ExternalAuthParam(r)
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
@ -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{},
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ func (api *API) externalAuthByID(w http.ResponseWriter, r *http.Request) {
|
|||
// @Tags Git
|
||||
// @Param externalauth path string true "External Provider ID" format(string)
|
||||
// @Success 204
|
||||
// @Router /externalauth/{externalauth}/device [post]
|
||||
// @Router /external-auth/{externalauth}/device [post]
|
||||
func (api *API) postExternalAuthDeviceByID(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
@ -169,7 +169,7 @@ func (api *API) postExternalAuthDeviceByID(rw http.ResponseWriter, r *http.Reque
|
|||
// @Tags Git
|
||||
// @Param externalauth path string true "Git Provider ID" format(string)
|
||||
// @Success 200 {object} codersdk.ExternalAuthDevice
|
||||
// @Router /externalauth/{externalauth}/device [get]
|
||||
// @Router /external-auth/{externalauth}/device [get]
|
||||
func (*API) externalAuthDeviceByID(rw http.ResponseWriter, r *http.Request) {
|
||||
config := httpmw.ExternalAuthParam(r)
|
||||
ctx := r.Context()
|
||||
|
@ -255,7 +255,7 @@ func (api *API) externalAuthCallback(externalAuthConfig *externalauth.Config) ht
|
|||
redirect := state.Redirect
|
||||
if redirect == "" {
|
||||
// This is a nicely rendered screen on the frontend
|
||||
redirect = fmt.Sprintf("/externalauth/%s", externalAuthConfig.ID)
|
||||
redirect = fmt.Sprintf("/external-auth/%s", externalAuthConfig.ID)
|
||||
}
|
||||
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,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"
|
||||
|
@ -35,9 +36,13 @@ type Config struct {
|
|||
// ID is a unique identifier for the authenticator.
|
||||
ID string
|
||||
// Type is the type of provider.
|
||||
Type codersdk.ExternalAuthProvider
|
||||
Type string
|
||||
// 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.
|
||||
|
@ -113,7 +118,7 @@ validate:
|
|||
// to the read replica in time.
|
||||
//
|
||||
// We do an exponential backoff here to give the write time to propagate.
|
||||
if c.Type == codersdk.ExternalAuthProviderGitHub && r.Wait(retryCtx) {
|
||||
if c.Type == string(codersdk.EnhancedExternalAuthProviderGitHub) && r.Wait(retryCtx) {
|
||||
goto validate
|
||||
}
|
||||
// The token is no longer valid!
|
||||
|
@ -171,7 +176,7 @@ func (c *Config) ValidateToken(ctx context.Context, token string) (bool, *coders
|
|||
}
|
||||
|
||||
var user *codersdk.ExternalAuthUser
|
||||
if c.Type == codersdk.ExternalAuthProviderGitHub {
|
||||
if c.Type == string(codersdk.EnhancedExternalAuthProviderGitHub) {
|
||||
var ghUser github.User
|
||||
err = json.NewDecoder(res.Body).Decode(&ghUser)
|
||||
if err == nil {
|
||||
|
@ -217,7 +222,7 @@ func (c *Config) AppInstallations(ctx context.Context, token string) ([]codersdk
|
|||
return nil, false, nil
|
||||
}
|
||||
installs := []codersdk.ExternalAuthAppInstallation{}
|
||||
if c.Type == codersdk.ExternalAuthProviderGitHub {
|
||||
if c.Type == string(codersdk.EnhancedExternalAuthProviderGitHub) {
|
||||
var ghInstalls struct {
|
||||
Installations []*github.Installation `json:"installations"`
|
||||
}
|
||||
|
@ -245,50 +250,158 @@ 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.ExternalAuthProviderAzureDevops:
|
||||
typ = codersdk.ExternalAuthProviderAzureDevops
|
||||
case codersdk.ExternalAuthProviderBitBucket:
|
||||
typ = codersdk.ExternalAuthProviderBitBucket
|
||||
case codersdk.ExternalAuthProviderGitHub:
|
||||
typ = codersdk.ExternalAuthProviderGitHub
|
||||
case codersdk.ExternalAuthProviderGitLab:
|
||||
typ = codersdk.ExternalAuthProviderGitLab
|
||||
default:
|
||||
return nil, xerrors.Errorf("unknown git provider type: %q", entry.Type)
|
||||
}
|
||||
if entry.ID == "" {
|
||||
// Default to the type.
|
||||
entry.ID = string(typ)
|
||||
}
|
||||
if valid := httpapi.NameValid(entry.ID); valid != nil {
|
||||
entry := entry
|
||||
|
||||
// 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(&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)
|
||||
if entry.ID == entry.Type {
|
||||
return nil, xerrors.Errorf("multiple %s external auth providers provided. you must specify a unique id for each", entry.Type)
|
||||
}
|
||||
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))
|
||||
authRedirect, err := accessURL.Parse(fmt.Sprintf("/external-auth/%s/callback", entry.ID))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse externalauth callback url: %w", err)
|
||||
return nil, xerrors.Errorf("parse external auth callback url: %w", err)
|
||||
}
|
||||
regex := regex[typ]
|
||||
|
||||
var regex *regexp.Regexp
|
||||
if entry.Regex != "" {
|
||||
regex, err = regexp.Compile(entry.Regex)
|
||||
if err != nil {
|
||||
|
@ -299,30 +412,17 @@ 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
|
||||
// Azure DevOps uses JWT token authentication!
|
||||
if typ == codersdk.ExternalAuthProviderAzureDevops {
|
||||
if entry.Type == string(codersdk.EnhancedExternalAuthProviderAzureDevops) {
|
||||
oauthConfig = &jwtConfig{oc}
|
||||
}
|
||||
|
||||
|
@ -330,17 +430,16 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con
|
|||
OAuth2Config: oauthConfig,
|
||||
ID: entry.ID,
|
||||
Regex: regex,
|
||||
Type: typ,
|
||||
Type: entry.Type,
|
||||
NoRefresh: entry.NoRefresh,
|
||||
ValidateURL: entry.ValidateURL,
|
||||
AppInstallationsURL: entry.AppInstallationsURL,
|
||||
AppInstallURL: entry.AppInstallURL,
|
||||
DisplayName: entry.DisplayName,
|
||||
DisplayIcon: entry.DisplayIcon,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
@ -356,3 +455,123 @@ func ConvertConfig(entries []codersdk.GitAuthConfig, accessURL *url.URL) ([]*Con
|
|||
}
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
// applyDefaultsToConfig applies defaults to the config entry.
|
||||
func applyDefaultsToConfig(config *codersdk.ExternalAuthConfig) {
|
||||
defaults := defaults[codersdk.EnhancedExternalAuthProvider(config.Type)]
|
||||
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 = config.Type
|
||||
}
|
||||
if config.DisplayName == "" {
|
||||
config.DisplayName = config.Type
|
||||
}
|
||||
if config.DisplayIcon == "" {
|
||||
// This is a key emoji.
|
||||
config.DisplayIcon = "/emojis/1f511.png"
|
||||
}
|
||||
}
|
||||
|
||||
var defaults = map[codersdk.EnhancedExternalAuthProvider]codersdk.ExternalAuthConfig{
|
||||
codersdk.EnhancedExternalAuthProviderAzureDevops: {
|
||||
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.EnhancedExternalAuthProviderBitBucket: {
|
||||
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.EnhancedExternalAuthProviderGitLab: {
|
||||
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.EnhancedExternalAuthProviderGitHub: {
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
// 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", ""),
|
||||
)...,
|
||||
)
|
||||
}
|
|
@ -176,7 +176,7 @@ func TestRefreshToken(t *testing.T) {
|
|||
}),
|
||||
},
|
||||
GitConfigOpt: func(cfg *externalauth.Config) {
|
||||
cfg.Type = codersdk.ExternalAuthProviderGitHub
|
||||
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -206,7 +206,7 @@ func TestRefreshToken(t *testing.T) {
|
|||
}),
|
||||
},
|
||||
GitConfigOpt: func(cfg *externalauth.Config) {
|
||||
cfg.Type = codersdk.ExternalAuthProviderGitHub
|
||||
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -237,7 +237,7 @@ func TestRefreshToken(t *testing.T) {
|
|||
}),
|
||||
},
|
||||
GitConfigOpt: func(cfg *externalauth.Config) {
|
||||
cfg.Type = codersdk.ExternalAuthProviderGitHub
|
||||
cfg.Type = codersdk.EnhancedExternalAuthProviderGitHub.String()
|
||||
},
|
||||
DB: db,
|
||||
})
|
||||
|
@ -266,42 +266,38 @@ 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{{
|
||||
Type: "moo",
|
||||
}},
|
||||
Error: "unknown git provider type",
|
||||
}, {
|
||||
Name: "InvalidID",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
ID: "$hi$",
|
||||
}},
|
||||
Error: "doesn't have a valid id",
|
||||
}, {
|
||||
Name: "NoClientID",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
}},
|
||||
Error: "client_id must be provided",
|
||||
}, {
|
||||
Name: "DuplicateType",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
ClientID: "example",
|
||||
ClientSecret: "example",
|
||||
}, {
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
ClientID: "example-2",
|
||||
ClientSecret: "example-2",
|
||||
}},
|
||||
Error: "multiple github external auth providers provided",
|
||||
}, {
|
||||
Name: "InvalidRegex",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitHub),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitHub),
|
||||
ClientID: "example",
|
||||
ClientSecret: "example",
|
||||
Regex: `\K`,
|
||||
|
@ -309,8 +305,8 @@ func TestConvertYAML(t *testing.T) {
|
|||
Error: "compile regex for external auth provider",
|
||||
}, {
|
||||
Name: "NoDeviceURL",
|
||||
Input: []codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitLab),
|
||||
Input: []codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
ClientID: "example",
|
||||
ClientSecret: "example",
|
||||
DeviceFlow: true,
|
||||
|
@ -332,8 +328,8 @@ func TestConvertYAML(t *testing.T) {
|
|||
|
||||
t.Run("CustomScopesAndEndpoint", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
config, err := externalauth.ConvertConfig([]codersdk.GitAuthConfig{{
|
||||
Type: string(codersdk.ExternalAuthProviderGitLab),
|
||||
config, err := externalauth.ConvertConfig([]codersdk.ExternalAuthConfig{{
|
||||
Type: string(codersdk.EnhancedExternalAuthProviderGitLab),
|
||||
ClientID: "id",
|
||||
ClientSecret: "secret",
|
||||
AuthURL: "https://auth.com",
|
||||
|
@ -341,7 +337,7 @@ func TestConvertYAML(t *testing.T) {
|
|||
Scopes: []string{"read"},
|
||||
}}, &url.URL{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "https://auth.com?client_id=id&redirect_uri=%2Fexternalauth%2Fgitlab%2Fcallback&response_type=code&scope=read", config[0].AuthCodeURL(""))
|
||||
require.Equal(t, "https://auth.com?client_id=id&redirect_uri=%2Fexternal-auth%2Fgitlab%2Fcallback&response_type=code&scope=read", config[0].AuthCodeURL(""))
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -34,7 +34,7 @@ func TestExternalAuthByID(t *testing.T) {
|
|||
ExternalAuthConfigs: []*externalauth.Config{{
|
||||
ID: "test",
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -51,7 +51,7 @@ func TestExternalAuthByID(t *testing.T) {
|
|||
ID: "test",
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
// AzureDevops doesn't have a user endpoint!
|
||||
Type: codersdk.ExternalAuthProviderAzureDevops,
|
||||
Type: codersdk.EnhancedExternalAuthProviderAzureDevops.String(),
|
||||
}},
|
||||
})
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -75,7 +75,7 @@ func TestExternalAuthByID(t *testing.T) {
|
|||
ID: "test",
|
||||
ValidateURL: validateSrv.URL,
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -116,7 +116,7 @@ func TestExternalAuthByID(t *testing.T) {
|
|||
ValidateURL: srv.URL + "/user",
|
||||
AppInstallationsURL: srv.URL + "/installs",
|
||||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -249,7 +249,7 @@ func TestGitAuthCallback(t *testing.T) {
|
|||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -268,7 +268,7 @@ func TestGitAuthCallback(t *testing.T) {
|
|||
agentClient.SetSessionToken(authToken)
|
||||
token, err := agentClient.GitAuth(context.Background(), "github.com/asd/asd", false)
|
||||
require.NoError(t, err)
|
||||
require.True(t, strings.HasSuffix(token.URL, fmt.Sprintf("/externalauth/%s", "github")))
|
||||
require.True(t, strings.HasSuffix(token.URL, fmt.Sprintf("/external-auth/%s", "github")), token.URL)
|
||||
})
|
||||
t.Run("UnauthorizedCallback", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -278,7 +278,7 @@ func TestGitAuthCallback(t *testing.T) {
|
|||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
resp := coderdtest.RequestExternalAuthCallback(t, "github", client)
|
||||
|
@ -292,7 +292,7 @@ func TestGitAuthCallback(t *testing.T) {
|
|||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
_ = coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -300,7 +300,7 @@ func TestGitAuthCallback(t *testing.T) {
|
|||
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
|
||||
location, err := resp.Location()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "/externalauth/github", location.Path)
|
||||
require.Equal(t, "/external-auth/github", location.Path)
|
||||
|
||||
// Callback again to simulate updating the token.
|
||||
resp = coderdtest.RequestExternalAuthCallback(t, "github", client)
|
||||
|
@ -319,7 +319,7 @@ func TestGitAuthCallback(t *testing.T) {
|
|||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
@ -376,7 +376,7 @@ func TestGitAuthCallback(t *testing.T) {
|
|||
},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
NoRefresh: true,
|
||||
}},
|
||||
})
|
||||
|
@ -420,7 +420,7 @@ func TestGitAuthCallback(t *testing.T) {
|
|||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
|
|
@ -65,9 +65,9 @@ func Test_RoutePatterns(t *testing.T) {
|
|||
"/api/**",
|
||||
"/@*/*/apps/**",
|
||||
"/%40*/*/apps/**",
|
||||
"/externalauth/*/callback",
|
||||
"/external-auth/*/callback",
|
||||
},
|
||||
output: "^(/api/?|/api/.+/?|/@[^/]+/[^/]+/apps/.+/?|/%40[^/]+/[^/]+/apps/.+/?|/externalauth/[^/]+/callback/?)$",
|
||||
output: "^(/api/?|/api/.+/?|/@[^/]+/[^/]+/apps/.+/?|/%40[^/]+/[^/]+/apps/.+/?|/external-auth/[^/]+/callback/?)$",
|
||||
},
|
||||
{
|
||||
name: "Slash",
|
||||
|
|
|
@ -280,7 +280,7 @@ func (api *API) templateVersionRichParameters(rw http.ResponseWriter, r *http.Re
|
|||
// @Tags Templates
|
||||
// @Param templateversion path string true "Template version ID" format(uuid)
|
||||
// @Success 200 {array} codersdk.TemplateVersionExternalAuth
|
||||
// @Router /templateversions/{templateversion}/externalauth [get]
|
||||
// @Router /templateversions/{templateversion}/external-auth [get]
|
||||
func (api *API) templateVersionExternalAuth(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var (
|
||||
|
@ -307,7 +307,7 @@ func (api *API) templateVersionExternalAuth(rw http.ResponseWriter, r *http.Requ
|
|||
}
|
||||
|
||||
// This is the URL that will redirect the user with a state token.
|
||||
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/externalauth/%s", config.ID))
|
||||
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/external-auth/%s", config.ID))
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to parse access URL.",
|
||||
|
@ -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{
|
||||
|
|
|
@ -342,7 +342,7 @@ func TestTemplateVersionsExternalAuth(t *testing.T) {
|
|||
OAuth2Config: &testutil.OAuth2Config{},
|
||||
ID: "github",
|
||||
Regex: regexp.MustCompile(`github\.com`),
|
||||
Type: codersdk.ExternalAuthProviderGitHub,
|
||||
Type: codersdk.EnhancedExternalAuthProviderGitHub.String(),
|
||||
}},
|
||||
})
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
|
|
|
@ -26,7 +26,7 @@ func Middleware(tracerProvider trace.TracerProvider) func(http.Handler) http.Han
|
|||
"/api/**",
|
||||
"/@*/*/apps/**",
|
||||
"/%40*/*/apps/**",
|
||||
"/externalauth/*/callback",
|
||||
"/external-auth/*/callback",
|
||||
}.MustCompile()
|
||||
|
||||
var tracer trace.Tracer
|
||||
|
|
|
@ -59,7 +59,7 @@ func Test_Middleware(t *testing.T) {
|
|||
{"/%40hi/hi/apps/hi", true},
|
||||
{"/%40hi/hi/apps/hi/hi", true},
|
||||
{"/%40hi/hi/apps/hi/hi", true},
|
||||
{"/externalauth/hi/callback", true},
|
||||
{"/external-auth/hi/callback", true},
|
||||
|
||||
// Other routes that should not be collected.
|
||||
{"/index.html", false},
|
||||
|
|
|
@ -2201,6 +2201,13 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
|||
})
|
||||
return
|
||||
}
|
||||
enhancedType := codersdk.EnhancedExternalAuthProvider(externalAuthConfig.Type)
|
||||
if !enhancedType.Git() {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "External auth provider does not support git.",
|
||||
})
|
||||
return
|
||||
}
|
||||
workspaceAgent := httpmw.WorkspaceAgent(r)
|
||||
// We must get the workspace to get the owner ID!
|
||||
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
|
||||
|
@ -2272,13 +2279,13 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
|||
if !valid {
|
||||
continue
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(enhancedType, externalAuthLink.OAuthAccessToken))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// This is the URL that will redirect the user with a state token.
|
||||
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/externalauth/%s", externalAuthConfig.ID))
|
||||
redirectURL, err := api.AccessURL.Parse(fmt.Sprintf("/external-auth/%s", externalAuthConfig.ID))
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to parse access URL.",
|
||||
|
@ -2320,20 +2327,20 @@ func (api *API) workspaceAgentsGitAuth(rw http.ResponseWriter, r *http.Request)
|
|||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(externalAuthConfig.Type, externalAuthLink.OAuthAccessToken))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, formatGitAuthAccessToken(enhancedType, externalAuthLink.OAuthAccessToken))
|
||||
}
|
||||
|
||||
// Provider types have different username/password formats.
|
||||
func formatGitAuthAccessToken(typ codersdk.ExternalAuthProvider, token string) agentsdk.GitAuthResponse {
|
||||
func formatGitAuthAccessToken(typ codersdk.EnhancedExternalAuthProvider, token string) agentsdk.GitAuthResponse {
|
||||
var resp agentsdk.GitAuthResponse
|
||||
switch typ {
|
||||
case codersdk.ExternalAuthProviderGitLab:
|
||||
case codersdk.EnhancedExternalAuthProviderGitLab:
|
||||
// https://stackoverflow.com/questions/25409700/using-gitlab-token-to-clone-without-authentication
|
||||
resp = agentsdk.GitAuthResponse{
|
||||
Username: "oauth2",
|
||||
Password: token,
|
||||
}
|
||||
case codersdk.ExternalAuthProviderBitBucket:
|
||||
case codersdk.EnhancedExternalAuthProviderBitBucket:
|
||||
// https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/#Cloning-a-repository-with-an-access-token
|
||||
resp = agentsdk.GitAuthResponse{
|
||||
Username: "x-token-auth",
|
||||
|
|
|
@ -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"`
|
||||
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"`
|
||||
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,21 +321,34 @@ type TraceConfig struct {
|
|||
DataDog clibase.Bool `json:"data_dog" typescript:",notnull"`
|
||||
}
|
||||
|
||||
type GitAuthConfig 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"`
|
||||
// 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 {
|
||||
|
@ -1710,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,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -7,10 +7,39 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
// EnhancedExternalAuthProvider is a constant that represents enhanced
|
||||
// support for a type of external authentication. All of the Git providers
|
||||
// are examples of enhanced, because they support intercepting "git clone".
|
||||
type EnhancedExternalAuthProvider string
|
||||
|
||||
func (e EnhancedExternalAuthProvider) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
// Git returns whether the provider is a Git provider.
|
||||
func (e EnhancedExternalAuthProvider) Git() bool {
|
||||
switch e {
|
||||
case EnhancedExternalAuthProviderGitHub,
|
||||
EnhancedExternalAuthProviderGitLab,
|
||||
EnhancedExternalAuthProviderBitBucket,
|
||||
EnhancedExternalAuthProviderAzureDevops:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
EnhancedExternalAuthProviderAzureDevops EnhancedExternalAuthProvider = "azure-devops"
|
||||
EnhancedExternalAuthProviderGitHub EnhancedExternalAuthProvider = "github"
|
||||
EnhancedExternalAuthProviderGitLab EnhancedExternalAuthProvider = "gitlab"
|
||||
EnhancedExternalAuthProviderBitBucket EnhancedExternalAuthProvider = "bitbucket"
|
||||
)
|
||||
|
||||
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"`
|
||||
|
@ -50,7 +79,7 @@ type ExternalAuthDeviceExchange struct {
|
|||
}
|
||||
|
||||
func (c *Client) ExternalAuthDeviceByID(ctx context.Context, provider string) (ExternalAuthDevice, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/externalauth/%s/device", provider), nil)
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/external-auth/%s/device", provider), nil)
|
||||
if err != nil {
|
||||
return ExternalAuthDevice{}, err
|
||||
}
|
||||
|
@ -64,7 +93,7 @@ func (c *Client) ExternalAuthDeviceByID(ctx context.Context, provider string) (E
|
|||
|
||||
// ExchangeGitAuth exchanges a device code for an external auth token.
|
||||
func (c *Client) ExternalAuthDeviceExchange(ctx context.Context, provider string, req ExternalAuthDeviceExchange) error {
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/externalauth/%s/device", provider), req)
|
||||
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/external-auth/%s/device", provider), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -77,7 +106,7 @@ func (c *Client) ExternalAuthDeviceExchange(ctx context.Context, provider string
|
|||
|
||||
// ExternalAuthByID returns the external auth for the given provider by ID.
|
||||
func (c *Client) ExternalAuthByID(ctx context.Context, provider string) (ExternalAuth, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/externalauth/%s", provider), nil)
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/external-auth/%s", provider), nil)
|
||||
if err != nil {
|
||||
return ExternalAuth{}, err
|
||||
}
|
||||
|
|
|
@ -34,10 +34,12 @@ type TemplateVersion struct {
|
|||
}
|
||||
|
||||
type TemplateVersionExternalAuth struct {
|
||||
ID string `json:"id"`
|
||||
Type ExternalAuthProvider `json:"type"`
|
||||
AuthenticateURL string `json:"authenticate_url"`
|
||||
Authenticated bool `json:"authenticated"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
DisplayName string `json:"display_name"`
|
||||
DisplayIcon string `json:"display_icon"`
|
||||
AuthenticateURL string `json:"authenticate_url"`
|
||||
Authenticated bool `json:"authenticated"`
|
||||
}
|
||||
|
||||
type ValidationMonotonicOrder string
|
||||
|
@ -134,7 +136,7 @@ func (c *Client) TemplateVersionRichParameters(ctx context.Context, version uuid
|
|||
|
||||
// TemplateVersionExternalAuth returns authentication providers for the requested template version.
|
||||
func (c *Client) TemplateVersionExternalAuth(ctx context.Context, version uuid.UUID) ([]TemplateVersionExternalAuth, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/externalauth", version), nil)
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/templateversions/%s/external-auth", version), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -744,35 +744,6 @@ func (c *Client) WorkspaceAgentLogsAfter(ctx context.Context, agentID uuid.UUID,
|
|||
}), nil
|
||||
}
|
||||
|
||||
// ExternalAuthProvider is a constant that represents the
|
||||
// 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"
|
||||
ExternalAuthProviderGitLab ExternalAuthProvider = "gitlab"
|
||||
ExternalAuthProviderBitBucket ExternalAuthProvider = "bitbucket"
|
||||
ExternalAuthProviderOpenIDConnect ExternalAuthProvider = "openid-connect"
|
||||
)
|
||||
|
||||
type WorkspaceAgentLog struct {
|
||||
ID int64 `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
||||
|
|
|
@ -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.
|
||||
|
||||
<video autoplay playsinline loop>
|
||||
<source src="https://github.com/coder/coder/blob/main/site/static/gitauth.mp4?raw=true" type="video/mp4">
|
||||
<source src="https://github.com/coder/coder/blob/main/site/static/external-auth.mp4?raw=true" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
## 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|<name of service e.g. jfrog>
|
||||
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
|
|
@ -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",
|
||||
|
@ -222,6 +221,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",
|
||||
|
@ -232,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,
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/externalauth/{externalauth} \
|
||||
curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth} \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /externalauth/{externalauth}`
|
||||
`GET /external-auth/{externalauth}`
|
||||
|
||||
### Parameters
|
||||
|
||||
|
@ -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",
|
||||
|
@ -65,12 +65,12 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio
|
|||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/externalauth/{externalauth}/device \
|
||||
curl -X GET http://coder-server:8080/api/v2/external-auth/{externalauth}/device \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /externalauth/{externalauth}/device`
|
||||
`GET /external-auth/{externalauth}/device`
|
||||
|
||||
### Parameters
|
||||
|
||||
|
@ -106,11 +106,11 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio
|
|||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X POST http://coder-server:8080/api/v2/externalauth/{externalauth}/device \
|
||||
curl -X POST http://coder-server:8080/api/v2/external-auth/{externalauth}/device \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`POST /externalauth/{externalauth}/device`
|
||||
`POST /external-auth/{externalauth}/device`
|
||||
|
||||
### Parameters
|
||||
|
||||
|
|
|
@ -620,7 +620,7 @@
|
|||
|
||||
_None_
|
||||
|
||||
## clibase.Struct-array_codersdk_GitAuthConfig
|
||||
## clibase.Struct-array_codersdk_ExternalAuthConfig
|
||||
|
||||
```json
|
||||
{
|
||||
|
@ -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",
|
||||
|
@ -646,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
|
||||
|
||||
|
@ -2043,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",
|
||||
|
@ -2053,6 +2054,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",
|
||||
|
@ -2063,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,
|
||||
|
@ -2408,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",
|
||||
|
@ -2418,6 +2421,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",
|
||||
|
@ -2428,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,
|
||||
|
@ -2602,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
|
||||
|
||||
|
@ -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
|
||||
|
@ -2817,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
|
||||
|
@ -2839,24 +2888,6 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
| `user_code` | string | false | | |
|
||||
| `verification_uri` | string | false | | |
|
||||
|
||||
## codersdk.ExternalAuthProvider
|
||||
|
||||
```json
|
||||
"azure-devops"
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value |
|
||||
| ---------------- |
|
||||
| `azure-devops` |
|
||||
| `github` |
|
||||
| `gitlab` |
|
||||
| `bitbucket` |
|
||||
| `openid-connect` |
|
||||
|
||||
## codersdk.ExternalAuthUser
|
||||
|
||||
```json
|
||||
|
@ -2945,44 +2976,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,
|
||||
"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 | | |
|
||||
| `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
|
||||
|
@ -4741,19 +4734,23 @@ 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"
|
||||
"type": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------ | -------------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `authenticate_url` | string | false | | |
|
||||
| `authenticated` | boolean | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `type` | [codersdk.ExternalAuthProvider](#codersdkexternalauthprovider) | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------------ | ------- | -------- | ------------ | ----------- |
|
||||
| `authenticate_url` | string | false | | |
|
||||
| `authenticated` | boolean | false | | |
|
||||
| `display_icon` | string | false | | |
|
||||
| `display_name` | string | false | | |
|
||||
| `id` | string | false | | |
|
||||
| `type` | string | false | | |
|
||||
|
||||
## codersdk.TemplateVersionParameter
|
||||
|
||||
|
|
|
@ -1806,12 +1806,12 @@ To perform this operation, you must be authenticated. [Learn more](authenticatio
|
|||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/externalauth \
|
||||
curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/external-auth \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /templateversions/{templateversion}/externalauth`
|
||||
`GET /templateversions/{templateversion}/external-auth`
|
||||
|
||||
### Parameters
|
||||
|
||||
|
@ -1828,8 +1828,10 @@ 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"
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
@ -1844,23 +1846,15 @@ curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/e
|
|||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------- | ------------------------------------------------------------------------ | -------- | ------------ | ----------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» authenticate_url` | string | false | | |
|
||||
| `» authenticated` | boolean | 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` |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------- | ------- | -------- | ------------ | ----------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» authenticate_url` | string | false | | |
|
||||
| `» authenticated` | boolean | false | | |
|
||||
| `» display_icon` | string | false | | |
|
||||
| `» display_name` | string | false | | |
|
||||
| `» id` | string | false | | |
|
||||
| `» type` | string | false | | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -57,6 +57,7 @@ export default defineConfig({
|
|||
CODER_GITAUTH_0_ID: gitAuth.deviceProvider,
|
||||
CODER_GITAUTH_0_TYPE: "github",
|
||||
CODER_GITAUTH_0_CLIENT_ID: "client",
|
||||
CODER_GITAUTH_0_CLIENT_SECRET: "secret",
|
||||
CODER_GITAUTH_0_DEVICE_FLOW: "true",
|
||||
CODER_GITAUTH_0_APP_INSTALL_URL:
|
||||
"https://github.com/apps/coder/installations/new",
|
||||
|
|
|
@ -46,7 +46,7 @@ test("external auth device", async ({ page }) => {
|
|||
sentPending.done();
|
||||
});
|
||||
|
||||
await page.goto(`/externalauth/${gitAuth.deviceProvider}`, {
|
||||
await page.goto(`/external-auth/${gitAuth.deviceProvider}`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await page.getByText(device.user_code).isVisible();
|
||||
|
@ -70,11 +70,11 @@ test("external auth web", async ({ baseURL, page }) => {
|
|||
});
|
||||
srv.use(gitAuth.authPath, (req, res) => {
|
||||
res.redirect(
|
||||
`${baseURL}/externalauth/${gitAuth.webProvider}/callback?code=1234&state=` +
|
||||
`${baseURL}/external-auth/${gitAuth.webProvider}/callback?code=1234&state=` +
|
||||
req.query.state,
|
||||
);
|
||||
});
|
||||
await page.goto(`/externalauth/${gitAuth.webProvider}`, {
|
||||
await page.goto(`/external-auth/${gitAuth.webProvider}`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
// This endpoint doesn't have the installations URL set intentionally!
|
||||
|
|
|
@ -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(
|
||||
|
@ -210,7 +210,7 @@ export const AppRouter: FC = () => {
|
|||
<Route path="health" element={<HealthPage />} />
|
||||
|
||||
<Route
|
||||
path="externalauth/:provider"
|
||||
path="external-auth/:provider"
|
||||
element={<ExternalAuthPage />}
|
||||
/>
|
||||
|
||||
|
@ -292,7 +292,10 @@ export const AppRouter: FC = () => {
|
|||
<Route path="appearance" element={<AppearanceSettingsPage />} />
|
||||
<Route path="network" element={<NetworkSettingsPage />} />
|
||||
<Route path="userauth" element={<UserAuthSettingsPage />} />
|
||||
<Route path="gitauth" element={<GitAuthSettingsPage />} />
|
||||
<Route
|
||||
path="external-auth"
|
||||
element={<ExternalAuthSettingsPage />}
|
||||
/>
|
||||
<Route
|
||||
path="workspace-proxies"
|
||||
element={<WorkspaceProxyPage />}
|
||||
|
|
|
@ -338,7 +338,7 @@ export const getTemplateVersionExternalAuth = async (
|
|||
versionId: string,
|
||||
): Promise<TypesGen.TemplateVersionExternalAuth[]> => {
|
||||
const response = await axios.get(
|
||||
`/api/v2/templateversions/${versionId}/externalauth`,
|
||||
`/api/v2/templateversions/${versionId}/external-auth`,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
@ -861,14 +861,14 @@ export const getExperiments = async (): Promise<TypesGen.Experiment[]> => {
|
|||
export const getExternalAuthProvider = async (
|
||||
provider: string,
|
||||
): Promise<TypesGen.ExternalAuth> => {
|
||||
const resp = await axios.get(`/api/v2/externalauth/${provider}`);
|
||||
const resp = await axios.get(`/api/v2/external-auth/${provider}`);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
export const getExternalAuthDevice = async (
|
||||
provider: string,
|
||||
): Promise<TypesGen.ExternalAuthDevice> => {
|
||||
const resp = await axios.get(`/api/v2/externalauth/${provider}/device`);
|
||||
const resp = await axios.get(`/api/v2/external-auth/${provider}/device`);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
|
@ -876,7 +876,10 @@ export const exchangeExternalAuthDevice = async (
|
|||
provider: string,
|
||||
req: TypesGen.ExternalAuthDeviceExchange,
|
||||
): Promise<void> => {
|
||||
const resp = await axios.post(`/api/v2/externalauth/${provider}/device`, req);
|
||||
const resp = await axios.post(
|
||||
`/api/v2/external-auth/${provider}/device`,
|
||||
req,
|
||||
);
|
||||
return resp.data;
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -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[];
|
||||
|
@ -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,23 +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;
|
||||
}
|
||||
|
||||
// From codersdk/gitsshkey.go
|
||||
export interface GitSSHKey {
|
||||
readonly user_id: string;
|
||||
|
@ -1013,7 +1015,9 @@ export interface TemplateVersion {
|
|||
// From codersdk/templateversions.go
|
||||
export interface TemplateVersionExternalAuth {
|
||||
readonly id: string;
|
||||
readonly type: ExternalAuthProvider;
|
||||
readonly type: string;
|
||||
readonly display_name: string;
|
||||
readonly display_icon: string;
|
||||
readonly authenticate_url: string;
|
||||
readonly authenticated: boolean;
|
||||
}
|
||||
|
@ -1631,6 +1635,19 @@ export const DisplayApps: DisplayApp[] = [
|
|||
"web_terminal",
|
||||
];
|
||||
|
||||
// From codersdk/externalauth.go
|
||||
export type EnhancedExternalAuthProvider =
|
||||
| "azure-devops"
|
||||
| "bitbucket"
|
||||
| "github"
|
||||
| "gitlab";
|
||||
export const EnhancedExternalAuthProviders: EnhancedExternalAuthProvider[] = [
|
||||
"azure-devops",
|
||||
"bitbucket",
|
||||
"github",
|
||||
"gitlab",
|
||||
];
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export type Entitlement = "entitled" | "grace_period" | "not_entitled";
|
||||
export const Entitlements: Entitlement[] = [
|
||||
|
@ -1656,21 +1673,6 @@ export const Experiments: Experiment[] = [
|
|||
"workspace_actions",
|
||||
];
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
export type ExternalAuthProvider =
|
||||
| "azure-devops"
|
||||
| "bitbucket"
|
||||
| "github"
|
||||
| "gitlab"
|
||||
| "openid-connect";
|
||||
export const ExternalAuthProviders: ExternalAuthProvider[] = [
|
||||
"azure-devops",
|
||||
"bitbucket",
|
||||
"github",
|
||||
"gitlab",
|
||||
"openid-connect",
|
||||
];
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export type FeatureName =
|
||||
| "advanced_template_scheduling"
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -163,8 +163,8 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
|||
|
||||
{externalAuth && externalAuth.length > 0 && (
|
||||
<FormSection
|
||||
title="Git Authentication"
|
||||
description="This template requires authentication to automatically perform Git operations on create."
|
||||
title="External Authentication"
|
||||
description="This template requires authentication to external services."
|
||||
>
|
||||
<FormFields>
|
||||
{externalAuth.map((auth) => (
|
||||
|
@ -174,7 +174,8 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
|||
authenticated={auth.authenticated}
|
||||
externalAuthPollingState={externalAuthPollingState}
|
||||
startPollingExternalAuth={startPollingExternalAuth}
|
||||
type={auth.type}
|
||||
displayName={auth.display_name}
|
||||
displayIcon={auth.display_icon}
|
||||
error={externalAuthErrors[auth.id]}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -11,56 +11,64 @@ type Story = StoryObj<typeof ExternalAuth>;
|
|||
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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<ExternalAuthProps> = (props) => {
|
||||
const {
|
||||
type,
|
||||
displayName,
|
||||
displayIcon,
|
||||
authenticated,
|
||||
authenticateURL,
|
||||
externalAuthPollingState,
|
||||
|
@ -37,32 +33,9 @@ export const ExternalAuth: FC<ExternalAuthProps> = (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 (
|
||||
<Tooltip
|
||||
title={authenticated && `${prettyName} has already been connected.`}
|
||||
title={authenticated && `${displayName} has already been connected.`}
|
||||
>
|
||||
<Stack alignItems="center" spacing={1}>
|
||||
<LoadingButton
|
||||
|
@ -70,7 +43,14 @@ export const ExternalAuth: FC<ExternalAuthProps> = (props) => {
|
|||
href={authenticateURL}
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<Icon />}
|
||||
startIcon={
|
||||
<img
|
||||
src={displayIcon}
|
||||
alt={`${displayName} Icon`}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
}
|
||||
disabled={authenticated}
|
||||
className={styles.button}
|
||||
color={error ? "error" : undefined}
|
||||
|
@ -86,8 +66,8 @@ export const ExternalAuth: FC<ExternalAuthProps> = (props) => {
|
|||
}}
|
||||
>
|
||||
{authenticated
|
||||
? `Authenticated with ${prettyName}`
|
||||
: `Login with ${prettyName}`}
|
||||
? `Authenticated with ${displayName}`
|
||||
: `Login with ${displayName}`}
|
||||
</LoadingButton>
|
||||
|
||||
{externalAuthPollingState === "abandoned" && (
|
||||
|
|
|
@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("Git Authentication Settings")}</title>
|
||||
<title>{pageTitle("External Authentication Settings")}</title>
|
||||
</Helmet>
|
||||
|
||||
<GitAuthSettingsPageView config={deploymentValues.config} />
|
||||
<ExternalAuthSettingsPageView config={deploymentValues.config} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GitAuthSettingsPage;
|
||||
export default ExternalAuthSettingsPage;
|
|
@ -1,12 +1,12 @@
|
|||
import { GitAuthSettingsPageView } from "./GitAuthSettingsPageView";
|
||||
import { ExternalAuthSettingsPageView } from "./ExternalAuthSettingsPageView";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
const meta: Meta<typeof GitAuthSettingsPageView> = {
|
||||
title: "pages/GitAuthSettingsPageView",
|
||||
component: GitAuthSettingsPageView,
|
||||
const meta: Meta<typeof ExternalAuthSettingsPageView> = {
|
||||
title: "pages/ExternalAuthSettingsPageView",
|
||||
component: ExternalAuthSettingsPageView,
|
||||
args: {
|
||||
config: {
|
||||
git_auth: [
|
||||
external_auth: [
|
||||
{
|
||||
id: "0000-1111",
|
||||
type: "GitHub",
|
||||
|
@ -21,6 +21,8 @@ const meta: Meta<typeof GitAuthSettingsPageView> = {
|
|||
scopes: [],
|
||||
device_flow: true,
|
||||
device_code_url: "",
|
||||
display_icon: "",
|
||||
display_name: "GitHub",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -28,6 +30,6 @@ const meta: Meta<typeof GitAuthSettingsPageView> = {
|
|||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof GitAuthSettingsPageView>;
|
||||
type Story = StoryObj<typeof ExternalAuthSettingsPageView>;
|
||||
|
||||
export const Page: Story = {};
|
|
@ -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 (
|
||||
<>
|
||||
<Header
|
||||
title="Git Authentication"
|
||||
description="Coder integrates with GitHub, GitLab, BitBucket, and Azure Repos to authenticate developers with your Git provider."
|
||||
docsHref={docs("/admin/git-providers")}
|
||||
title="External Authentication"
|
||||
description="Coder integrates with GitHub, GitLab, BitBucket, Azure Repos, and OpenID Connect to authenticate developers with external services."
|
||||
docsHref={docs("/admin/external-auth")}
|
||||
/>
|
||||
|
||||
<video
|
||||
|
@ -33,7 +33,7 @@ export const GitAuthSettingsPageView = ({
|
|||
muted
|
||||
loop
|
||||
playsInline
|
||||
src="/gitauth.mp4"
|
||||
src="/external-auth.mp4"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
borderRadius: 4,
|
||||
|
@ -42,7 +42,8 @@ export const GitAuthSettingsPageView = ({
|
|||
|
||||
<div className={styles.description}>
|
||||
<Alert severity="info" actions={<EnterpriseBadge key="enterprise" />}>
|
||||
Integrating with multiple Git providers is an Enterprise feature.
|
||||
Integrating with multiple External authentication providers is an
|
||||
Enterprise feature.
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
|
@ -56,7 +57,8 @@ export const GitAuthSettingsPageView = ({
|
|||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{((config.git_auth === null || config.git_auth?.length === 0) && (
|
||||
{((config.external_auth === null ||
|
||||
config.external_auth?.length === 0) && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<div className={styles.empty}>
|
||||
|
@ -65,7 +67,7 @@ export const GitAuthSettingsPageView = ({
|
|||
</TableCell>
|
||||
</TableRow>
|
||||
)) ||
|
||||
config.git_auth?.map((git: GitAuthConfig) => {
|
||||
config.external_auth?.map((git: ExternalAuthConfig) => {
|
||||
const name = git.id || git.type;
|
||||
return (
|
||||
<TableRow key={name}>
|
|
@ -72,8 +72,7 @@ const ExternalAuthPage: FC = () => {
|
|||
!getExternalAuthProviderQuery.data.authenticated &&
|
||||
!getExternalAuthProviderQuery.data.device
|
||||
) {
|
||||
window.location.href = `/externalauth/${provider}/callback`;
|
||||
|
||||
window.location.href = `/external-auth/${provider}/callback`;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,12 +15,12 @@ const Template: StoryFn<ExternalAuthPageViewProps> = (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: [
|
||||
|
|
|
@ -35,7 +35,7 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
|
|||
if (!externalAuth.authenticated) {
|
||||
return (
|
||||
<SignInLayout>
|
||||
<Welcome message={`Authenticate with ${externalAuth.type}`} />
|
||||
<Welcome message={`Authenticate with ${externalAuth.display_name}`} />
|
||||
|
||||
{externalAuth.device && (
|
||||
<GitDeviceAuth
|
||||
|
@ -50,9 +50,7 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
|
|||
const hasInstallations = externalAuth.installations.length > 0;
|
||||
|
||||
// We only want to wrap this with a link if an install URL is available!
|
||||
let installTheApp: JSX.Element = (
|
||||
<>{`install the ${externalAuth.type} App`}</>
|
||||
);
|
||||
let installTheApp: React.ReactNode = `install the ${externalAuth.display_name} App`;
|
||||
if (externalAuth.app_install_url) {
|
||||
installTheApp = (
|
||||
<Link
|
||||
|
@ -67,12 +65,14 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
|
|||
|
||||
return (
|
||||
<SignInLayout>
|
||||
<Welcome message={`You've authenticated with ${externalAuth.type}!`} />
|
||||
<Welcome
|
||||
message={`You've authenticated with ${externalAuth.display_name}!`}
|
||||
/>
|
||||
<p className={styles.text}>
|
||||
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!"}
|
||||
</p>
|
||||
|
||||
{externalAuth.installations.length > 0 && (
|
||||
|
@ -126,7 +126,7 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
|
|||
{externalAuth.installations.length > 0
|
||||
? "Configure"
|
||||
: "Install"}{" "}
|
||||
the {externalAuth.type} App
|
||||
the {externalAuth.display_name} App
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
|
|
|
@ -2188,16 +2188,20 @@ export const MockTemplateVersionExternalAuthGithub: TypesGen.TemplateVersionExte
|
|||
{
|
||||
id: "github",
|
||||
type: "github",
|
||||
authenticate_url: "https://example.com/externalauth/github",
|
||||
authenticate_url: "https://example.com/external-auth/github",
|
||||
authenticated: false,
|
||||
display_icon: "/icon/github.svg",
|
||||
display_name: "GitHub",
|
||||
};
|
||||
|
||||
export const MockTemplateVersionExternalAuthGithubAuthenticated: TypesGen.TemplateVersionExternalAuth =
|
||||
{
|
||||
id: "github",
|
||||
type: "github",
|
||||
authenticate_url: "https://example.com/externalauth/github",
|
||||
authenticate_url: "https://example.com/external-auth/github",
|
||||
authenticated: true,
|
||||
display_icon: "/icon/github.svg",
|
||||
display_name: "GitHub",
|
||||
};
|
||||
|
||||
export const MockDeploymentStats: TypesGen.DeploymentStats = {
|
||||
|
|
|
@ -112,7 +112,7 @@ export const handlers = [
|
|||
},
|
||||
),
|
||||
rest.get(
|
||||
"/api/v2/templateversions/:templateVersionId/externalauth",
|
||||
"/api/v2/templateversions/:templateVersionId/external-auth",
|
||||
async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json([]));
|
||||
},
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
|
||||
export const AzureDevOpsIcon = (props: SvgIconProps): JSX.Element => (
|
||||
<SvgIcon {...props} viewBox="0 0 111 110">
|
||||
<svg viewBox="0 0 111 110" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#clip0_1916_993)">
|
||||
<path
|
||||
d="M83.0365 94.769L0 82.288L83.0365 110L111 98.365V11.2115L83.0365 0V94.769Z"
|
||||
|
@ -21,5 +18,4 @@ export const AzureDevOpsIcon = (props: SvgIconProps): JSX.Element => (
|
|||
<rect width="111" height="110" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</SvgIcon>
|
||||
);
|
||||
</svg>
|
Before Width: | Height: | Size: 819 B After Width: | Height: | Size: 695 B |
|
@ -1,8 +1,5 @@
|
|||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
|
||||
export const BitbucketIcon = (props: SvgIconProps): JSX.Element => (
|
||||
<SvgIcon {...props} viewBox="0 0 501 450">
|
||||
<g clipPath="url(#clip0_1917_1001)">
|
||||
<svg viewBox="0 0 501 450" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1917_1001)">
|
||||
<path
|
||||
d="M17.0206 0.0721333C14.6826 0.0419786 12.3663 0.523969 10.2344 1.48427C8.10245 2.44457 6.20658 3.8599 4.67987 5.63088C3.15316 7.40186 2.03262 9.48557 1.39691 11.7357C0.761211 13.9858 0.625758 16.3479 1.00007 18.6559L69.0071 431.504C69.8544 436.556 72.4548 441.148 76.3515 444.474C80.2481 447.799 85.1919 449.645 90.3144 449.688H416.572C420.412 449.737 424.142 448.405 427.082 445.935C430.023 443.465 431.978 440.021 432.592 436.23L500.6 18.736C500.974 16.428 500.838 14.0659 500.203 11.8158C499.567 9.56568 498.446 7.48197 496.92 5.71098C495.393 3.94 493.497 2.52467 491.365 1.56437C489.233 0.604073 486.917 0.122079 484.579 0.152234L17.0206 0.0721333ZM303.387 298.454H199.254L171.058 151.146H328.619L303.387 298.454Z"
|
||||
fill="#2684FF"
|
||||
|
@ -21,12 +18,11 @@ export const BitbucketIcon = (props: SvgIconProps): JSX.Element => (
|
|||
y2="386.327"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0.18" stopColor="#0052CC" />
|
||||
<stop offset="1" stopColor="#2684FF" />
|
||||
<stop offset="0.18" stop-color="#0052CC" />
|
||||
<stop offset="1" stop-color="#2684FF" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1917_1001">
|
||||
<rect width="501" height="450" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</SvgIcon>
|
||||
);
|
||||
</svg>
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1 @@
|
|||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 961 B |
|
@ -0,0 +1,25 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 194 186">
|
||||
<g clipPath="url(#clip0_1915_987)">
|
||||
<path
|
||||
d="M189.83 73.7299L189.56 73.0399L163.42 4.81995C162.888 3.48288 161.946 2.34863 160.73 1.57996C159.513 0.82434 158.093 0.460411 156.662 0.537307C155.232 0.614203 153.859 1.12822 152.73 2.00996C151.613 2.91701 150.803 4.14609 150.41 5.52995L132.76 59.53H61.2899L43.6399 5.52995C43.2571 4.13855 42.4453 2.90331 41.3199 1.99995C40.1907 1.11822 38.8181 0.6042 37.3875 0.527305C35.9569 0.450409 34.5371 0.814338 33.3199 1.56995C32.1062 2.34173 31.1653 3.47499 30.6299 4.80995L4.43991 73L4.17991 73.69C0.416933 83.522 -0.0475445 94.3109 2.8565 104.43C5.76055 114.549 11.8757 123.45 20.2799 129.79L20.3699 129.86L20.6099 130.03L60.4299 159.85L80.1299 174.76L92.1299 183.82C93.5336 184.886 95.2475 185.463 97.0099 185.463C98.7723 185.463 100.486 184.886 101.89 183.82L113.89 174.76L133.59 159.85L173.65 129.85L173.75 129.77C182.135 123.429 188.236 114.537 191.136 104.432C194.035 94.3262 193.577 83.5527 189.83 73.7299Z"
|
||||
fill="#E24329"
|
||||
/>
|
||||
<path
|
||||
d="M189.83 73.7299L189.56 73.0399C176.823 75.6543 164.82 81.0495 154.41 88.8399L97 132.25C116.55 147.04 133.57 159.89 133.57 159.89L173.63 129.89L173.73 129.81C182.127 123.469 188.238 114.572 191.141 104.457C194.045 94.3434 193.585 83.5598 189.83 73.7299Z"
|
||||
fill="#FC6D26"
|
||||
/>
|
||||
<path
|
||||
d="M60.4299 159.89L80.1299 174.8L92.1299 183.86C93.5336 184.926 95.2475 185.503 97.0099 185.503C98.7723 185.503 100.486 184.926 101.89 183.86L113.89 174.8L133.59 159.89C133.59 159.89 116.55 147 96.9999 132.25C77.4499 147 60.4299 159.89 60.4299 159.89Z"
|
||||
fill="#FCA326"
|
||||
/>
|
||||
<path
|
||||
d="M39.5799 88.84C29.1778 81.0335 17.1779 75.6243 4.43991 73L4.17991 73.69C0.416933 83.5221 -0.0475445 94.311 2.8565 104.43C5.76055 114.549 11.8757 123.45 20.2799 129.79L20.3699 129.86L20.6099 130.03L60.4299 159.85C60.4299 159.85 77.4299 147 96.9999 132.21L39.5799 88.84Z"
|
||||
fill="#FC6D26"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1915_987">
|
||||
<rect width="194" height="186" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
Loading…
Reference in New Issue