mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Password Policy: Validate strong password upon update (#83959)
* add drawer for auth settings * add StrongPasswordField component * Add style to different behaviours * update style for component * add componenet to ChangePasswordForm * pass the event handlers to the child component * add style for label container * expose strong password policy config option to front end * enforce password validation with config option
This commit is contained in:
@@ -264,4 +264,5 @@ export interface AuthSettings {
|
||||
GenericOAuthSkipOrgRoleSync?: boolean;
|
||||
|
||||
disableLogin?: boolean;
|
||||
basicAuthStrongPasswordPolicy?: boolean;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ type FrontendSettingsAuthDTO struct {
|
||||
// Deprecated: this is no longer used and will be removed in Grafana 11
|
||||
OktaSkipOrgRoleSync bool `json:"OktaSkipOrgRoleSync"`
|
||||
|
||||
DisableLogin bool `json:"disableLogin"`
|
||||
DisableLogin bool `json:"disableLogin"`
|
||||
BasicAuthStrongPasswordPolicy bool `json:"basicAuthStrongPasswordPolicy"`
|
||||
}
|
||||
|
||||
type FrontendSettingsBuildInfoDTO struct {
|
||||
|
||||
@@ -322,19 +322,20 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
|
||||
|
||||
oauthProviders := hs.SocialService.GetOAuthInfoProviders()
|
||||
frontendSettings.Auth = dtos.FrontendSettingsAuthDTO{
|
||||
AuthProxyEnableLoginToken: hs.Cfg.AuthProxy.EnableLoginToken,
|
||||
OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync,
|
||||
SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync,
|
||||
LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync,
|
||||
JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuth.SkipOrgRoleSync,
|
||||
GoogleSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GoogleProviderName]),
|
||||
GrafanaComSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GrafanaComProviderName]),
|
||||
GenericOAuthSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GenericOAuthProviderName]),
|
||||
AzureADSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.AzureADProviderName]),
|
||||
GithubSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitHubProviderName]),
|
||||
GitLabSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitlabProviderName]),
|
||||
OktaSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.OktaProviderName]),
|
||||
DisableLogin: hs.Cfg.DisableLogin,
|
||||
AuthProxyEnableLoginToken: hs.Cfg.AuthProxy.EnableLoginToken,
|
||||
OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync,
|
||||
SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync,
|
||||
LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync,
|
||||
JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuth.SkipOrgRoleSync,
|
||||
GoogleSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GoogleProviderName]),
|
||||
GrafanaComSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GrafanaComProviderName]),
|
||||
GenericOAuthSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GenericOAuthProviderName]),
|
||||
AzureADSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.AzureADProviderName]),
|
||||
GithubSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitHubProviderName]),
|
||||
GitLabSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GitlabProviderName]),
|
||||
OktaSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.OktaProviderName]),
|
||||
DisableLogin: hs.Cfg.DisableLogin,
|
||||
BasicAuthStrongPasswordPolicy: hs.Cfg.BasicAuthStrongPasswordPolicy,
|
||||
}
|
||||
|
||||
if hs.pluginsCDNService != nil && hs.pluginsCDNService.IsEnabled() {
|
||||
|
||||
123
public/app/core/components/ValidationLabels/ValidationLabels.tsx
Normal file
123
public/app/core/components/ValidationLabels/ValidationLabels.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Box, Icon, Text, useStyles2 } from '@grafana/ui';
|
||||
import config from 'app/core/config';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
interface StrongPasswordValidation {
|
||||
message: string;
|
||||
validation: (value: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ValidationLabelsProps {
|
||||
strongPasswordValidations: StrongPasswordValidation[];
|
||||
password: string;
|
||||
pristine: boolean;
|
||||
}
|
||||
|
||||
export interface ValidationLabelProps {
|
||||
strongPasswordValidation: StrongPasswordValidation;
|
||||
password: string;
|
||||
pristine: boolean;
|
||||
}
|
||||
|
||||
export const strongPasswordValidations: StrongPasswordValidation[] = [
|
||||
{
|
||||
message: 'At least 12 characters',
|
||||
validation: (value: string) => value.length >= 12,
|
||||
},
|
||||
{
|
||||
message: 'One uppercase letter',
|
||||
validation: (value: string) => /[A-Z]+/.test(value),
|
||||
},
|
||||
{
|
||||
message: 'One lowercase letter',
|
||||
validation: (value: string) => /[a-z]+/.test(value),
|
||||
},
|
||||
{
|
||||
message: 'One number',
|
||||
validation: (value: string) => /[0-9]+/.test(value),
|
||||
},
|
||||
{
|
||||
message: 'One symbol',
|
||||
validation: (value: string) => /[\W]/.test(value),
|
||||
},
|
||||
];
|
||||
|
||||
export const strongPasswordValidationRegister = (value: string) => {
|
||||
return (
|
||||
!config.auth.basicAuthStrongPasswordPolicy ||
|
||||
strongPasswordValidations.every((validation) => validation.validation(value)) ||
|
||||
t(
|
||||
'profile.change-password.strong-password-validation-register',
|
||||
'Password does not comply with the strong password policy'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const ValidationLabels = ({ strongPasswordValidations, password, pristine }: ValidationLabelsProps) => {
|
||||
return (
|
||||
<Box marginBottom={2}>
|
||||
{strongPasswordValidations.map((validation) => (
|
||||
<ValidationLabel
|
||||
key={validation.message}
|
||||
strongPasswordValidation={validation}
|
||||
password={password}
|
||||
pristine={pristine}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const ValidationLabel = ({ strongPasswordValidation, password, pristine }: ValidationLabelProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { basicAuthStrongPasswordPolicy } = config.auth;
|
||||
if (!basicAuthStrongPasswordPolicy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { message, validation } = strongPasswordValidation;
|
||||
const result = password.length > 0 && validation(password);
|
||||
|
||||
const iconName = result || pristine ? 'check' : 'exclamation-triangle';
|
||||
const textColor = result ? 'secondary' : pristine ? 'primary' : 'error';
|
||||
|
||||
let iconClassName = undefined;
|
||||
if (result) {
|
||||
iconClassName = styles.icon.valid;
|
||||
} else if (pristine) {
|
||||
iconClassName = styles.icon.pending;
|
||||
} else {
|
||||
iconClassName = styles.icon.error;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={message} display={'flex'} alignItems={'center'} marginTop={1}>
|
||||
<Icon className={cx(styles.icon.style, iconClassName)} name={iconName} />
|
||||
<Text color={textColor}>{message}</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
icon: {
|
||||
style: css({
|
||||
marginRight: theme.spacing(1),
|
||||
}),
|
||||
valid: css({
|
||||
color: theme.colors.success.text,
|
||||
}),
|
||||
pending: css({
|
||||
color: theme.colors.secondary.text,
|
||||
}),
|
||||
error: css({
|
||||
color: theme.colors.error.text,
|
||||
}),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,12 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Button, Field, Form, HorizontalGroup, LinkButton } from '@grafana/ui';
|
||||
import {
|
||||
ValidationLabels,
|
||||
strongPasswordValidations,
|
||||
strongPasswordValidationRegister,
|
||||
} from 'app/core/components/ValidationLabels/ValidationLabels';
|
||||
import config from 'app/core/config';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { UserDTO } from 'app/types';
|
||||
@@ -17,6 +22,10 @@ export interface Props {
|
||||
}
|
||||
|
||||
export const ChangePasswordForm = ({ user, onChangePassword, isSaving }: Props) => {
|
||||
const [displayValidationLabels, setDisplayValidationLabels] = useState(false);
|
||||
const [pristine, setPristine] = useState(true);
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
|
||||
const { disableLoginForm } = config;
|
||||
const authSource = user.authLabels?.length && user.authLabels[0];
|
||||
|
||||
@@ -69,9 +78,14 @@ export const ChangePasswordForm = ({ user, onChangePassword, isSaving }: Props)
|
||||
<PasswordField
|
||||
id="new-password"
|
||||
autoComplete="new-password"
|
||||
onFocus={() => setDisplayValidationLabels(true)}
|
||||
value={newPassword}
|
||||
{...register('newPassword', {
|
||||
onBlur: () => setPristine(false),
|
||||
onChange: (e) => setNewPassword(e.target.value),
|
||||
required: t('profile.change-password.new-password-required', 'New password is required'),
|
||||
validate: {
|
||||
strongPasswordValidationRegister,
|
||||
confirm: (v) =>
|
||||
v === getValues().confirmNew ||
|
||||
t('profile.change-password.passwords-must-match', 'Passwords must match'),
|
||||
@@ -85,7 +99,13 @@ export const ChangePasswordForm = ({ user, onChangePassword, isSaving }: Props)
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{displayValidationLabels && (
|
||||
<ValidationLabels
|
||||
pristine={pristine}
|
||||
password={newPassword}
|
||||
strongPasswordValidations={strongPasswordValidations}
|
||||
/>
|
||||
)}
|
||||
<Field
|
||||
label={t('profile.change-password.confirm-password-label', 'Confirm password')}
|
||||
invalid={!!errors.confirmNew}
|
||||
|
||||
@@ -1208,7 +1208,8 @@
|
||||
"new-password-same-as-old": "New password can't be the same as the old one.",
|
||||
"old-password-label": "Old password",
|
||||
"old-password-required": "Old password is required",
|
||||
"passwords-must-match": "Passwords must match"
|
||||
"passwords-must-match": "Passwords must match",
|
||||
"strong-password-validation-register": "Password does not comply with the strong password policy"
|
||||
}
|
||||
},
|
||||
"public-dashboard": {
|
||||
|
||||
@@ -1208,7 +1208,8 @@
|
||||
"new-password-same-as-old": "Ńęŵ päşşŵőřđ čäʼn'ŧ þę ŧĥę şämę äş ŧĥę őľđ őʼnę.",
|
||||
"old-password-label": "Øľđ päşşŵőřđ",
|
||||
"old-password-required": "Øľđ päşşŵőřđ įş řęqūįřęđ",
|
||||
"passwords-must-match": "Päşşŵőřđş mūşŧ mäŧčĥ"
|
||||
"passwords-must-match": "Päşşŵőřđş mūşŧ mäŧčĥ",
|
||||
"strong-password-validation-register": "Päşşŵőřđ đőęş ʼnőŧ čőmpľy ŵįŧĥ ŧĥę şŧřőʼnģ päşşŵőřđ pőľįčy"
|
||||
}
|
||||
},
|
||||
"public-dashboard": {
|
||||
|
||||
Reference in New Issue
Block a user