Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 153 additions & 33 deletions coderd/workspaceagents.go
Original file line number Diff line number Diff line change
Expand Up @@ -2083,39 +2083,10 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
// new token to be issued!
listen := query.Has("listen")

var externalAuthConfig *externalauth.Config
for _, extAuth := range api.ExternalAuthConfigs {
if extAuth.ID == id {
externalAuthConfig = extAuth
break
}
if match == "" || extAuth.Regex == nil {
continue
}
matches := extAuth.Regex.MatchString(match)
if !matches {
continue
}
externalAuthConfig = extAuth
}
if externalAuthConfig == nil {
detail := "External auth provider not found."
if len(api.ExternalAuthConfigs) > 0 {
regexURLs := make([]string, 0, len(api.ExternalAuthConfigs))
for _, extAuth := range api.ExternalAuthConfigs {
if extAuth.Regex == nil {
continue
}
regexURLs = append(regexURLs, fmt.Sprintf("%s=%q", extAuth.ID, extAuth.Regex.String()))
}
detail = fmt.Sprintf("The configured external auth provider have regex filters that do not match the url. Provider url regex: %s", strings.Join(regexURLs, ","))
}
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("No matching external auth provider found in Coder for the url %q.", match),
Detail: detail,
})
return
}
// Resolve the calling agent's own workspace/build before selecting a
// provider below, so the match-only (GIT_ASKPASS) path can be scoped to
// this workspace's own template-declared providers instead of scanning
// every provider configured on the deployment.
workspaceAgent := httpmw.WorkspaceAgent(r)
// We must get the workspace to get the owner ID!
resource, err := api.Database.GetWorkspaceResourceByID(ctx, workspaceAgent.ResourceID)
Expand Down Expand Up @@ -2143,6 +2114,83 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
return
}

var externalAuthConfig *externalauth.Config
if id != "" {
// Explicit ID path: exact match only. Deterministic, and deliberately
// unaffected by the template-scoped narrowing below.
for _, extAuth := range api.ExternalAuthConfigs {
if extAuth.ID == id {
externalAuthConfig = extAuth
break
}
}
} else {
// match-only path (GIT_ASKPASS supplies a hostname, never an ID):
// narrow to the workspace's own template-declared providers first.
// Only fall back to the full deployment-wide scan when every declared
// provider is configured and none of them match this hostname (e.g. a
// host the template never declared). Report an error rather than guess
// when several declared providers match, or when the declaration set is
// stale.
//
// Both errors use 404 so `coder gitaskpass` warns and defers to git's
// own credential behavior instead of failing the git operation.
declared, err := api.workspaceAgentsExternalAuthDeclaredCandidates(ctx, build, match)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to resolve the workspace's template-declared external auth providers.",
Detail: err.Error(),
})
return
}
switch {
case len(declared.matchedIDs) > 1:
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("Multiple external auth providers declared by this workspace's template match %q: %s.", match, strings.Join(declared.matchedIDs, ", ")),
Detail: "Coder cannot tell which of them to use. Request a token with an explicit provider ID instead, using `coder external-auth access-token <id>`.",
})
return
case declared.config != nil:
externalAuthConfig = declared.config
case len(declared.missingIDs) > 0:
// A declared provider that the deployment no longer configures
// leaves no way to tell whether it was the one meant to serve this
// hostname, so another provider's token could silently stand in for
// it. Refuse instead, which keeps the template scoping above intact
// even while a template and the deployment config disagree.
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("This workspace's template declares external auth provider(s) that this deployment no longer configures: %s.", strings.Join(declared.missingIDs, ", ")),
Detail: "Coder will not substitute a different provider's token. Restore that provider's configuration, or update the template to declare a configured provider.",
})
return
default:
for _, extAuth := range api.ExternalAuthConfigs {
if extAuth.Regex == nil || !extAuth.Regex.MatchString(match) {
continue
}
externalAuthConfig = extAuth
}
}
}
if externalAuthConfig == nil {
detail := "External auth provider not found."
if len(api.ExternalAuthConfigs) > 0 {
regexURLs := make([]string, 0, len(api.ExternalAuthConfigs))
for _, extAuth := range api.ExternalAuthConfigs {
if extAuth.Regex == nil {
continue
}
regexURLs = append(regexURLs, fmt.Sprintf("%s=%q", extAuth.ID, extAuth.Regex.String()))
}
detail = fmt.Sprintf("The configured external auth provider have regex filters that do not match the url. Provider url regex: %s", strings.Join(regexURLs, ","))
}
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
Message: fmt.Sprintf("No matching external auth provider found in Coder for the url %q.", match),
Detail: detail,
})
return
}

// Pre-check if the caller can read the external auth links for the owner of the
// workspace. Do this up front because a sql.ErrNoRows is expected if the user is
// in the flow of authenticating. If no row is present, the auth check is delayed
Expand Down Expand Up @@ -2239,6 +2287,78 @@ func (api *API) workspaceAgentsExternalAuth(rw http.ResponseWriter, r *http.Requ
httpapi.Write(ctx, rw, http.StatusOK, resp)
}

// declaredExternalAuthCandidates describes how a workspace template's declared
// external auth providers relate to one hostname-only (GIT_ASKPASS) request.
type declaredExternalAuthCandidates struct {
// config is the provider to use, set only when exactly one declared and
// configured provider matches the hostname.
config *externalauth.Config
// matchedIDs holds the ID of every declared and configured provider whose
// regex matches the hostname, in declaration order.
matchedIDs []string
// missingIDs holds the ID of every declared provider that the deployment
// does not configure.
missingIDs []string
}

// workspaceAgentsExternalAuthDeclaredCandidates resolves which configured
// external auth provider should service a hostname-only (GIT_ASKPASS) request,
// scoped to the given build's template version's declared providers. The zero
// value means the template declares no providers at all.
//
// config is set only when exactly one declared provider matched, so callers
// should distinguish four outcomes:
// - several matchedIDs: report the collision rather than guess between them.
// - config set: use it.
// - no match but some missingIDs: the declaration set is stale, so report
// that instead of substituting a provider the template never declared.
// - no match and nothing missing: the template's providers simply do not
// serve this hostname, so fall back to a deployment-wide scan.
func (api *API) workspaceAgentsExternalAuthDeclaredCandidates(ctx context.Context, build database.WorkspaceBuild, match string) (declaredExternalAuthCandidates, error) {
// Template reads authorize through the template's ACL, which the owner may
// no longer have. The version ID is server-derived from the agent's token.
//nolint:gocritic // Agent needs system access to read its own template version's declared providers.
sysCtx := dbauthz.AsSystemRestricted(ctx)
templateVersion, err := api.Database.GetTemplateVersionByID(sysCtx, build.TemplateVersionID)
if err != nil {
return declaredExternalAuthCandidates{}, xerrors.Errorf("get template version: %w", err)
}

var declared []database.ExternalAuthProvider
if err := json.Unmarshal(templateVersion.ExternalAuthProviders, &declared); err != nil {
return declaredExternalAuthCandidates{}, xerrors.Errorf("unmarshal template version external auth providers: %w", err)
}

var (
candidates []*externalauth.Config
out declaredExternalAuthCandidates
)
for _, provider := range declared {
idx := slices.IndexFunc(api.ExternalAuthConfigs, func(extAuth *externalauth.Config) bool {
return extAuth.ID == provider.ID
})
if idx < 0 {
out.missingIDs = append(out.missingIDs, provider.ID)
continue
}
extAuth := api.ExternalAuthConfigs[idx]
if extAuth.Regex == nil || !extAuth.Regex.MatchString(match) {
continue
}
candidates = append(candidates, extAuth)
}

for _, candidate := range candidates {
out.matchedIDs = append(out.matchedIDs, candidate.ID)
}
// Only a single match is actionable. Leave config nil when several match so
// the caller reports the collision instead of picking one arbitrarily.
if len(candidates) == 1 {
out.config = candidates[0]
}
return out, nil
}

func (api *API) workspaceAgentsExternalAuthListen(ctx context.Context, rw http.ResponseWriter, previous *database.ExternalAuthLink, externalAuthConfig *externalauth.Config, workspace database.Workspace, gitRef chatGitRef) {
// Since we're ticking frequently and this sign-in operation is rare,
// we are OK with polling to avoid the complexity of pubsub.
Expand Down
Loading
Loading