Path: blob/main/components/dashboard/src/teams/sso/SSOConfigForm.tsx
2501 views
/**1* Copyright (c) 2023 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { FC, useCallback } from "react";7import { InputWithCopy } from "../../components/InputWithCopy";8import { InputField } from "../../components/forms/InputField";9import { TextInputField } from "../../components/forms/TextInputField";10import { gitpodHostUrl } from "../../service/service";11import { useOnBlurError } from "../../hooks/use-onblur-error";12import isURL from "validator/lib/isURL";13import { useCurrentOrg } from "../../data/organizations/orgs-query";14import { useUpsertOIDCClientMutation } from "../../data/oidc-clients/upsert-oidc-client-mutation";15import { Subheading } from "../../components/typography/headings";16import { CheckboxInputField } from "../../components/forms/CheckboxInputField";1718type Props = {19config: SSOConfig;20readOnly?: boolean;21onChange: (config: Partial<SSOConfig>) => void;22};2324export const SSOConfigForm: FC<Props> = ({ config, readOnly = false, onChange }) => {25const redirectUrl = gitpodHostUrl.with({ pathname: `/iam/oidc/callback` }).toString();2627const issuerError = useOnBlurError(`Please enter a valid URL.`, isValidIssuer(config.issuer));28const clientIdError = useOnBlurError("Client ID is missing.", isValidClientID(config.clientId));29const clientSecretError = useOnBlurError("Client Secret is missing.", isValidClientSecret(config.clientSecret));3031return (32<>33<Subheading>34<strong>1.</strong> Add the following <strong>redirect URI</strong> to your identity provider's35configuration.36</Subheading>3738<InputField>39<InputWithCopy value={redirectUrl} tip="Copy the redirect URI to clipboard" />40</InputField>4142<Subheading className="mt-8">43<strong>2.</strong> Find the information below from your identity provider.44</Subheading>4546<TextInputField47label="Issuer URL"48value={config.issuer}49placeholder={"e.g. https://accounts.google.com"}50error={issuerError.message}51disabled={readOnly}52onBlur={issuerError.onBlur}53onChange={(val) => onChange({ issuer: val })}54/>5556<TextInputField57label="Client ID"58value={config.clientId}59error={clientIdError.message}60disabled={readOnly}61onBlur={clientIdError.onBlur}62onChange={(val) => onChange({ clientId: val })}63/>6465<TextInputField66label="Client Secret"67type="password"68value={config.clientSecret}69error={clientSecretError.message}70disabled={readOnly}71onBlur={clientSecretError.onBlur}72onChange={(val) => onChange({ clientSecret: val })}73/>7475<CheckboxInputField76label="Use PKCE"77checked={config.usePKCE}78disabled={readOnly}79onChange={(val) => onChange({ usePKCE: val })}80/>8182<Subheading className="mt-8">83<strong>3.</strong> Restrict available accounts in your Identity Providers.84<a85href="https://www.gitpod.io/docs/enterprise/setup-gitpod/configure-sso#restrict-available-accounts-in-your-identity-providers"86target="_blank"87rel="noreferrer noopener"88className="gp-link"89>90Learn more91</a>92.93</Subheading>9495<InputField label="CEL Expression (optional)">96<textarea97style={{ height: "160px" }}98className="w-full resize-none"99value={config.celExpression}100onChange={(val) => onChange({ celExpression: val.target.value })}101/>102</InputField>103</>104);105};106107export type SSOConfig = {108id?: string;109issuer: string;110clientId: string;111clientSecret: string;112celExpression?: string;113usePKCE: boolean;114};115116export const ssoConfigReducer = (state: SSOConfig, action: Partial<SSOConfig>) => {117return { ...state, ...action };118};119120export const isValid = (state: SSOConfig) => {121return isValidIssuer(state.issuer) && isValidClientID(state.clientId) && isValidClientSecret(state.clientSecret);122};123124const isValidIssuer = (issuer: SSOConfig["issuer"]) => {125return issuer.trim().length > 0 && isURL(issuer);126};127128const isValidClientID = (clientID: SSOConfig["clientId"]) => {129return clientID.trim().length > 0;130};131132const isValidClientSecret = (clientSecret: SSOConfig["clientSecret"]) => {133return clientSecret.trim().length > 0;134};135136export const useSaveSSOConfig = () => {137const { data: org } = useCurrentOrg();138const upsertClientConfig = useUpsertOIDCClientMutation();139140const save = useCallback(141async (ssoConfig: SSOConfig) => {142if (upsertClientConfig.isLoading) {143throw new Error("Already saving");144}145if (!org) {146throw new Error("No current org selected");147}148149if (!isValid(ssoConfig)) {150throw new Error("Invalid SSO config");151}152153const trimmedIssuer = ssoConfig.issuer.trim();154const trimmedClientId = ssoConfig.clientId.trim();155const trimmedClientSecret = ssoConfig.clientSecret.trim();156const trimmedCelExpression = ssoConfig.celExpression?.trim();157158return upsertClientConfig.mutateAsync({159config: !ssoConfig.id160? {161organizationId: org.id,162oauth2Config: {163clientId: trimmedClientId,164clientSecret: trimmedClientSecret,165celExpression: trimmedCelExpression,166usePkce: ssoConfig.usePKCE,167},168oidcConfig: {169issuer: trimmedIssuer,170},171}172: {173id: ssoConfig.id,174organizationId: org.id,175oauth2Config: {176clientId: trimmedClientId,177// TODO: determine how we should handle when user doesn't change their secret178clientSecret: trimmedClientSecret.toLowerCase() === "redacted" ? "" : trimmedClientSecret,179celExpression: trimmedCelExpression,180usePkce: ssoConfig.usePKCE,181},182oidcConfig: {183issuer: trimmedIssuer,184},185},186});187},188[org, upsertClientConfig],189);190191return {192save,193isLoading: upsertClientConfig.isLoading,194isError: upsertClientConfig.isError,195error: upsertClientConfig.error,196};197};198199200