= () => {\n const styles = useStyles2(getStyles);\n return (\n \n \n \n Configuration required\n \n \n You have no authentication configuration created at the moment.\n \n \n Refer to the documentation on how to configure authentication\n \n
\n );\n};\n\nconst getStyles = (theme: GrafanaTheme2) => {\n return {\n container: css({\n display: 'flex',\n flexDirection: 'column',\n gap: theme.spacing(2),\n backgroundColor: theme.colors.background.secondary,\n borderRadius: theme.shape.radius.default,\n padding: theme.spacing(3),\n width: 'max-content',\n margin: theme.spacing(3, 'auto'),\n }),\n };\n};\n\nexport default ConfigureAuthCTA;\n","import React from 'react';\n\nimport { isIconName } from '@grafana/data';\nimport { Badge, Card, Icon } from '@grafana/ui';\n\nimport { UIMap } from '../constants';\nimport { getProviderUrl } from '../utils/url';\n\ntype Props = {\n providerId: string;\n enabled: boolean;\n configPath?: string;\n authType?: string;\n onClick?: () => void;\n};\n\nexport function ProviderCard({ providerId, enabled, configPath, authType, onClick }: Props) {\n //@ts-expect-error\n const url = getProviderUrl({ configPath, id: providerId });\n const [iconName, displayName] = UIMap[providerId] || ['lock', providerId.toUpperCase()];\n return (\n \n {displayName}\n {authType}\n {isIconName(iconName) && (\n \n \n \n )}\n \n \n \n \n );\n}\n","import React, { JSX, useEffect } from 'react';\nimport { connect, ConnectedProps } from 'react-redux';\n\nimport { reportInteraction } from '@grafana/runtime';\nimport { Grid, TextLink } from '@grafana/ui';\nimport { Page } from 'app/core/components/Page/Page';\nimport { StoreState } from 'app/types';\n\nimport ConfigureAuthCTA from './components/ConfigureAuthCTA';\nimport { ProviderCard } from './components/ProviderCard';\nimport { loadSettings } from './state/actions';\n\nimport { getRegisteredAuthProviders } from './index';\n\ninterface OwnProps {}\n\nexport type Props = OwnProps & ConnectedProps;\n\nfunction mapStateToProps(state: StoreState) {\n const { isLoading, providerStatuses, providers } = state.authConfig;\n return {\n isLoading,\n providerStatuses,\n providers,\n };\n}\n\nconst mapDispatchToProps = {\n loadSettings,\n};\n\nconst connector = connect(mapStateToProps, mapDispatchToProps);\n\nexport const AuthConfigPageUnconnected = ({\n providerStatuses,\n isLoading,\n loadSettings,\n providers,\n}: Props): JSX.Element => {\n useEffect(() => {\n loadSettings();\n }, [loadSettings]);\n\n const authProviders = getRegisteredAuthProviders();\n const availableProviders = authProviders.filter((p) => !providerStatuses[p.id]?.hide);\n const onProviderCardClick = (providerType: string) => {\n reportInteraction('authentication_ui_provider_clicked', { provider: providerType });\n };\n\n const providerList = availableProviders.length\n ? [\n ...availableProviders.map((p) => ({\n provider: p.id,\n settings: { ...providerStatuses[p.id], configPath: p.configPath, type: p.type },\n })),\n ...providers,\n ]\n : providers;\n return (\n \n Manage your auth settings and configure single sign-on. Find out more in our{' '}\n \n documentation\n \n .\n >\n }\n >\n \n {!providerList.length ? (\n \n ) : (\n \n {providerList\n // Temporarily filter out providers that don't have the UI implemented\n .filter(({ provider }) => !['grafana_com'].includes(provider))\n .map(({ provider, settings }) => (\n onProviderCardClick(provider)}\n //@ts-expect-error Remove legacy types\n configPath={settings.configPath}\n />\n ))}\n \n )}\n \n \n );\n};\n\nexport default connector(AuthConfigPageUnconnected);\n","import { css } from '@emotion/css';\nimport history from 'history';\nimport React, { useEffect, useState } from 'react';\nimport { Prompt, Redirect } from 'react-router-dom';\n\nimport { Button, Modal } from '@grafana/ui';\n\nexport interface Props {\n confirmRedirect?: boolean;\n onDiscard: () => void;\n /** Extra check to invoke when location changes.\n * Could be useful in multistep forms where each step has a separate URL\n */\n onLocationChange?: (location: history.Location) => void;\n}\n\n/**\n * Component handling redirects when a form has unsaved changes.\n * Page reloads are handled in useEffect via beforeunload event.\n * URL navigation is handled by react-router's components since it does not trigger beforeunload event.\n */\nexport const FormPrompt = ({ confirmRedirect, onDiscard, onLocationChange }: Props) => {\n const [modalIsOpen, setModalIsOpen] = useState(false);\n const [blockedLocation, setBlockedLocation] = useState(null);\n const [changesDiscarded, setChangesDiscarded] = useState(false);\n\n useEffect(() => {\n const onBeforeUnload = (e: BeforeUnloadEvent) => {\n if (confirmRedirect) {\n e.preventDefault();\n e.returnValue = '';\n }\n };\n window.addEventListener('beforeunload', onBeforeUnload);\n return () => {\n window.removeEventListener('beforeunload', onBeforeUnload);\n };\n }, [confirmRedirect]);\n\n // Returning 'false' from this function will prevent navigation to the next URL\n const handleRedirect = (location: history.Location) => {\n // Do not show the unsaved changes modal if only the URL params have changed\n const currentPath = window.location.pathname;\n const nextPath = location.pathname;\n if (currentPath === nextPath) {\n return true;\n }\n\n const locationChangeCheck = onLocationChange?.(location);\n\n let blockRedirect = confirmRedirect && !changesDiscarded;\n if (locationChangeCheck !== undefined) {\n blockRedirect = blockRedirect && locationChangeCheck;\n }\n\n if (blockRedirect) {\n setModalIsOpen(true);\n setBlockedLocation(location);\n return false;\n }\n\n if (locationChangeCheck) {\n onDiscard();\n }\n\n return true;\n };\n\n const onBackToForm = () => {\n setModalIsOpen(false);\n setBlockedLocation(null);\n };\n\n const onDiscardChanges = () => {\n setModalIsOpen(false);\n setChangesDiscarded(true);\n onDiscard();\n };\n\n return (\n <>\n \n {blockedLocation && changesDiscarded && }\n \n >\n );\n};\n\ninterface UnsavedChangesModalProps {\n onDiscard: () => void;\n onBackToForm: () => void;\n isOpen: boolean;\n}\n\nconst UnsavedChangesModal = ({ onDiscard, onBackToForm, isOpen }: UnsavedChangesModalProps) => {\n return (\n \n Changes that you made may not be saved.
\n \n \n \n \n \n );\n};\n","import { SelectableValue } from '@grafana/data';\n\nexport function isSelectableValue(value: unknown): value is SelectableValue[] {\n return Array.isArray(value) && value.every((v) => typeof v === 'object' && v !== null && 'value' in v);\n}\n","import React from 'react';\nimport { validate as uuidValidate } from 'uuid';\n\nimport { TextLink } from '@grafana/ui';\nimport { contextSrv } from 'app/core/core';\n\nimport { FieldData, SSOProvider, SSOSettingsField } from './types';\nimport { isSelectableValue } from './utils/guards';\nimport { isUrlValid } from './utils/url';\n\n/** Map providers to their settings */\nexport const fields: Record> = {\n github: ['name', 'clientId', 'clientSecret', 'teamIds', 'allowedOrganizations'],\n google: ['name', 'clientId', 'clientSecret', 'allowedDomains'],\n gitlab: ['name', 'clientId', 'clientSecret', 'allowedOrganizations', 'teamIds'],\n azuread: ['name', 'clientId', 'clientSecret', 'authUrl', 'tokenUrl', 'scopes', 'allowedGroups', 'allowedDomains'],\n okta: [\n 'name',\n 'clientId',\n 'clientSecret',\n 'authUrl',\n 'tokenUrl',\n 'apiUrl',\n 'roleAttributePath',\n 'allowedGroups',\n 'allowedDomains',\n ],\n};\n\ntype Section = Record<\n SSOProvider['provider'],\n Array<{\n name: string;\n id: string;\n hidden?: boolean;\n fields: SSOSettingsField[];\n }>\n>;\n\nexport const sectionFields: Section = {\n generic_oauth: [\n {\n name: 'General settings',\n id: 'general',\n fields: [\n 'name',\n 'clientId',\n 'clientSecret',\n 'authStyle',\n 'scopes',\n 'authUrl',\n 'tokenUrl',\n 'apiUrl',\n 'allowSignUp',\n 'autoLogin',\n 'signoutRedirectUrl',\n ],\n },\n {\n name: 'User mapping',\n id: 'user',\n fields: [\n 'nameAttributePath',\n 'loginAttributePath',\n 'emailAttributeName',\n 'emailAttributePath',\n 'idTokenAttributeName',\n 'roleAttributePath',\n 'roleAttributeStrict',\n 'allowAssignGrafanaAdmin',\n 'skipOrgRoleSync',\n ],\n },\n {\n name: 'Extra security measures',\n id: 'extra',\n fields: [\n 'allowedOrganizations',\n 'allowedDomains',\n 'defineAllowedGroups',\n { name: 'allowedGroups', dependsOn: 'defineAllowedGroups' },\n { name: 'groupsAttributePath', dependsOn: 'defineAllowedGroups' },\n 'defineAllowedTeamsIds',\n { name: 'teamIds', dependsOn: 'defineAllowedTeamsIds' },\n { name: 'teamsUrl', dependsOn: 'defineAllowedTeamsIds' },\n { name: 'teamIdsAttributePath', dependsOn: 'defineAllowedTeamsIds' },\n 'usePkce',\n 'useRefreshToken',\n ],\n },\n {\n name: 'TLS',\n id: 'tls',\n fields: ['tlsSkipVerifyInsecure', 'tlsClientCert', 'tlsClientKey', 'tlsClientCa'],\n },\n ],\n};\n\n/**\n * List all the fields that can be used in the form\n */\nexport function fieldMap(provider: string): Record {\n return {\n clientId: {\n label: 'Client Id',\n type: 'text',\n description: 'The client Id of your OAuth2 app.',\n validation: {\n required: true,\n message: 'This field is required',\n },\n },\n clientSecret: {\n label: 'Client secret',\n type: 'secret',\n description: 'The client secret of your OAuth2 app.',\n },\n allowedOrganizations: {\n label: 'Allowed organizations',\n type: 'select',\n description:\n 'List of comma- or space-separated organizations. The user should be a member \\n' +\n 'of at least one organization to log in.',\n multi: true,\n allowCustomValue: true,\n options: [],\n placeholder: 'Enter organizations (my-team, myteam...) and press Enter to add',\n },\n allowedDomains: {\n label: 'Allowed domains',\n type: 'select',\n description:\n 'List of comma- or space-separated domains. The user should belong to at least \\n' + 'one domain to log in.',\n multi: true,\n allowCustomValue: true,\n options: [],\n },\n authUrl: {\n label: 'Auth URL',\n type: 'text',\n description: 'The authorization endpoint of your OAuth2 provider.',\n validation: {\n required: true,\n validate: (value) => {\n return isUrlValid(value);\n },\n message: 'This field is required and must be a valid URL.',\n },\n },\n authStyle: {\n label: 'Auth style',\n type: 'select',\n description: 'It determines how client_id and client_secret are sent to Oauth2 provider. Default is AutoDetect.',\n multi: false,\n options: [\n { value: 'AutoDetect', label: 'AutoDetect' },\n { value: 'InParams', label: 'InParams' },\n { value: 'InHeader', label: 'InHeader' },\n ],\n defaultValue: { value: 'AutoDetect', label: 'AutoDetect' },\n },\n tokenUrl: {\n label: 'Token URL',\n type: 'text',\n description: 'The token endpoint of your OAuth2 provider.',\n validation: {\n required: true,\n validate: (value) => {\n return isUrlValid(value);\n },\n message: 'This field is required and must be a valid URL.',\n },\n },\n scopes: {\n label: 'Scopes',\n type: 'select',\n description: 'List of comma- or space-separated OAuth2 scopes.',\n multi: true,\n allowCustomValue: true,\n options: [],\n },\n allowedGroups: {\n label: 'Allowed groups',\n type: 'select',\n description: (\n <>\n List of comma- or space-separated groups. The user should be a member of at least one group to log in.{' '}\n {provider === 'generic_oauth' &&\n 'If you configure allowed_groups, you must also configure groups_attribute_path.'}\n >\n ),\n multi: true,\n allowCustomValue: true,\n options: [],\n validation:\n provider === 'azuread'\n ? {\n validate: (value) => {\n if (typeof value === 'string') {\n return uuidValidate(value);\n }\n if (isSelectableValue(value)) {\n return value.every((v) => v?.value && uuidValidate(v.value));\n }\n return true;\n },\n message: 'Allowed groups must be Object Ids.',\n }\n : undefined,\n },\n apiUrl: {\n label: 'API URL',\n type: 'text',\n description: (\n <>\n The user information endpoint of your OAuth2 provider. Information returned by this endpoint must be\n compatible with{' '}\n \n OpenID UserInfo\n \n .\n >\n ),\n validation: {\n required: false,\n validate: (value) => {\n if (typeof value !== 'string') {\n return false;\n }\n\n if (value.length) {\n return isUrlValid(value);\n }\n\n return true;\n },\n message: 'This field must be a valid URL if set.',\n },\n },\n roleAttributePath: {\n label: 'Role attribute path',\n description: 'JMESPath expression to use for Grafana role lookup.',\n type: 'text',\n validation: {\n required: false,\n },\n },\n name: {\n label: 'Display name',\n description:\n 'Will be displayed on the login page as \"Sign in with ...\". Helpful if you use more than one identity providers or SSO protocols.',\n type: 'text',\n },\n allowSignUp: {\n label: 'Allow sign up',\n description: 'If not enabled, only existing Grafana users can log in using OAuth.',\n type: 'switch',\n },\n autoLogin: {\n label: 'Auto login',\n description: 'Log in automatically, skipping the login screen.',\n type: 'switch',\n },\n signoutRedirectUrl: {\n label: 'Sign out redirect URL',\n description: 'The URL to redirect the user to after signing out from Grafana.',\n type: 'text',\n validation: {\n required: false,\n },\n },\n emailAttributeName: {\n label: 'Email attribute name',\n description: 'Name of the key to use for user email lookup within the attributes map of OAuth2 ID token.',\n type: 'text',\n },\n emailAttributePath: {\n label: 'Email attribute path',\n description: 'JMESPath expression to use for user email lookup from the user information.',\n type: 'text',\n },\n nameAttributePath: {\n label: 'Name attribute path',\n description:\n 'JMESPath expression to use for user name lookup from the user ID token. \\n' +\n 'This name will be used as the user’s display name.',\n type: 'text',\n },\n loginAttributePath: {\n label: 'Login attribute path',\n description: 'JMESPath expression to use for user login lookup from the user ID token.',\n type: 'text',\n },\n idTokenAttributeName: {\n label: 'ID token attribute name',\n description: 'The name of the key used to extract the ID token from the returned OAuth2 token.',\n type: 'text',\n },\n roleAttributeStrict: {\n label: 'Role attribute strict mode',\n description: 'If enabled, denies user login if the Grafana role cannot be extracted using Role attribute path.',\n type: 'switch',\n },\n allowAssignGrafanaAdmin: {\n label: 'Allow assign Grafana admin',\n description: 'If enabled, it will automatically sync the Grafana server administrator role.',\n type: 'switch',\n hidden: !contextSrv.isGrafanaAdmin,\n },\n skipOrgRoleSync: {\n label: 'Skip organization role sync',\n description: 'Prevent synchronizing users’ organization roles from your IdP.',\n type: 'switch',\n },\n defineAllowedGroups: {\n label: 'Define allowed groups',\n type: 'switch',\n },\n defineAllowedTeamsIds: {\n label: 'Define allowed teams ids',\n type: 'switch',\n },\n usePkce: {\n label: 'Use PKCE',\n description: (\n <>\n If enabled, Grafana will use{' '}\n \n Proof Key for Code Exchange (PKCE)\n {' '}\n with the OAuth2 Authorization Code Grant.\n >\n ),\n type: 'checkbox',\n },\n useRefreshToken: {\n label: 'Use refresh token',\n description:\n 'If enabled, Grafana will fetch a new access token using the refresh token provided by the OAuth2 provider.',\n type: 'checkbox',\n },\n tlsClientCa: {\n label: 'TLS client ca',\n description: 'The file path to the trusted certificate authority list. Is not applicable on Grafana Cloud.',\n type: 'text',\n },\n tlsClientCert: {\n label: 'TLS client cert',\n description: 'The file path to the certificate. Is not applicable on Grafana Cloud.',\n type: 'text',\n },\n tlsClientKey: {\n label: 'TLS client key',\n description: 'The file path to the key. Is not applicable on Grafana Cloud.',\n type: 'text',\n },\n tlsSkipVerifyInsecure: {\n label: 'TLS skip verify',\n description:\n 'If enabled, the client accepts any certificate presented by the server and any host \\n' +\n 'name in that certificate. You should only use this for testing, because this mode leaves \\n' +\n 'SSL/TLS susceptible to man-in-the-middle attacks.',\n type: 'switch',\n },\n groupsAttributePath: {\n label: 'Groups attribute path',\n description:\n 'JMESPath expression to use for user group lookup. If you configure allowed_groups, \\n' +\n 'you must also configure groups_attribute_path.',\n type: 'text',\n },\n teamsUrl: {\n label: 'Teams URL',\n description: (\n <>\n The URL used to query for Team Ids. If not set, the default value is /teams.{' '}\n {provider === 'generic_oauth' &&\n 'If you configure teams_url, you must also configure team_ids_attribute_path.'}\n >\n ),\n type: 'text',\n validation: {\n validate: (value, formValues) => {\n let result = true;\n if (formValues.teamIds.length) {\n result = !!value;\n }\n\n if (typeof value === 'string' && value.length) {\n result = isUrlValid(value);\n }\n return result;\n },\n message: 'This field must be set if Team Ids are configured and must be a valid URL.',\n },\n },\n teamIdsAttributePath: {\n label: 'Team Ids attribute path',\n description:\n 'The JMESPath expression to use for Grafana Team Id lookup within the results returned by the teams_url endpoint.',\n type: 'text',\n validation: {\n validate: (value, formValues) => {\n if (formValues.teamIds.length) {\n return !!value;\n }\n return true;\n },\n message: 'This field must be set if Team Ids are configured.',\n },\n },\n teamIds: {\n label: 'Team Ids',\n type: 'select',\n description: (\n <>\n {provider === 'github' ? 'Integer' : 'String'} list of Team Ids. If set, the user must be a member of one of\n the given teams to log in.{' '}\n {provider === 'generic_oauth' &&\n 'If you configure team_ids, you must also configure teams_url and team_ids_attribute_path.'}\n >\n ),\n multi: true,\n allowCustomValue: true,\n options: [],\n placeholder: 'Enter Team Ids and press Enter to add',\n validation:\n provider === 'github'\n ? {\n validate: (value) => {\n if (typeof value === 'string') {\n return isNumeric(value);\n }\n if (isSelectableValue(value)) {\n return value.every((v) => v?.value && isNumeric(v.value));\n }\n return true;\n },\n message: 'Team Ids must be numbers.',\n }\n : undefined,\n },\n };\n}\n\n// Check if a string contains only numeric values\nfunction isNumeric(value: string) {\n return /^-?\\d+$/.test(value);\n}\n","import { css } from '@emotion/css';\nimport React, { useEffect, useState } from 'react';\nimport { UseFormReturn, Controller } from 'react-hook-form';\n\nimport { Checkbox, Field, Input, SecretInput, Select, Switch, useTheme2 } from '@grafana/ui';\n\nimport { fieldMap } from './fields';\nimport { SSOProviderDTO, SSOSettingsField } from './types';\nimport { isSelectableValue } from './utils/guards';\n\ninterface FieldRendererProps\n extends Pick, 'register' | 'control' | 'watch' | 'setValue' | 'unregister'> {\n field: SSOSettingsField;\n errors: UseFormReturn['formState']['errors'];\n secretConfigured: boolean;\n provider: string;\n}\n\nexport const FieldRenderer = ({\n field,\n register,\n errors,\n watch,\n setValue,\n control,\n unregister,\n secretConfigured,\n provider,\n}: FieldRendererProps) => {\n const [isSecretConfigured, setIsSecretConfigured] = useState(secretConfigured);\n const isDependantField = typeof field !== 'string';\n const name = isDependantField ? field.name : field;\n const parentValue = isDependantField ? watch(field.dependsOn) : null;\n const fieldData = fieldMap(provider)[name];\n const theme = useTheme2();\n // Unregister a field that depends on a toggle to clear its data\n useEffect(() => {\n if (isDependantField) {\n if (!parentValue) {\n unregister(name);\n }\n }\n }, [unregister, name, parentValue, isDependantField]);\n\n if (!field) {\n console.log('missing field:', name);\n return null;\n }\n\n if (!!fieldData.hidden) {\n return null;\n }\n\n // Dependant field means the field depends on another field's value and shouldn't be rendered if the parent field is false\n if (isDependantField) {\n const parentValue = watch(field.dependsOn);\n if (!parentValue) {\n return null;\n }\n }\n const fieldProps = {\n label: fieldData.label,\n required: !!fieldData.validation?.required,\n invalid: !!errors[name],\n error: fieldData.validation?.message,\n key: name,\n description: fieldData.description,\n defaultValue: fieldData.defaultValue?.value,\n };\n\n switch (fieldData.type) {\n case 'text':\n return (\n \n \n \n );\n case 'secret':\n return (\n \n (\n {\n setIsSecretConfigured(false);\n setValue(name, '');\n }}\n />\n )}\n />\n \n );\n case 'select':\n const watchOptions = watch(name);\n let options = fieldData.options;\n if (!fieldData.options?.length) {\n options = isSelectableValue(watchOptions) ? watchOptions : [];\n }\n return (\n \n {\n return (\n \n );\n case 'switch':\n return (\n \n \n \n );\n case 'checkbox':\n return (\n \n );\n default:\n console.error(`Unknown field type: ${fieldData.type}`);\n return null;\n }\n};\n","import { SelectableValue } from '@grafana/data';\n\nimport { fieldMap, fields } from '../fields';\nimport { FieldData, SSOProvider, SSOProviderDTO } from '../types';\n\nimport { isSelectableValue } from './guards';\n\nexport const emptySettings: SSOProviderDTO = {\n allowAssignGrafanaAdmin: false,\n allowSignUp: false,\n allowedDomains: [],\n allowedGroups: [],\n allowedOrganizations: [],\n apiUrl: '',\n authStyle: '',\n authUrl: '',\n autoLogin: false,\n clientId: '',\n clientSecret: '',\n emailAttributeName: '',\n emailAttributePath: '',\n emptyScopes: false,\n enabled: false,\n extra: {},\n groupsAttributePath: '',\n hostedDomain: '',\n icon: 'shield',\n name: '',\n roleAttributePath: '',\n roleAttributeStrict: false,\n scopes: [],\n signoutRedirectUrl: '',\n skipOrgRoleSync: false,\n teamIds: [],\n teamIdsAttributePath: '',\n teamsUrl: '',\n tlsClientCa: '',\n tlsClientCert: '',\n tlsClientKey: '',\n tlsSkipVerify: false,\n tokenUrl: '',\n type: '',\n usePkce: false,\n useRefreshToken: false,\n};\n\nconst strToValue = (val: string | string[]): SelectableValue[] => {\n if (!val?.length) {\n return [];\n }\n if (Array.isArray(val)) {\n return val.map((v) => ({ label: v, value: v }));\n }\n return val.split(/[\\s,]/).map((s) => ({ label: s, value: s }));\n};\n\nexport function dataToDTO(data?: SSOProvider): SSOProviderDTO {\n if (!data) {\n return emptySettings;\n }\n const arrayFields = getArrayFields(fieldMap(data.provider));\n const settings = { ...data.settings };\n for (const field of arrayFields) {\n //@ts-expect-error\n settings[field] = strToValue(settings[field]);\n }\n //@ts-expect-error\n return settings;\n}\n\nconst valuesToString = (values: Array>) => {\n return values.map(({ value }) => value).join(',');\n};\n\nconst includeRequiredKeysOnly = (\n obj: SSOProviderDTO,\n requiredKeys: Array\n): Partial => {\n if (!requiredKeys) {\n return obj;\n }\n let result: Partial = {};\n for (const key of requiredKeys) {\n //@ts-expect-error\n result[key] = obj[key];\n }\n return result;\n};\n\n// Convert the DTO to the data format used by the API\nexport function dtoToData(dto: SSOProviderDTO, provider: string) {\n const arrayFields = getArrayFields(fieldMap(provider));\n let current: Partial = dto;\n\n if (fields[provider]) {\n current = includeRequiredKeysOnly(dto, [...fields[provider], 'enabled']);\n }\n const settings = { ...current };\n\n for (const field of arrayFields) {\n const value = current[field];\n if (value) {\n if (isSelectableValue(value)) {\n //@ts-expect-error\n settings[field] = valuesToString(value);\n } else if (isSelectableValue([value])) {\n //@ts-expect-error\n settings[field] = value.value;\n }\n }\n }\n return settings;\n}\n\nexport function getArrayFields(obj: Record): Array {\n return Object.entries(obj)\n .filter(([_, value]) => value.type === 'select')\n .map(([key]) => key as keyof SSOProviderDTO);\n}\n","import React, { useState } from 'react';\nimport { useForm } from 'react-hook-form';\n\nimport { AppEvents } from '@grafana/data';\nimport { getAppEvents, getBackendSrv, isFetchError, locationService, reportInteraction } from '@grafana/runtime';\nimport { Box, Button, CollapsableSection, ConfirmModal, Field, LinkButton, Stack, Switch } from '@grafana/ui';\n\nimport { FormPrompt } from '../../core/components/FormPrompt/FormPrompt';\nimport { Page } from '../../core/components/Page/Page';\n\nimport { FieldRenderer } from './FieldRenderer';\nimport { fields, sectionFields } from './fields';\nimport { SSOProvider, SSOProviderDTO } from './types';\nimport { dataToDTO, dtoToData } from './utils/data';\n\nconst appEvents = getAppEvents();\n\ninterface ProviderConfigProps {\n config?: SSOProvider;\n isLoading?: boolean;\n provider: string;\n}\n\nexport const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConfigProps) => {\n const {\n register,\n handleSubmit,\n control,\n reset,\n watch,\n setValue,\n unregister,\n formState: { errors, dirtyFields, isSubmitted },\n } = useForm({ defaultValues: dataToDTO(config), mode: 'onSubmit', reValidateMode: 'onChange' });\n const [isSaving, setIsSaving] = useState(false);\n const providerFields = fields[provider];\n const [submitError, setSubmitError] = useState(false);\n const dataSubmitted = isSubmitted && !submitError;\n const sections = sectionFields[provider];\n const [resetConfig, setResetConfig] = useState(false);\n\n const onSubmit = async (data: SSOProviderDTO) => {\n setIsSaving(true);\n setSubmitError(false);\n const requestData = dtoToData(data, provider);\n try {\n await getBackendSrv().put(\n `/api/v1/sso-settings/${provider}`,\n {\n id: config?.id,\n provider: config?.provider,\n settings: { ...requestData },\n },\n {\n showErrorAlert: false,\n }\n );\n\n reportInteraction('grafana_authentication_ssosettings_saved', {\n provider,\n enabled: requestData.enabled,\n });\n\n appEvents.publish({\n type: AppEvents.alertSuccess.name,\n payload: ['Settings saved'],\n });\n reset(data);\n // Delay redirect so the form state can update\n setTimeout(() => {\n locationService.push(`/admin/authentication`);\n }, 300);\n } catch (error) {\n let message = '';\n if (isFetchError(error)) {\n message = error.data.message;\n } else if (error instanceof Error) {\n message = error.message;\n }\n appEvents.publish({\n type: AppEvents.alertError.name,\n payload: [message],\n });\n setSubmitError(true);\n setIsSaving(false);\n }\n };\n\n const onResetConfig = async () => {\n try {\n await getBackendSrv().delete(`/api/v1/sso-settings/${provider}`, undefined, { showSuccessAlert: false });\n reportInteraction('grafana_authentication_ssosettings_removed', {\n provider,\n });\n\n appEvents.publish({\n type: AppEvents.alertSuccess.name,\n payload: ['Settings reset to defaults'],\n });\n setTimeout(() => {\n locationService.push(`/admin/authentication`);\n });\n } catch (error) {\n let message = '';\n if (isFetchError(error)) {\n message = error.data.message;\n } else if (error instanceof Error) {\n message = error.message;\n }\n appEvents.publish({\n type: AppEvents.alertError.name,\n payload: [message],\n });\n }\n };\n\n return (\n \n \n {resetConfig && (\n \n Are you sure you want to reset this configuration?\n \n After resetting these settings Grafana will use the provider configuration from the system (config\n file/environment variables) if any.\n \n \n }\n confirmText=\"Reset\"\n onDismiss={() => setResetConfig(false)}\n onConfirm={async () => {\n await onResetConfig();\n setResetConfig(false);\n }}\n />\n )}\n \n );\n};\n","import React, { useEffect } from 'react';\nimport { connect, ConnectedProps } from 'react-redux';\n\nimport { NavModelItem } from '@grafana/data';\nimport { Page } from 'app/core/components/Page/Page';\nimport { GrafanaRouteComponentProps } from 'app/core/navigation/types';\n\nimport { StoreState } from '../../types';\n\nimport { ProviderConfigForm } from './ProviderConfigForm';\nimport { UIMap } from './constants';\nimport { loadProviders } from './state/actions';\nimport { SSOProvider } from './types';\n\nconst getPageNav = (config?: SSOProvider): NavModelItem => {\n if (!config) {\n return {\n text: 'Authentication',\n subTitle: 'Configure authentication providers',\n icon: 'shield',\n id: 'authentication',\n };\n }\n\n const providerDisplayName = UIMap[config.provider][1] || config.provider.toUpperCase();\n\n return {\n text: providerDisplayName || '',\n subTitle: `To configure ${providerDisplayName} OAuth2 you must register your application with ${providerDisplayName}. The provider will generate a Client ID and Client Secret for you to use.`,\n icon: config.settings.icon || 'shield',\n id: config.provider,\n };\n};\n\ninterface RouteProps extends GrafanaRouteComponentProps<{ provider: string }> {}\n\nfunction mapStateToProps(state: StoreState, props: RouteProps) {\n const { isLoading, providers } = state.authConfig;\n const { provider } = props.match.params;\n const config = providers.find((config) => config.provider === provider);\n return {\n config,\n isLoading,\n provider,\n };\n}\n\nconst mapDispatchToProps = {\n loadProviders,\n};\n\nconst connector = connect(mapStateToProps, mapDispatchToProps);\nexport type Props = ConnectedProps;\n\n/**\n * Separate the Page logic from the Content logic for easier testing.\n */\nexport const ProviderConfigPage = ({ config, loadProviders, isLoading, provider }: Props) => {\n const pageNav = getPageNav(config);\n\n useEffect(() => {\n loadProviders(provider);\n }, [loadProviders, provider]);\n\n if (!config) {\n return null;\n }\n return (\n \n \n \n );\n};\n\nexport default connector(ProviderConfigPage);\n","import { IconName } from '@grafana/data';\n\nexport const BASE_PATH = 'admin/authentication/';\n\n// TODO Remove when this is available from API\nexport const UIMap: Record = {\n github: ['github', 'GitHub'],\n gitlab: ['gitlab', 'GitLab'],\n google: ['google', 'Google'],\n generic_oauth: ['lock', 'Generic OAuth'],\n grafana_com: ['grafana', 'Grafana.com'],\n azuread: ['microsoft', 'Azure AD'],\n okta: ['okta', 'Okta'],\n};\n","import { lastValueFrom } from 'rxjs';\n\nimport { config, getBackendSrv, isFetchError } from '@grafana/runtime';\nimport { contextSrv } from 'app/core/core';\nimport { AccessControlAction, Settings, ThunkResult, UpdateSettingsQuery } from 'app/types';\n\nimport { getAuthProviderStatus, getRegisteredAuthProviders, SSOProvider } from '..';\nimport { AuthProviderStatus, SettingsError } from '../types';\n\nimport {\n loadingBegin,\n loadingEnd,\n providersLoaded,\n providerStatusesLoaded,\n resetError,\n setError,\n settingsUpdated,\n} from './reducers';\n\nexport function loadSettings(): ThunkResult> {\n return async (dispatch) => {\n if (contextSrv.hasPermission(AccessControlAction.SettingsRead)) {\n dispatch(loadingBegin());\n dispatch(loadProviders());\n const result = await getBackendSrv().get('/api/admin/settings');\n dispatch(settingsUpdated(result));\n await dispatch(loadProviderStatuses());\n dispatch(loadingEnd());\n return result;\n }\n };\n}\n\nexport function loadProviders(provider = ''): ThunkResult> {\n return async (dispatch) => {\n if (!config.featureToggles.ssoSettingsApi) {\n return [];\n }\n const result = await getBackendSrv().get(`/api/v1/sso-settings${provider ? `/${provider}` : ''}`);\n dispatch(providersLoaded(provider ? [result] : result));\n return result;\n };\n}\n\nexport function loadProviderStatuses(): ThunkResult {\n return async (dispatch) => {\n const registeredProviders = getRegisteredAuthProviders();\n const providerStatuses: Record = {};\n const getStatusPromises: Array> = [];\n for (const provider of registeredProviders) {\n getStatusPromises.push(getAuthProviderStatus(provider.id));\n }\n const statuses = await Promise.all(getStatusPromises);\n for (let i = 0; i < registeredProviders.length; i++) {\n const provider = registeredProviders[i];\n providerStatuses[provider.id] = statuses[i];\n }\n dispatch(providerStatusesLoaded(providerStatuses));\n };\n}\n\nexport function saveSettings(data: UpdateSettingsQuery): ThunkResult> {\n return async (dispatch) => {\n if (contextSrv.hasPermission(AccessControlAction.SettingsWrite)) {\n try {\n await lastValueFrom(\n getBackendSrv().fetch({\n url: '/api/admin/settings',\n method: 'PUT',\n data,\n showSuccessAlert: false,\n showErrorAlert: false,\n })\n );\n dispatch(resetError());\n return true;\n } catch (error) {\n console.log(error);\n if (isFetchError(error)) {\n error.isHandled = true;\n const updateErr: SettingsError = {\n message: error.data?.message,\n errors: error.data?.errors,\n };\n dispatch(setError(updateErr));\n return false;\n }\n }\n }\n return false;\n };\n}\n","import { BASE_PATH } from '../constants';\nimport { AuthProviderInfo } from '../types';\n\nexport function getProviderUrl(provider: AuthProviderInfo) {\n return BASE_PATH + (provider.configPath || provider.id);\n}\n\nexport const isUrlValid = (url: unknown): boolean => {\n if (typeof url !== 'string') {\n return false;\n }\n try {\n const parsedUrl = new URL(url);\n return parsedUrl.protocol.includes('http');\n } catch (_) {\n return false;\n }\n};\n","export default /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i;","import REGEX from './regex.js';\n\nfunction validate(uuid) {\n return typeof uuid === 'string' && REGEX.test(uuid);\n}\n\nexport default validate;"],"names":["ConfigureAuthCTA","styles","getStyles","Stack","Icon","Text","TextLink","theme","ProviderCard","providerId","enabled","configPath","authType","onClick","url","iconName","displayName","Card","Badge","mapStateToProps","state","isLoading","providerStatuses","providers","mapDispatchToProps","connector","AuthConfigPageUnconnected","loadSettings","availableProviders","p","onProviderCardClick","providerType","providerList","Page","Grid","provider","settings","FormPrompt","confirmRedirect","onDiscard","onLocationChange","modalIsOpen","setModalIsOpen","blockedLocation","setBlockedLocation","changesDiscarded","setChangesDiscarded","onBeforeUnload","e","handleRedirect","location","currentPath","nextPath","locationChangeCheck","blockRedirect","onBackToForm","onDiscardChanges","UnsavedChangesModal","isOpen","Modal","Button","isSelectableValue","value","v","fields","sectionFields","fieldMap","formValues","result","isNumeric","FieldRenderer","field","register","errors","watch","setValue","control","unregister","secretConfigured","isSecretConfigured","setIsSecretConfigured","isDependantField","name","parentValue","fieldData","fieldProps","Field","Input","ref","SecretInput","watchOptions","options","onChange","invalid","Select","customValue","Switch","Checkbox","emptySettings","strToValue","val","s","dataToDTO","data","arrayFields","getArrayFields","valuesToString","values","includeRequiredKeysOnly","obj","requiredKeys","key","dtoToData","dto","current","_","ProviderConfigForm","config","handleSubmit","reset","dirtyFields","isSubmitted","isSaving","setIsSaving","providerFields","submitError","setSubmitError","dataSubmitted","sections","resetConfig","setResetConfig","onSubmit","requestData","error","message","onResetConfig","section","index","CollapsableSection","Box","event","ConfirmModal","getPageNav","providerDisplayName","props","ProviderConfigPage","loadProviders","pageNav","BASE_PATH","UIMap","dispatch","loadProviderStatuses","registeredProviders","getStatusPromises","statuses","i","saveSettings","updateErr","getProviderUrl","isUrlValid","validate","uuid"],"sourceRoot":""}