Skip to content

Commit

Permalink
CR
Browse files Browse the repository at this point in the history
  • Loading branch information
rudream committed Jan 16, 2025
1 parent 0ff39d6 commit aaccc84
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 47 deletions.
2 changes: 2 additions & 0 deletions api/client/webclient/webconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 13 additions & 22 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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(),
}
}

Expand Down
55 changes: 44 additions & 11 deletions lib/web/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -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
}

Expand Down
23 changes: 13 additions & 10 deletions web/packages/teleport/src/AuthConnectors/AuthConnectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -179,7 +182,7 @@ export function AuthConnectors() {
items={items}
onDelete={resources.remove}
defaultConnector={defaultConnector}
setAsDefault={updateDefaultConnector}
setAsDefault={onUpdateDefaultConnector}
/>
)}
</Box>
Expand Down
25 changes: 24 additions & 1 deletion web/packages/teleport/src/Login/useLogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>(() => {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -188,3 +195,19 @@ export type State = ReturnType<typeof useLogin> & {
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<T>(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;
}
8 changes: 7 additions & 1 deletion web/packages/teleport/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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;
},
Expand Down
16 changes: 14 additions & 2 deletions web/packages/teleport/src/services/resources/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<{
Expand Down

0 comments on commit aaccc84

Please sign in to comment.