From aaccc84c7b64ac7d3d2fa6566e88d727579138f9 Mon Sep 17 00:00:00 2001 From: Yassine Bounekhla Date: Thu, 16 Jan 2025 13:15:45 -0500 Subject: [PATCH] CR --- api/client/webclient/webconfig.go | 2 + lib/web/apiserver.go | 35 +++++------- lib/web/resources.go | 55 +++++++++++++++---- .../src/AuthConnectors/AuthConnectors.tsx | 23 ++++---- web/packages/teleport/src/Login/useLogin.ts | 25 ++++++++- web/packages/teleport/src/config.ts | 8 ++- .../src/services/resources/resource.ts | 16 +++++- 7 files changed, 117 insertions(+), 47 deletions(-) diff --git a/api/client/webclient/webconfig.go b/api/client/webclient/webconfig.go index c7ac4ffa1fce5..90fa9436951c8 100644 --- a/api/client/webclient/webconfig.go +++ b/api/client/webclient/webconfig.go @@ -188,6 +188,8 @@ type WebConfigAuthSettings struct { AllowPasswordless bool `json:"allowPasswordless,omitempty"` // AuthType is the authentication type. AuthType string `json:"authType"` + // DefaultConnectorName is the name of the default connector in the auth preferences. This will be empty if the default is "local". + DefaultConnectorName string `json:"defaultConnectorName"` // PreferredLocalMFA is a server-side hint for clients to pick an MFA method // when various options are available. // It is empty if there is nothing to suggest. diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 673734d69c76e..95e776bdc9c21 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -976,7 +976,7 @@ func (h *Handler) bindDefaultEndpoints() { h.DELETE("/webapi/github/:name", h.WithAuth(h.deleteGithubConnector)) // Sets the default connector in the auth preference. - h.PUT("/webapi/defaultconnector", h.WithAuth(h.setDefaultConnectorHandle)) + h.PUT("/webapi/authconnector/default", h.WithAuth(h.setDefaultConnectorHandle)) h.GET("/webapi/trustedcluster", h.WithAuth(h.getTrustedClustersHandle)) h.POST("/webapi/trustedcluster", h.WithAuth(h.upsertTrustedClusterHandle)) @@ -1795,34 +1795,25 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou } else { authType := cap.GetType() var localConnectorName string + var defaultConnectorName string if authType == constants.Local { localConnectorName = cap.GetConnectorName() } else { - // Move the default connector to the top of the list so that it shows up first in the UI - defaultConnectorName := cap.GetConnectorName() - for i, provider := range authProviders { - if provider.Name == defaultConnectorName && provider.Type == authType { - // Remove it from its current position - defaultProvider := authProviders[i] - authProviders = append(authProviders[:i], authProviders[i+1:]...) - // Insert it at the beginning - authProviders = append([]webclient.WebConfigAuthProvider{defaultProvider}, authProviders...) - break - } - } + defaultConnectorName = cap.GetConnectorName() } authSettings = webclient.WebConfigAuthSettings{ - Providers: authProviders, - SecondFactor: types.LegacySecondFactorFromSecondFactors(cap.GetSecondFactors()), - LocalAuthEnabled: cap.GetAllowLocalAuth(), - AllowPasswordless: cap.GetAllowPasswordless(), - AuthType: authType, - PreferredLocalMFA: cap.GetPreferredLocalMFA(), - LocalConnectorName: localConnectorName, - PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), - MOTD: cap.GetMessageOfTheDay(), + Providers: authProviders, + SecondFactor: types.LegacySecondFactorFromSecondFactors(cap.GetSecondFactors()), + LocalAuthEnabled: cap.GetAllowLocalAuth(), + AllowPasswordless: cap.GetAllowPasswordless(), + AuthType: authType, + DefaultConnectorName: defaultConnectorName, + PreferredLocalMFA: cap.GetPreferredLocalMFA(), + LocalConnectorName: localConnectorName, + PrivateKeyPolicy: cap.GetPrivateKeyPolicy(), + MOTD: cap.GetMessageOfTheDay(), } } diff --git a/lib/web/resources.go b/lib/web/resources.go index c3ba816cd7eef..f6f3807dac72f 100644 --- a/lib/web/resources.go +++ b/lib/web/resources.go @@ -34,6 +34,7 @@ import ( "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth" + "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/httplib" @@ -218,19 +219,35 @@ func (h *Handler) getGithubConnectorsHandle(w http.ResponseWriter, r *http.Reque return nil, trace.Wrap(err) } - authPref, err := clt.GetAuthPreference(r.Context()) + defaultConnectorName, defaultConnectorType, err := ProcessDefaultConnector(r.Context(), clt, connectors) if err != nil { - return nil, trace.Wrap(err, "failed to get auth preference") + return nil, trace.Wrap(err) + } + + return &ui.ListAuthConnectorsResponse{ + DefaultConnectorName: defaultConnectorName, + DefaultConnectorType: defaultConnectorType, + Connectors: connectors, + }, nil +} + +// ProcessDefaultConnector returns the default connector type and validates that the provided connectors list contains the default connector that is set in the auth preference. +// If it isn't, it will return a fallback connector which should be used as the default, as well as update the actual auth preference to refect the change. +func ProcessDefaultConnector(ctx context.Context, clt authclient.ClientI, connectors []ui.ResourceItem) (connectorName string, connectorType string, err error) { + authPref, err := clt.GetAuthPreference(ctx) + if err != nil { + return "", "", trace.Wrap(err, "failed to get auth preference") } defaultConnectorName := authPref.GetConnectorName() defaultConnectorType := authPref.GetType() + if len(connectors) == 0 || defaultConnectorType == constants.Local { // If there are no connectors or the default is already local, default to 'local' as the default connector. defaultConnectorType = constants.Local defaultConnectorName = "" } else { - // Ensure that the default connector in the auth preference exists in the list. + // Ensure that the default connector set in the auth preference exists in the list. found := false for _, c := range connectors { if c.Name == defaultConnectorName && c.Kind == defaultConnectorType { @@ -245,22 +262,18 @@ func (h *Handler) getGithubConnectorsHandle(w http.ResponseWriter, r *http.Reque } } - // Update the auth preference to reflect any changes. + // If the default connector we are returning here is different from the initial, also update the actual auth preference so that it's in sync. if defaultConnectorName != authPref.GetConnectorName() || defaultConnectorType != authPref.GetType() { authPref.SetConnectorName(defaultConnectorName) authPref.SetType(defaultConnectorType) - _, err = clt.UpsertAuthPreference(r.Context(), authPref) + _, err = clt.UpsertAuthPreference(ctx, authPref) if err != nil { - return nil, trace.Wrap(err, "failed to set fallback auth preference") + return "", "", trace.Wrap(err, "failed to set fallback auth preference") } } - return &ui.ListAuthConnectorsResponse{ - DefaultConnectorName: defaultConnectorName, - DefaultConnectorType: defaultConnectorType, - Connectors: connectors, - }, nil + return defaultConnectorName, defaultConnectorType, nil } func getGithubConnectors(ctx context.Context, clt resourcesAPIGetter) ([]ui.ResourceItem, error) { @@ -283,6 +296,26 @@ func (h *Handler) deleteGithubConnector(w http.ResponseWriter, r *http.Request, return nil, trace.Wrap(err) } + authPref, err := clt.GetAuthPreference(r.Context()) + if err != nil { + return nil, trace.Wrap(err, "failed to get auth preference") + } + + defaultConnectorName := authPref.GetConnectorName() + defaultConnectorType := authPref.GetType() + // If the connector being deleted is the default, have the auth preference fallback to another connector. + if defaultConnectorType == constants.Github && defaultConnectorName == connectorName { + connectors, err := getGithubConnectors(r.Context(), clt) + if err != nil { + return nil, trace.Wrap(err) + } + + _, _, err = ProcessDefaultConnector(r.Context(), clt, connectors) + if err != nil { + return nil, trace.Wrap(err) + } + } + return OK(), nil } diff --git a/web/packages/teleport/src/AuthConnectors/AuthConnectors.tsx b/web/packages/teleport/src/AuthConnectors/AuthConnectors.tsx index 25fae481612e4..ca8145a680ed2 100644 --- a/web/packages/teleport/src/AuthConnectors/AuthConnectors.tsx +++ b/web/packages/teleport/src/AuthConnectors/AuthConnectors.tsx @@ -91,17 +91,20 @@ export function AuthConnectors() { ); const [setDefaultAttempt, updateDefaultConnector] = useAsync( - async (connector: DefaultAuthConnector) => { - const originalDefault = defaultConnector; - setDefaultConnector(connector); - ctx.resourceService.setDefaultAuthConnector(connector).catch(err => { - // Revert back to the original default if the operation failed. - setDefaultConnector(originalDefault); - throw err; - }); - } + async (connector: DefaultAuthConnector) => + await ctx.resourceService.setDefaultAuthConnector(connector) ); + function onUpdateDefaultConnector(connector: DefaultAuthConnector) { + const originalDefault = defaultConnector; + setDefaultConnector(connector); + updateDefaultConnector(connector).catch(err => { + // Revert back to the original default if the operation failed. + setDefaultConnector(originalDefault); + throw err; + }); + } + function remove(name: string) { return ctx.resourceService .deleteGithubConnector(name) @@ -179,7 +182,7 @@ export function AuthConnectors() { items={items} onDelete={resources.remove} defaultConnector={defaultConnector} - setAsDefault={updateDefaultConnector} + setAsDefault={onUpdateDefaultConnector} /> )} diff --git a/web/packages/teleport/src/Login/useLogin.ts b/web/packages/teleport/src/Login/useLogin.ts index 2a3a6f4126e4f..c1c0d40e9a850 100644 --- a/web/packages/teleport/src/Login/useLogin.ts +++ b/web/packages/teleport/src/Login/useLogin.ts @@ -36,6 +36,7 @@ export default function useLogin() { const authProviders = cfg.getAuthProviders(); const auth2faType = cfg.getAuth2faType(); + const defaultConnectorName = cfg.getDefaultConnectorName(); const isLocalAuthEnabled = cfg.getLocalAuthFlag(); const motd = cfg.getMotd(); const [showMotd, setShowMotd] = useState(() => { @@ -121,12 +122,18 @@ export default function useLogin() { history.push(ssoUri, true); } + // Move the default connector to the front of the list so that it shows up at the top. + const sortedProviders = moveToFront( + authProviders, + p => p.name === defaultConnectorName + ); + return { attempt, onLogin, checkingValidSession, onLoginWithSso, - authProviders, + authProviders: sortedProviders, auth2faType, preferredMfaType: cfg.getPreferredMfaType(), isLocalAuthEnabled, @@ -188,3 +195,19 @@ export type State = ReturnType & { isRecoveryEnabled?: boolean; onRecover?: (isRecoverPassword: boolean) => void; }; + +/** + * moveToFront returns a copy of an array with the element that matches the condition to the front of it. + */ +function moveToFront(arr: T[], condition: (item: T) => boolean): T[] { + console.log(arr); + const copy = [...arr]; + const index = copy.findIndex(condition); + + if (index > 0) { + const [item] = copy.splice(index, 1); + copy.unshift(item); + } + + return copy; +} diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts index 254254ecf6ebb..3d25bd56380b7 100644 --- a/web/packages/teleport/src/config.ts +++ b/web/packages/teleport/src/config.ts @@ -125,6 +125,8 @@ const cfg = { providers: [] as AuthProvider[], second_factor: 'off' as Auth2faType, authType: 'local' as AuthType, + /** defaultConnectorName is the name of the default connector from the cluster's auth preferences. This will empty if the auth type is "local" */ + defaultConnectorName: '', preferredLocalMfa: '' as PreferredMfaType, // motd is the message of the day, displayed to users before login. motd: '', @@ -429,7 +431,7 @@ const cfg = { msTeamsAppZipPath: '/v1/webapi/sites/:clusterId/plugins/:plugin/files/msteams_app.zip', - defaultConnectorPath: '/v1/webapi/defaultconnector', + defaultConnectorPath: '/v1/webapi/authconnector/default', yaml: { parse: '/v1/webapi/yaml/parse/:kind', @@ -503,6 +505,10 @@ const cfg = { return cfg.auth ? cfg.auth.second_factor : null; }, + getDefaultConnectorName() { + return cfg.auth ? cfg.auth.defaultConnectorName : ''; + }, + getPreferredMfaType() { return cfg.auth ? cfg.auth.preferredLocalMfa : null; }, diff --git a/web/packages/teleport/src/services/resources/resource.ts b/web/packages/teleport/src/services/resources/resource.ts index 6cd98d706178a..b8b2bfe639b6d 100644 --- a/web/packages/teleport/src/services/resources/resource.ts +++ b/web/packages/teleport/src/services/resources/resource.ts @@ -20,6 +20,7 @@ import cfg, { UrlListRolesParams, UrlResourcesParams } from 'teleport/config'; import api from 'teleport/services/api'; import { ResourcesResponse, UnifiedResource } from '../agents'; +import auth, { MfaChallengeScope } from '../auth/auth'; import { DefaultAuthConnector, makeResource, @@ -67,8 +68,19 @@ class ResourceService { })); } - setDefaultAuthConnector(req: DefaultAuthConnector | { type: 'local' }) { - return api.put(cfg.api.defaultConnectorPath, req); + async setDefaultAuthConnector(req: DefaultAuthConnector | { type: 'local' }) { + // This is an admin action that needs an mfa challenge with reuse allowed. + const challenge = await auth.getMfaChallenge({ + scope: MfaChallengeScope.ADMIN_ACTION, + allowReuse: true, + isMfaRequiredRequest: { + admin_action: {}, + }, + }); + + const challengeResponse = await auth.getMfaChallengeResponse(challenge); + + return api.put(cfg.api.defaultConnectorPath, req, challengeResponse); } async fetchRoles(params?: UrlListRolesParams): Promise<{