Auth: Translate Auth provider form fields (#105059)

* first pass at translating all of auth config

* rename variables

* translate missing phrases

* add more
This commit is contained in:
Josh Hunt
2025-05-08 15:42:30 +01:00
committed by GitHub
parent 310b234fbc
commit ded1aacdb6
4 changed files with 644 additions and 360 deletions
@@ -1,4 +1,4 @@
import { useState } from 'react'; import { useMemo, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { AppEvents } from '@grafana/data'; import { AppEvents } from '@grafana/data';
@@ -22,7 +22,7 @@ import { FormPrompt } from '../../core/components/FormPrompt/FormPrompt';
import { Page } from '../../core/components/Page/Page'; import { Page } from '../../core/components/Page/Page';
import { FieldRenderer } from './FieldRenderer'; import { FieldRenderer } from './FieldRenderer';
import { sectionFields } from './fields'; import { getSectionFields } from './fields';
import { SSOProvider, SSOProviderDTO } from './types'; import { SSOProvider, SSOProviderDTO } from './types';
import { dataToDTO, dtoToData } from './utils/data'; import { dataToDTO, dtoToData } from './utils/data';
@@ -49,7 +49,7 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [submitError, setSubmitError] = useState(false); const [submitError, setSubmitError] = useState(false);
const dataSubmitted = isSubmitted && !submitError; const dataSubmitted = isSubmitted && !submitError;
const sections = sectionFields[provider]; const sections = useMemo(() => getSectionFields()[provider], [provider]);
const [resetConfig, setResetConfig] = useState(false); const [resetConfig, setResetConfig] = useState(false);
const additionalActionsMenu = ( const additionalActionsMenu = (
@@ -211,7 +211,13 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf
onClick={() => onSaveAttempt(true)} onClick={() => onSaveAttempt(true)}
variant={isEnabled ? 'secondary' : undefined} variant={isEnabled ? 'secondary' : undefined}
> >
{isSaving ? (isEnabled ? 'Disabling...' : 'Saving...') : isEnabled ? 'Disable' : 'Save and enable'} {isSaving
? isEnabled
? t('auth-config.provider-config-form.disabling', 'Disabling...')
: t('auth-config.provider-config-form.saving', 'Saving...')
: isEnabled
? t('auth-config.provider-config-form.disable', 'Disable')
: t('auth-config.provider-config-form.save-and-enable', 'Save and enable')}
</Button> </Button>
<Button type={'submit'} disabled={isSaving} variant={'secondary'} onClick={() => onSaveAttempt(false)}> <Button type={'submit'} disabled={isSaving} variant={'secondary'} onClick={() => onSaveAttempt(false)}>
+325 -143
View File
@@ -22,10 +22,15 @@ type Section = Record<
}> }>
>; >;
export const sectionFields: Section = { export const getSectionFields = (): Section => {
const generalSettingsLabel = t('auth-config.fields.section-general-settings', 'General settings');
const userMappingLabel = t('auth-config.fields.section-user-mapping', 'User mapping');
const extraSecurityLabel = t('auth-config.fields.section-extra-security', 'Extra security measures');
return {
azuread: [ azuread: [
{ {
name: 'General settings', name: generalSettingsLabel,
id: 'general', id: 'general',
fields: [ fields: [
'name', 'name',
@@ -43,7 +48,7 @@ export const sectionFields: Section = {
], ],
}, },
{ {
name: 'User mapping', name: userMappingLabel,
id: 'user', id: 'user',
fields: ['roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'], fields: ['roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
}, },
@@ -66,7 +71,7 @@ export const sectionFields: Section = {
], ],
generic_oauth: [ generic_oauth: [
{ {
name: 'General settings', name: generalSettingsLabel,
id: 'general', id: 'general',
fields: [ fields: [
'name', 'name',
@@ -84,7 +89,7 @@ export const sectionFields: Section = {
], ],
}, },
{ {
name: 'User mapping', name: userMappingLabel,
id: 'user', id: 'user',
fields: [ fields: [
'nameAttributePath', 'nameAttributePath',
@@ -101,7 +106,7 @@ export const sectionFields: Section = {
], ],
}, },
{ {
name: 'Extra security measures', name: extraSecurityLabel,
id: 'extra', id: 'extra',
fields: [ fields: [
'allowedOrganizations', 'allowedOrganizations',
@@ -124,17 +129,23 @@ export const sectionFields: Section = {
], ],
google: [ google: [
{ {
name: 'General settings', name: generalSettingsLabel,
id: 'general', id: 'general',
fields: ['name', 'clientId', 'clientSecret', 'scopes', 'allowSignUp', 'autoLogin', 'signoutRedirectUrl'], fields: ['name', 'clientId', 'clientSecret', 'scopes', 'allowSignUp', 'autoLogin', 'signoutRedirectUrl'],
}, },
{ {
name: 'User mapping', name: userMappingLabel,
id: 'user', id: 'user',
fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'], fields: [
'roleAttributePath',
'roleAttributeStrict',
'orgMapping',
'allowAssignGrafanaAdmin',
'skipOrgRoleSync',
],
}, },
{ {
name: 'Extra security measures', name: extraSecurityLabel,
id: 'extra', id: 'extra',
fields: [ fields: [
'validateHd', 'validateHd',
@@ -152,17 +163,23 @@ export const sectionFields: Section = {
], ],
github: [ github: [
{ {
name: 'General settings', name: generalSettingsLabel,
id: 'general', id: 'general',
fields: ['name', 'clientId', 'clientSecret', 'scopes', 'allowSignUp', 'autoLogin', 'signoutRedirectUrl'], fields: ['name', 'clientId', 'clientSecret', 'scopes', 'allowSignUp', 'autoLogin', 'signoutRedirectUrl'],
}, },
{ {
name: 'User mapping', name: userMappingLabel,
id: 'user', id: 'user',
fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'], fields: [
'roleAttributePath',
'roleAttributeStrict',
'orgMapping',
'allowAssignGrafanaAdmin',
'skipOrgRoleSync',
],
}, },
{ {
name: 'Extra security measures', name: extraSecurityLabel,
id: 'extra', id: 'extra',
fields: [ fields: [
'allowedOrganizations', 'allowedOrganizations',
@@ -179,17 +196,23 @@ export const sectionFields: Section = {
], ],
gitlab: [ gitlab: [
{ {
name: 'General settings', name: generalSettingsLabel,
id: 'general', id: 'general',
fields: ['name', 'clientId', 'clientSecret', 'scopes', 'allowSignUp', 'autoLogin', 'signoutRedirectUrl'], fields: ['name', 'clientId', 'clientSecret', 'scopes', 'allowSignUp', 'autoLogin', 'signoutRedirectUrl'],
}, },
{ {
name: 'User mapping', name: userMappingLabel,
id: 'user', id: 'user',
fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'], fields: [
'roleAttributePath',
'roleAttributeStrict',
'orgMapping',
'allowAssignGrafanaAdmin',
'skipOrgRoleSync',
],
}, },
{ {
name: 'Extra security measures', name: extraSecurityLabel,
id: 'extra', id: 'extra',
fields: [ fields: [
'allowedDomains', 'allowedDomains',
@@ -205,7 +228,7 @@ export const sectionFields: Section = {
], ],
okta: [ okta: [
{ {
name: 'General settings', name: generalSettingsLabel,
id: 'general', id: 'general',
fields: [ fields: [
'name', 'name',
@@ -221,7 +244,7 @@ export const sectionFields: Section = {
], ],
}, },
{ {
name: 'User mapping', name: userMappingLabel,
id: 'user', id: 'user',
fields: [ fields: [
'roleAttributePath', 'roleAttributePath',
@@ -233,7 +256,7 @@ export const sectionFields: Section = {
], ],
}, },
{ {
name: 'Extra security measures', name: extraSecurityLabel,
id: 'extra', id: 'extra',
fields: [ fields: [
'allowedDomains', 'allowedDomains',
@@ -248,85 +271,138 @@ export const sectionFields: Section = {
}, },
], ],
}; };
};
// These field names should not be translated because they refer to specific technical terminology.
// We put them in variables so they can be referred to in otherwise translated descriptions and not
// risk being translated.
const clientIDLabel = 'Client ID';
const clientSecretLabel = 'Client secret';
const scopesLabel = 'Scopes';
const openIDConnectDiscoveryLabel = 'OpenID Connect Discovery URL';
const authURLLabel = 'Auth URL';
const tokenURLLabel = 'Token URL';
const apiURLLabel = 'API URL';
const jmesPathLabel = 'JMESPath';
/** /**
* List all the fields that can be used in the form * List all the fields that can be used in the form
*/ */
export function fieldMap(provider: string): Record<string, FieldData> { export function fieldMap(provider: string): Record<string, FieldData> {
const orgMappingLabel = t('auth-config.fields.organization-mapping-label', 'Organization mapping');
const orgAttributePathLabel = t(
'auth-config.fields.organization-attribute-path-label',
'Organization attribute path'
);
const teamsURLLabel = t('auth-config.fields.teams-url-label', 'Teams URL');
const teamIDsAttributePathLabel = t('auth-config.fields.team-ids-attribute-path-label', 'Team IDs attribute path');
const allowedGroupsLabel = t('auth-config.fields.allowed-groups-label', 'Allowed groups');
const groupsAttributePathLabel = t('auth-config.fields.groups-attribute-path-label', 'Groups attribute path');
const teamIDsLabel = t('auth-config.fields.team-ids-label', 'Team IDs');
const allowedDomainsLabel = t('auth-config.fields.allowed-domains-label', 'Allowed domains');
return { return {
clientAuthentication: { clientAuthentication: {
label: 'Client authentication', label: t('auth-config.fields.client-authentication-label', 'Client authentication'),
type: 'select', type: 'select',
description: 'The client authentication method used to authenticate to the token endpoint.', description: t(
'auth-config.fields.client-authentication-description',
'The client authentication method used to authenticate to the token endpoint.'
),
multi: false, multi: false,
options: clientAuthenticationOptions(provider), options: clientAuthenticationOptions(provider),
defaultValue: { value: 'none', label: 'None' }, defaultValue: { value: 'none', label: 'None' },
validation: { validation: {
required: true, required: true,
message: 'This field is required', message: t('auth-config.fields.required', 'This field is required'),
}, },
}, },
clientId: { clientId: {
label: 'Client Id', label: clientIDLabel,
type: 'text', type: 'text',
description: 'The client Id of your OAuth2 app.', description: t('auth-config.fields.client-id-description', 'The {{ clientIDLabel }} of your OAuth2 app.', {
clientIDLabel,
}),
validation: { validation: {
required: true, required: true,
message: 'This field is required', message: t('auth-config.fields.required', 'This field is required'),
}, },
}, },
clientSecret: { clientSecret: {
label: 'Client secret', label: clientSecretLabel,
type: 'secret', type: 'secret',
description: 'The client secret of your OAuth2 app.', description: t(
'auth-config.fields.client-secret-description',
'The {{ clientSecretLabel }} of your OAuth2 app.',
{
clientSecretLabel,
}
),
}, },
managedIdentityClientId: { managedIdentityClientId: {
label: 'FIC managed identity client Id', label: t('auth-config.fields.managed-identity-client-id-label', 'FIC managed identity client ID'),
type: 'text', type: 'text',
description: 'The managed identity client Id of the federated identity credential of your OAuth2 app.', description: t(
'auth-config.fields.managed-identity-client-id-description',
'The managed identity client ID of the federated identity credential of your OAuth2 app.'
),
}, },
federatedCredentialAudience: { federatedCredentialAudience: {
label: 'FIC audience', label: t('auth-config.fields.federated-credential-audience-label', 'FIC audience'),
type: 'text', type: 'text',
description: 'The audience of the federated identity credential of your OAuth2 app.', description: t(
'auth-config.fields.federated-credential-audience-description',
'The audience of the federated identity credential of your OAuth2 app.'
),
}, },
allowedOrganizations: { allowedOrganizations: {
label: 'Allowed organizations', label: t('auth-config.fields.allowed-organizations-label', 'Allowed organizations'),
type: 'select', type: 'select',
description: description: t(
'List of comma- or space-separated organizations. The user should be a member \n' + 'auth-config.fields.allowed-organizations-description',
'of at least one organization to log in.', 'List of comma- or space-separated organizations. The user should be a member \nof at least one organization to log in.'
),
multi: true, multi: true,
allowCustomValue: true, allowCustomValue: true,
options: [], options: [],
placeholder: 'Enter organizations (my-team, myteam...) and press Enter to add', placeholder: t(
'auth-config.fields.allowed-organizations-placeholder',
'Enter organizations (my-team, myteam...) and press Enter to add'
),
}, },
allowedDomains: { allowedDomains: {
label: 'Allowed domains', label: allowedDomainsLabel,
type: 'select', type: 'select',
description: description: t(
'List of comma- or space-separated domains. The user should belong to at least \n' + 'one domain to log in.', 'auth-config.fields.allowed-domains-description',
'List of comma- or space-separated domains. The user should belong to at least \none domain to log in.'
),
multi: true, multi: true,
allowCustomValue: true, allowCustomValue: true,
options: [], options: [],
}, },
authUrl: { authUrl: {
label: 'Auth URL', label: authURLLabel,
type: 'text', type: 'text',
description: 'The authorization endpoint of your OAuth2 provider.', description: t('auth-config.fields.auth-url-description', 'The authorization endpoint of your OAuth2 provider.'),
validation: { validation: {
required: true, required: true,
validate: (value) => { validate: (value) => {
return isUrlValid(value); return isUrlValid(value);
}, },
message: 'This field is required and must be a valid URL.', message: t('auth-config.fields.auth-url-required', 'This field is required and must be a valid URL.'),
}, },
}, },
authStyle: { authStyle: {
label: 'Auth style', label: t('auth-config.fields.auth-style-label', 'Auth style'),
type: 'select', type: 'select',
description: description: t(
'It determines how "Client Id" and "Client secret" are sent to Oauth2 provider. Default is AutoDetect.', 'auth-config.fields.auth-style-description',
'It determines how "{{ clientIDLabel }}" and "{{ clientSecretLabel }}" are sent to Oauth2 provider. Default is AutoDetect.',
{ clientIDLabel, clientSecretLabel }
),
multi: false, multi: false,
options: [ options: [
{ value: 'AutoDetect', label: 'AutoDetect' }, { value: 'AutoDetect', label: 'AutoDetect' },
@@ -336,27 +412,33 @@ export function fieldMap(provider: string): Record<string, FieldData> {
defaultValue: { value: 'AutoDetect', label: 'AutoDetect' }, defaultValue: { value: 'AutoDetect', label: 'AutoDetect' },
}, },
tokenUrl: { tokenUrl: {
label: 'Token URL', label: tokenURLLabel,
type: 'text', type: 'text',
description: 'The token endpoint of your OAuth2 provider.', description: t('auth-config.fields.token-url-description', 'The token endpoint of your OAuth2 provider.'),
validation: { validation: {
required: true, required: true,
validate: (value) => { validate: (value) => {
return isUrlValid(value); return isUrlValid(value);
}, },
message: 'This field is required and must be a valid URL.', message: t('auth-config.fields.token-url-required', 'This field is required and must be a valid URL.'),
}, },
}, },
scopes: { scopes: {
label: 'Scopes', label: scopesLabel,
type: 'select', type: 'select',
description: 'List of comma- or space-separated OAuth2 scopes.', description: t(
'auth-config.fields.scopes-description',
'List of comma- or space-separated OAuth2 {{ scopesLabel }}.',
{
scopesLabel,
}
),
multi: true, multi: true,
allowCustomValue: true, allowCustomValue: true,
options: [], options: [],
}, },
allowedGroups: { allowedGroups: {
label: 'Allowed groups', label: allowedGroupsLabel,
type: 'select', type: 'select',
description: ( description: (
<> <>
@@ -366,7 +448,8 @@ export function fieldMap(provider: string): Record<string, FieldData> {
{provider === 'generic_oauth' && {provider === 'generic_oauth' &&
t( t(
'auth-config.fields.allowed-groups-description-oauth', 'auth-config.fields.allowed-groups-description-oauth',
'If you configure "Allowed groups", you must also configure "Groups attribute path".' 'If you configure "{{ allowedGroupsLabel }}", you must also configure "{{ groupsAttributePathLabel }}".',
{ allowedGroupsLabel, groupsAttributePathLabel }
)} )}
</> </>
), ),
@@ -385,12 +468,18 @@ export function fieldMap(provider: string): Record<string, FieldData> {
} }
return true; return true;
}, },
message: 'Allowed groups must be Object Ids.', message: t(
'auth-config.fields.allowed-groups-object-ids',
'{{ allowedGroupsLabel }} must be {{ objectIDsField }}.',
{
objectIDsField: 'Object IDs',
}
),
} }
: undefined, : undefined,
}, },
apiUrl: { apiUrl: {
label: 'API URL', label: apiURLLabel,
type: 'text', type: 'text',
description: ( description: (
<Trans i18nKey="auth-config.fields.api-url-description"> <Trans i18nKey="auth-config.fields.api-url-description">
@@ -415,116 +504,159 @@ export function fieldMap(provider: string): Record<string, FieldData> {
return true; return true;
}, },
message: 'This field must be a valid URL if set.', message: t('auth-config.fields.api-url-required', 'This field must be a valid URL if set.'),
}, },
}, },
roleAttributePath: { roleAttributePath: {
label: 'Role attribute path', label: t('auth-config.fields.role-attribute-path-label', 'Role attribute path'),
description: 'JMESPath expression to use for Grafana role lookup.', description: t(
'auth-config.fields.role-attribute-path-description',
'{{ jmesPathLabel }} expression to use for Grafana role lookup.',
{ jmesPathLabel }
),
type: 'text', type: 'text',
validation: { validation: {
required: false, required: false,
}, },
}, },
name: { name: {
label: 'Display name', label: t('auth-config.fields.display-name-label', 'Display name'),
description: description: t(
'Will be displayed on the login page as "Sign in with ...". Helpful if you use more than one identity providers or SSO protocols.', 'auth-config.fields.display-name-description',
'Will be displayed on the login page as "Sign in with ...". Helpful if you use more than one identity providers or SSO protocols.'
),
type: 'text', type: 'text',
}, },
allowSignUp: { allowSignUp: {
label: 'Allow sign up', label: t('auth-config.fields.allow-sign-up-label', 'Allow sign up'),
description: 'If not enabled, only existing Grafana users can log in using OAuth.', description: t(
'auth-config.fields.allow-sign-up-description',
'If not enabled, only existing Grafana users can log in using OAuth.'
),
type: 'switch', type: 'switch',
}, },
autoLogin: { autoLogin: {
label: 'Auto login', label: t('auth-config.fields.auto-login-label', 'Auto login'),
description: 'Log in automatically, skipping the login screen.', description: t('auth-config.fields.auto-login-description', 'Log in automatically, skipping the login screen.'),
type: 'switch', type: 'switch',
}, },
signoutRedirectUrl: { signoutRedirectUrl: {
label: 'Sign out redirect URL', label: t('auth-config.fields.signout-redirect-url-label', 'Sign out redirect URL'),
description: 'The URL to redirect the user to after signing out from Grafana.', description: t(
'auth-config.fields.signout-redirect-url-description',
'The URL to redirect the user to after signing out from Grafana.'
),
type: 'text', type: 'text',
validation: { validation: {
required: false, required: false,
}, },
}, },
emailAttributeName: { emailAttributeName: {
label: 'Email attribute name', label: t('auth-config.fields.email-attribute-name-label', 'Email attribute name'),
description: 'Name of the key to use for user email lookup within the attributes map of OAuth2 ID token.', description: t(
'auth-config.fields.email-attribute-name-description',
'Name of the key to use for user email lookup within the attributes map of OAuth2 ID token.'
),
type: 'text', type: 'text',
}, },
emailAttributePath: { emailAttributePath: {
label: 'Email attribute path', label: t('auth-config.fields.email-attribute-path-label', 'Email attribute path'),
description: 'JMESPath expression to use for user email lookup from the user information.', description: t(
'auth-config.fields.email-attribute-path-description',
'JMESPath expression to use for user email lookup from the user information.'
),
type: 'text', type: 'text',
}, },
nameAttributePath: { nameAttributePath: {
label: 'Name attribute path', label: t('auth-config.fields.name-attribute-path-label', 'Name attribute path'),
description: description: t(
'JMESPath expression to use for user name lookup from the user ID token. \n' + 'auth-config.fields.name-attribute-path-description',
'This name will be used as the user’s display name.', "JMESPath expression to use for user name lookup from the user ID token. \nThis name will be used as the user's display name."
),
type: 'text', type: 'text',
}, },
loginAttributePath: { loginAttributePath: {
label: 'Login attribute path', label: t('auth-config.fields.login-attribute-path-label', 'Login attribute path'),
description: 'JMESPath expression to use for user login lookup from the user ID token.', description: t(
'auth-config.fields.login-attribute-path-description',
'JMESPath expression to use for user login lookup from the user ID token.'
),
type: 'text', type: 'text',
}, },
idTokenAttributeName: { idTokenAttributeName: {
label: 'ID token attribute name', label: t('auth-config.fields.id-token-attribute-name-label', 'ID token attribute name'),
description: 'The name of the key used to extract the ID token from the returned OAuth2 token.', description: t(
'auth-config.fields.id-token-attribute-name-description',
'The name of the key used to extract the ID token from the returned OAuth2 token.'
),
type: 'text', type: 'text',
}, },
roleAttributeStrict: { roleAttributeStrict: {
label: 'Role attribute strict mode', label: t('auth-config.fields.role-attribute-strict-label', 'Role attribute strict mode'),
description: 'If enabled, denies user login if the Grafana role cannot be extracted using Role attribute path.', description: t(
'auth-config.fields.role-attribute-strict-description',
'If enabled, denies user login if the Grafana role cannot be extracted using Role attribute path.'
),
type: 'switch', type: 'switch',
}, },
allowAssignGrafanaAdmin: { allowAssignGrafanaAdmin: {
label: 'Allow assign Grafana admin', label: t('auth-config.fields.allow-assign-grafana-admin-label', 'Allow assign Grafana admin'),
description: 'If enabled, it will automatically sync the Grafana server administrator role.', description: t(
'auth-config.fields.allow-assign-grafana-admin-description',
'If enabled, it will automatically sync the Grafana server administrator role.'
),
type: 'switch', type: 'switch',
hidden: !contextSrv.isGrafanaAdmin, hidden: !contextSrv.isGrafanaAdmin,
}, },
skipOrgRoleSync: { skipOrgRoleSync: {
label: 'Skip organization role sync', label: t('auth-config.fields.skip-org-role-sync-label', 'Skip organization role sync'),
description: 'Prevent synchronizing users’ organization roles from your IdP.', description: t(
'auth-config.fields.skip-org-role-sync-description',
"Prevent synchronizing users' organization roles from your IdP."
),
type: 'switch', type: 'switch',
}, },
orgMapping: { orgMapping: {
label: 'Organization mapping', label: orgMappingLabel,
description: orgMappingDescription(provider), description: orgMappingDescription(provider),
type: 'select', type: 'select',
hidden: !contextSrv.isGrafanaAdmin, hidden: !contextSrv.isGrafanaAdmin,
multi: true, multi: true,
allowCustomValue: true, allowCustomValue: true,
options: [], options: [],
placeholder: 'Enter mappings (my-team:1:Viewer...) and press Enter to add', placeholder: t(
'auth-config.fields.organization-mapping-placeholder',
'Enter mappings (my-team:1:Viewer...) and press Enter to add'
),
}, },
orgAttributePath: { orgAttributePath: {
label: 'Organization attribute path', label: orgAttributePathLabel,
description: description: t(
'JMESPath expression to use for organization lookup. If you configure "Organization mapping", you must also configure "Organization attribute path".', 'auth-config.fields.organization-attribute-path-description',
'JMESPath expression to use for organization lookup. If you configure "{{ orgMappingLabel }}", you must also configure "{{ orgAttributePathLabel }}".',
{ orgMappingLabel, orgAttributePathLabel }
),
type: 'text', type: 'text',
hidden: !(['generic_oauth', 'okta'].includes(provider) && contextSrv.isGrafanaAdmin), hidden: !(['generic_oauth', 'okta'].includes(provider) && contextSrv.isGrafanaAdmin),
}, },
defineAllowedGroups: { defineAllowedGroups: {
label: 'Define allowed groups', label: t('auth-config.fields.define-allowed-groups-label', 'Define allowed groups'),
type: 'switch', type: 'switch',
}, },
defineAllowedTeamsIds: { defineAllowedTeamsIds: {
label: 'Define allowed teams ids', label: t('auth-config.fields.define-allowed-teams-ids-label', 'Define allowed teams IDs'),
type: 'switch', type: 'switch',
}, },
forceUseGraphApi: { forceUseGraphApi: {
label: 'Force use Graph API', label: t('auth-config.fields.force-use-graph-api-label', 'Force use Graph API'),
description: "If enabled, Grafana will fetch the users' groups using the Microsoft Graph API.", description: t(
'auth-config.fields.force-use-graph-api-description',
"If enabled, Grafana will fetch the users' groups using the Microsoft Graph API."
),
type: 'checkbox', type: 'checkbox',
}, },
usePkce: { usePkce: {
label: 'Use PKCE', label: t('auth-config.fields.use-pkce-label', 'Use PKCE'),
description: ( description: (
<Trans i18nKey="auth-config.fields.use-pkce-description"> <Trans i18nKey="auth-config.fields.use-pkce-description">
If enabled, Grafana will use{' '} If enabled, Grafana will use{' '}
@@ -537,55 +669,69 @@ export function fieldMap(provider: string): Record<string, FieldData> {
type: 'checkbox', type: 'checkbox',
}, },
useRefreshToken: { useRefreshToken: {
label: 'Use refresh token', label: t('auth-config.fields.use-refresh-token-label', 'Use refresh token'),
description: description: t(
'If enabled, Grafana will fetch a new access token using the refresh token provided by the OAuth2 provider.', 'auth-config.fields.use-refresh-token-description',
'If enabled, Grafana will fetch a new access token using the refresh token provided by the OAuth2 provider.'
),
type: 'checkbox', type: 'checkbox',
}, },
tlsClientCa: { tlsClientCa: {
label: 'TLS client ca', label: t('auth-config.fields.tls-client-ca-label', 'TLS client CA'),
description: 'The file path to the trusted certificate authority list. Is not applicable on Grafana Cloud.', description: t(
'auth-config.fields.tls-client-ca-description',
'The file path to the trusted certificate authority list. Is not applicable on Grafana Cloud.'
),
type: 'text', type: 'text',
hidden: !config.localFileSystemAvailable, hidden: !config.localFileSystemAvailable,
}, },
tlsClientCert: { tlsClientCert: {
label: 'TLS client cert', label: t('auth-config.fields.tls-client-cert-label', 'TLS client cert'),
description: 'The file path to the certificate. Is not applicable on Grafana Cloud.', description: t(
'auth-config.fields.tls-client-cert-description',
'The file path to the certificate. Is not applicable on Grafana Cloud.'
),
type: 'text', type: 'text',
hidden: !config.localFileSystemAvailable, hidden: !config.localFileSystemAvailable,
}, },
tlsClientKey: { tlsClientKey: {
label: 'TLS client key', label: t('auth-config.fields.tls-client-key-label', 'TLS client key'),
description: 'The file path to the key. Is not applicable on Grafana Cloud.', description: t(
'auth-config.fields.tls-client-key-description',
'The file path to the key. Is not applicable on Grafana Cloud.'
),
type: 'text', type: 'text',
hidden: !config.localFileSystemAvailable, hidden: !config.localFileSystemAvailable,
}, },
tlsSkipVerifyInsecure: { tlsSkipVerifyInsecure: {
label: 'TLS skip verify', label: t('auth-config.fields.tls-skip-verify-label', 'TLS skip verify'),
description: description: t(
'If enabled, the client accepts any certificate presented by the server and any host \n' + 'auth-config.fields.tls-skip-verify-description',
'name in that certificate. You should only use this for testing, because this mode leaves \n' + 'If enabled, the client accepts any certificate presented by the server and any host \nname in that certificate. You should only use this for testing, because this mode leaves \nSSL/TLS susceptible to man-in-the-middle attacks.'
'SSL/TLS susceptible to man-in-the-middle attacks.', ),
type: 'switch', type: 'switch',
}, },
groupsAttributePath: { groupsAttributePath: {
label: 'Groups attribute path', label: groupsAttributePathLabel,
description: description: t(
'JMESPath expression to use for user group lookup. If you configure "Allowed groups", \n' + 'auth-config.fields.groups-attribute-path-description',
'you must also configure "Groups attribute path".', 'JMESPath expression to use for user group lookup. If you configure "{{ allowedGroupsLabel }}", \nyou must also configure "{{ groupsAttributePathLabel }}".',
{ allowedGroupsLabel, groupsAttributePathLabel }
),
type: 'text', type: 'text',
}, },
teamsUrl: { teamsUrl: {
label: 'Teams URL', label: teamsURLLabel,
description: ( description: (
<> <>
<Trans i18nKey="auth-config.fields.teams-url-description"> <Trans i18nKey="auth-config.fields.teams-url-description">
The URL used to query for Team Ids. If not set, the default value is /teams. The URL used to query for Team IDs. If not set, the default value is /teams.
</Trans>{' '} </Trans>{' '}
{provider === 'generic_oauth' && {provider === 'generic_oauth' &&
t( t(
'auth-config.fields.teams-url-description-oauth', 'auth-config.fields.teams-url-description-oauth',
'If you configure "Teams URL", you must also configure "Team Ids attribute path".' 'If you configure "{{ teamsURLLabel }}", you must also configure "{{ teamIDsAttributePathLabel }}".',
{ teamsURLLabel, teamIDsAttributePathLabel }
)} )}
</> </>
), ),
@@ -602,13 +748,20 @@ export function fieldMap(provider: string): Record<string, FieldData> {
} }
return result; return result;
}, },
message: 'This field must be set if Team Ids are configured and must be a valid URL.', message: t(
'auth-config.fields.teams-url-required',
'This field must be set if "{{ teamIDsLabel }}" are configured and must be a valid URL.',
{ teamIDsLabel }
),
}, },
}, },
teamIdsAttributePath: { teamIdsAttributePath: {
label: 'Team Ids attribute path', label: teamIDsAttributePathLabel,
description: description: t(
'The JMESPath expression to use for Grafana Team Id lookup within the results returned by the "Teams URL" endpoint.', 'auth-config.fields.team-ids-attribute-path-description',
'The JMESPath expression to use for Grafana Team ID lookup within the results returned by the "{{ teamsURLLabel }}" endpoint.',
{ teamsURLLabel }
),
type: 'text', type: 'text',
validation: { validation: {
validate: (value, formValues) => { validate: (value, formValues) => {
@@ -617,32 +770,36 @@ export function fieldMap(provider: string): Record<string, FieldData> {
} }
return true; return true;
}, },
message: 'This field must be set if Team Ids are configured.', message: t(
'auth-config.fields.team-ids-attribute-path-required',
'This field must be set if "{{ teamIDsLabel }}" are configured.',
{ teamIDsLabel }
),
}, },
}, },
teamIds: { teamIds: {
label: 'Team Ids', label: teamIDsLabel,
type: 'select', type: 'select',
description: ( description: (
<> <>
{provider === 'github' {provider === 'github'
? t('auth-config.fields.team-ids-github', 'Integer list of Team Ids.') ? t('auth-config.fields.team-ids-github', 'Integer list of Team IDs.')
: t('auth-config.fields.team-ids-other', 'String list of Team Ids.')}{' '} : t('auth-config.fields.team-ids-other', 'String list of Team IDs.')}{' '}
<Trans i18nKey="auth-config.fields.team-ids-description"> <Trans i18nKey="auth-config.fields.team-ids-description">
If set, the user must be a member of one of the given teams to log in. If set, the user must be a member of one of the given teams to log in.
</Trans>{' '} </Trans>{' '}
{provider === 'generic_oauth' && {provider === 'generic_oauth' &&
t( t(
'auth-config.fields.team-ids-description-oauth', 'auth-config.fields.team-ids-description-oauth',
'If you configure "{{teamIds}}", you must also configure "{{teamsUrl}}" and "{{teamIdsAttributePath}}".', 'If you configure "{{ teamIDsLabel }}", you must also configure "{{ teamsURLLabel }}" and "{{ teamIDsAttributePathLabel }}".',
{ teamIds: 'Team Ids', teamsUrl: 'Teams URL', teamIdsAttributePath: 'Team Ids attribute path' } { teamIDsLabel, teamsURLLabel, teamIDsAttributePathLabel }
)} )}
</> </>
), ),
multi: true, multi: true,
allowCustomValue: true, allowCustomValue: true,
options: [], options: [],
placeholder: 'Enter Team Ids and press Enter to add', placeholder: t('auth-config.fields.team-ids-placeholder', 'Enter Team IDs and press Enter to add'),
validation: validation:
provider === 'github' provider === 'github'
? { ? {
@@ -655,25 +812,34 @@ export function fieldMap(provider: string): Record<string, FieldData> {
} }
return true; return true;
}, },
message: 'Team Ids must be numbers.', message: t('auth-config.fields.team-ids-numbers', 'Team IDs must be numbers.'),
} }
: undefined, : undefined,
}, },
hostedDomain: { hostedDomain: {
label: 'Hosted domain', label: t('auth-config.fields.hosted-domain-label', 'Hosted domain'),
description: 'The domain under which Grafana is hosted and accessible.', description: t(
'auth-config.fields.hosted-domain-description',
'The domain under which Grafana is hosted and accessible.'
),
type: 'text', type: 'text',
}, },
validateHd: { validateHd: {
label: 'Validate hosted domain', label: t('auth-config.fields.validate-hosted-domain-label', 'Validate hosted domain'),
description: description: t(
'If enabled, Grafana will match the Hosted Domain retrieved from the Google ID Token against the Allowed Domains list specified by the user.', 'auth-config.fields.validate-hosted-domain-description',
'If enabled, Grafana will match the Hosted Domain retrieved from the Google ID Token against the "{{ allowedDomainsLabel }}" list specified by the user.',
{ allowedDomainsLabel }
),
type: 'checkbox', type: 'checkbox',
}, },
serverDiscoveryUrl: { serverDiscoveryUrl: {
label: 'OpenID Connect Discovery URL', label: openIDConnectDiscoveryLabel,
description: description: t(
'The .well-known/openid-configuration endpoint for your IdP. The info extracted from this URL will be used to populate the Auth URL, Token URL and API URL fields.', 'auth-config.fields.server-discovery-url-description',
'The .well-known/openid-configuration endpoint for your IdP. The info extracted from this URL will be used to populate the "{{ authURLLabel }}", "{{ tokenURLLabel }}" and "{{ apiURLLabel }}" fields.',
{ authURLLabel, tokenURLLabel, apiURLLabel }
),
type: 'custom', type: 'custom',
content: (setValue) => <ServerDiscoveryField setValue={setValue} />, content: (setValue) => <ServerDiscoveryField setValue={setValue} />,
}, },
@@ -688,24 +854,40 @@ function isNumeric(value: string) {
function orgMappingDescription(provider: string): string { function orgMappingDescription(provider: string): string {
switch (provider) { switch (provider) {
case 'azuread': case 'azuread':
return 'List of "<GroupID>:<OrgIdOrName>:<Role>" mappings.'; return t(
'auth-config.fields.org-mapping-description-azuread',
'List of "<GroupID>:<OrgIdOrName>:<Role>" mappings.'
);
case 'github': case 'github':
return 'List of "<GitHubTeamName>:<OrgIdOrName>:<Role>" mappings.'; return t(
'auth-config.fields.org-mapping-description-github',
'List of "<GitHubTeamName>:<OrgIdOrName>:<Role>" mappings.'
);
case 'gitlab': case 'gitlab':
return 'List of "<GitlabGroupName>:<OrgIdOrName>:<Role>'; return t(
'auth-config.fields.org-mapping-description-gitlab',
'List of "<GitlabGroupName>:<OrgIdOrName>:<Role>" mappings.'
);
case 'google': case 'google':
return 'List of "<GoogleGroupName>:<OrgIdOrName>:<Role>'; return t(
'auth-config.fields.org-mapping-description-google',
'List of "<GoogleGroupName>:<OrgIdOrName>:<Role>" mappings.'
);
default: default:
// Generic OAuth, Okta // Generic OAuth, Okta
return 'List of "<ExternalName>:<OrgIdOrName>:<Role>" mappings.'; return t(
'auth-config.fields.org-mapping-description-generic',
'List of "<ExternalName>:<OrgIdOrName>:<Role>" mappings.'
);
} }
} }
function clientAuthenticationOptions(provider: string): Array<SelectableValue<string>> { function clientAuthenticationOptions(provider: string): Array<SelectableValue<string>> {
// Other options are purposefully not translated
switch (provider) { switch (provider) {
case 'azuread': case 'azuread':
return [ return [
{ value: 'none', label: 'None' }, { value: 'none', label: t('auth-config.fields.client-authentication-none', 'None') },
{ value: 'client_secret_post', label: 'Client secret' }, { value: 'client_secret_post', label: 'Client secret' },
{ value: 'managed_identity', label: 'Managed identity' }, { value: 'managed_identity', label: 'Managed identity' },
]; ];
@@ -1,6 +1,6 @@
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { fieldMap, sectionFields } from '../fields'; import { fieldMap, getSectionFields } from '../fields';
import { FieldData, SSOProvider, SSOProviderDTO } from '../types'; import { FieldData, SSOProvider, SSOProviderDTO } from '../types';
import { isSelectableValue } from './guards'; import { isSelectableValue } from './guards';
@@ -86,7 +86,7 @@ const valuesToString = (values: Array<SelectableValue<string>>) => {
}; };
const getFieldsForProvider = (provider: string) => { const getFieldsForProvider = (provider: string) => {
const sections = sectionFields[provider]; const sections = getSectionFields()[provider];
// include the enabled field because it is not part of the fields defined for providers // include the enabled field because it is not part of the fields defined for providers
const fields = ['enabled']; const fields = ['enabled'];
@@ -133,5 +133,5 @@ export function dtoToData(dto: SSOProviderDTO, provider: string) {
export function getArrayFields(obj: Record<string, FieldData>, providerFields: string[]): Array<keyof SSOProviderDTO> { export function getArrayFields(obj: Record<string, FieldData>, providerFields: string[]): Array<keyof SSOProviderDTO> {
return Object.entries(obj) return Object.entries(obj)
.filter(([key, value]) => providerFields.includes(key) && value.type === 'select') .filter(([key, value]) => providerFields.includes(key) && value.type === 'select')
.map(([key]) => key as keyof SSOProviderDTO); .map(([key]) => key as keyof SSOProviderDTO); // TODO: replace this with a type guard
} }
+102 -6
View File
@@ -2628,16 +2628,109 @@
"refer-documentation-configure-authentication": "Refer to the documentation on how to configure authentication" "refer-documentation-configure-authentication": "Refer to the documentation on how to configure authentication"
}, },
"fields": { "fields": {
"allow-assign-grafana-admin-description": "If enabled, it will automatically sync the Grafana server administrator role.",
"allow-assign-grafana-admin-label": "Allow assign Grafana admin",
"allow-sign-up-description": "If not enabled, only existing Grafana users can log in using OAuth.",
"allow-sign-up-label": "Allow sign up",
"allowed-domains-description": "List of comma- or space-separated domains. The user should belong to at least \none domain to log in.",
"allowed-domains-label": "Allowed domains",
"allowed-groups-description": "List of comma- or space-separated groups. The user should be a member of at least one group to log in.", "allowed-groups-description": "List of comma- or space-separated groups. The user should be a member of at least one group to log in.",
"allowed-groups-description-oauth": "If you configure \"Allowed groups\", you must also configure \"Groups attribute path\".", "allowed-groups-description-oauth": "If you configure \"{{ allowedGroupsLabel }}\", you must also configure \"{{ groupsAttributePathLabel }}\".",
"allowed-groups-label": "Allowed groups",
"allowed-groups-object-ids": "{{ allowedGroupsLabel }} must be {{ objectIDsField }}.",
"allowed-organizations-description": "List of comma- or space-separated organizations. The user should be a member \nof at least one organization to log in.",
"allowed-organizations-label": "Allowed organizations",
"allowed-organizations-placeholder": "Enter organizations (my-team, myteam...) and press Enter to add",
"api-url-description": "The user information endpoint of your OAuth2 provider. Information returned by this endpoint must be compatible with <2>OpenID UserInfo</2>.", "api-url-description": "The user information endpoint of your OAuth2 provider. Information returned by this endpoint must be compatible with <2>OpenID UserInfo</2>.",
"api-url-required": "This field must be a valid URL if set.",
"auth-style-description": "It determines how \"{{ clientIDLabel }}\" and \"{{ clientSecretLabel }}\" are sent to Oauth2 provider. Default is AutoDetect.",
"auth-style-label": "Auth style",
"auth-url-description": "The authorization endpoint of your OAuth2 provider.",
"auth-url-required": "This field is required and must be a valid URL.",
"auto-login-description": "Log in automatically, skipping the login screen.",
"auto-login-label": "Auto login",
"client-authentication-description": "The client authentication method used to authenticate to the token endpoint.",
"client-authentication-label": "Client authentication",
"client-authentication-none": "None",
"client-id-description": "The {{ clientIDLabel }} of your OAuth2 app.",
"client-secret-description": "The {{ clientSecretLabel }} of your OAuth2 app.",
"define-allowed-groups-label": "Define allowed groups",
"define-allowed-teams-ids-label": "Define allowed teams IDs",
"display-name-description": "Will be displayed on the login page as \"Sign in with ...\". Helpful if you use more than one identity providers or SSO protocols.",
"display-name-label": "Display name",
"email-attribute-name-description": "Name of the key to use for user email lookup within the attributes map of OAuth2 ID token.",
"email-attribute-name-label": "Email attribute name",
"email-attribute-path-description": "JMESPath expression to use for user email lookup from the user information.",
"email-attribute-path-label": "Email attribute path",
"federated-credential-audience-description": "The audience of the federated identity credential of your OAuth2 app.",
"federated-credential-audience-label": "FIC audience",
"force-use-graph-api-description": "If enabled, Grafana will fetch the users' groups using the Microsoft Graph API.",
"force-use-graph-api-label": "Force use Graph API",
"groups-attribute-path-description": "JMESPath expression to use for user group lookup. If you configure \"{{ allowedGroupsLabel }}\", \nyou must also configure \"{{ groupsAttributePathLabel }}\".",
"groups-attribute-path-label": "Groups attribute path",
"hosted-domain-description": "The domain under which Grafana is hosted and accessible.",
"hosted-domain-label": "Hosted domain",
"id-token-attribute-name-description": "The name of the key used to extract the ID token from the returned OAuth2 token.",
"id-token-attribute-name-label": "ID token attribute name",
"login-attribute-path-description": "JMESPath expression to use for user login lookup from the user ID token.",
"login-attribute-path-label": "Login attribute path",
"managed-identity-client-id-description": "The managed identity client ID of the federated identity credential of your OAuth2 app.",
"managed-identity-client-id-label": "FIC managed identity client ID",
"name-attribute-path-description": "JMESPath expression to use for user name lookup from the user ID token. \nThis name will be used as the user's display name.",
"name-attribute-path-label": "Name attribute path",
"org-mapping-description-azuread": "List of \"<GroupID>:<OrgIdOrName>:<Role>\" mappings.",
"org-mapping-description-generic": "List of \"<ExternalName>:<OrgIdOrName>:<Role>\" mappings.",
"org-mapping-description-github": "List of \"<GitHubTeamName>:<OrgIdOrName>:<Role>\" mappings.",
"org-mapping-description-gitlab": "List of \"<GitlabGroupName>:<OrgIdOrName>:<Role>\" mappings.",
"org-mapping-description-google": "List of \"<GoogleGroupName>:<OrgIdOrName>:<Role>\" mappings.",
"organization-attribute-path-description": "JMESPath expression to use for organization lookup. If you configure \"{{ orgMappingLabel }}\", you must also configure \"{{ orgAttributePathLabel }}\".",
"organization-attribute-path-label": "Organization attribute path",
"organization-mapping-label": "Organization mapping",
"organization-mapping-placeholder": "Enter mappings (my-team:1:Viewer...) and press Enter to add",
"required": "This field is required",
"role-attribute-path-description": "{{ jmesPathLabel }} expression to use for Grafana role lookup.",
"role-attribute-path-label": "Role attribute path",
"role-attribute-strict-description": "If enabled, denies user login if the Grafana role cannot be extracted using Role attribute path.",
"role-attribute-strict-label": "Role attribute strict mode",
"scopes-description": "List of comma- or space-separated OAuth2 {{ scopesLabel }}.",
"section-extra-security": "Extra security measures",
"section-general-settings": "General settings",
"section-user-mapping": "User mapping",
"server-discovery-url-description": "The .well-known/openid-configuration endpoint for your IdP. The info extracted from this URL will be used to populate the \"{{ authURLLabel }}\", \"{{ tokenURLLabel }}\" and \"{{ apiURLLabel }}\" fields.",
"signout-redirect-url-description": "The URL to redirect the user to after signing out from Grafana.",
"signout-redirect-url-label": "Sign out redirect URL",
"skip-org-role-sync-description": "Prevent synchronizing users' organization roles from your IdP.",
"skip-org-role-sync-label": "Skip organization role sync",
"team-ids-attribute-path-description": "The JMESPath expression to use for Grafana Team ID lookup within the results returned by the \"{{ teamsURLLabel }}\" endpoint.",
"team-ids-attribute-path-label": "Team IDs attribute path",
"team-ids-attribute-path-required": "This field must be set if \"{{ teamIDsLabel }}\" are configured.",
"team-ids-description": "If set, the user must be a member of one of the given teams to log in.", "team-ids-description": "If set, the user must be a member of one of the given teams to log in.",
"team-ids-description-oauth": "If you configure \"{{teamIds}}\", you must also configure \"{{teamsUrl}}\" and \"{{teamIdsAttributePath}}\".", "team-ids-description-oauth": "If you configure \"{{ teamIDsLabel }}\", you must also configure \"{{ teamsURLLabel }}\" and \"{{ teamIDsAttributePathLabel }}\".",
"team-ids-github": "Integer list of Team Ids.", "team-ids-github": "Integer list of Team IDs.",
"team-ids-label": "Team IDs",
"team-ids-numbers": "Team IDs must be numbers.",
"team-ids-other": "String list of Team Ids.", "team-ids-other": "String list of Team Ids.",
"teams-url-description": "The URL used to query for Team Ids. If not set, the default value is /teams.", "team-ids-placeholder": "Enter Team IDs and press Enter to add",
"teams-url-description-oauth": "If you configure \"Teams URL\", you must also configure \"Team Ids attribute path\".", "teams-url-description": "The URL used to query for Team IDs. If not set, the default value is /teams.",
"use-pkce-description": "If enabled, Grafana will use <2>Proof Key for Code Exchange (PKCE)</2> with the OAuth2 Authorization Code Grant." "teams-url-description-oauth": "If you configure \"{{ teamsURLLabel }}\", you must also configure \"{{ teamIDsAttributePathLabel }}\".",
"teams-url-label": "Teams URL",
"teams-url-required": "This field must be set if \"{{ teamIDsLabel }}\" are configured and must be a valid URL.",
"tls-client-ca-description": "The file path to the trusted certificate authority list. Is not applicable on Grafana Cloud.",
"tls-client-ca-label": "TLS client CA",
"tls-client-cert-description": "The file path to the certificate. Is not applicable on Grafana Cloud.",
"tls-client-cert-label": "TLS client cert",
"tls-client-key-description": "The file path to the key. Is not applicable on Grafana Cloud.",
"tls-client-key-label": "TLS client key",
"tls-skip-verify-description": "If enabled, the client accepts any certificate presented by the server and any host \nname in that certificate. You should only use this for testing, because this mode leaves \nSSL/TLS susceptible to man-in-the-middle attacks.",
"tls-skip-verify-label": "TLS skip verify",
"token-url-description": "The token endpoint of your OAuth2 provider.",
"token-url-required": "This field is required and must be a valid URL.",
"use-pkce-description": "If enabled, Grafana will use <2>Proof Key for Code Exchange (PKCE)</2> with the OAuth2 Authorization Code Grant.",
"use-pkce-label": "Use PKCE",
"use-refresh-token-description": "If enabled, Grafana will fetch a new access token using the refresh token provided by the OAuth2 provider.",
"use-refresh-token-label": "Use refresh token",
"validate-hosted-domain-description": "If enabled, Grafana will match the Hosted Domain retrieved from the Google ID Token against the \"{{ allowedDomainsLabel }}\" list specified by the user.",
"validate-hosted-domain-label": "Validate hosted domain"
}, },
"provider-card": { "provider-card": {
"text-badge-enabled": "Enabled", "text-badge-enabled": "Enabled",
@@ -2647,12 +2740,15 @@
"additional-actions-menu": { "additional-actions-menu": {
"label-reset-to-default-values": "Reset to default values" "label-reset-to-default-values": "Reset to default values"
}, },
"disable": "Disable",
"disabling": "Disabling...",
"discard": "Discard", "discard": "Discard",
"enabled-label-enabled": "Enabled", "enabled-label-enabled": "Enabled",
"label-enabled": "Enabled", "label-enabled": "Enabled",
"reset-configuration": "Are you sure you want to reset this configuration?", "reset-configuration": "Are you sure you want to reset this configuration?",
"reset-configuration-description": "After resetting these settings Grafana will use the provider configuration from the system (config file/environment variables) if any.", "reset-configuration-description": "After resetting these settings Grafana will use the provider configuration from the system (config file/environment variables) if any.",
"save": "Save", "save": "Save",
"save-and-enable": "Save and enable",
"saving": "Saving...", "saving": "Saving...",
"title-more-actions": "More actions", "title-more-actions": "More actions",
"title-reset": "Reset", "title-reset": "Reset",