mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-01-07 10:20:22 -06:00
Allow setting password policies from admin UI.
This commit is contained in:
@@ -318,10 +318,26 @@ export interface AuthConfig {
|
||||
refreshTokenTtlSec?:
|
||||
| number
|
||||
| undefined;
|
||||
/** / Disables password-based sign-up. */
|
||||
/** / Disables password-based sign-up. Does not affect already registered users. */
|
||||
disablePasswordAuth?:
|
||||
| boolean
|
||||
| undefined;
|
||||
/** / Minimal password length. Defaults to 8. */
|
||||
passwordMinimalLength?:
|
||||
| number
|
||||
| undefined;
|
||||
/** / Password must contain lower and upper-case letters. */
|
||||
passwordMustContainUpperAndLowerCase?:
|
||||
| boolean
|
||||
| undefined;
|
||||
/** / Password must contain digits in addition to alphabetic characters.. */
|
||||
passwordMustContainDigits?:
|
||||
| boolean
|
||||
| undefined;
|
||||
/** / Password must contain special, non-alphanumeric, characters. */
|
||||
passwordMustContainSpecialCharacters?:
|
||||
| boolean
|
||||
| undefined;
|
||||
/** / Map of configured OAuth providers. */
|
||||
oauthProviders: { [key: string]: OAuthProviderConfig };
|
||||
}
|
||||
@@ -940,6 +956,24 @@ export const AuthConfig: MessageFns<AuthConfig> = {
|
||||
if (message.disablePasswordAuth !== undefined && message.disablePasswordAuth !== false) {
|
||||
writer.uint32(24).bool(message.disablePasswordAuth);
|
||||
}
|
||||
if (message.passwordMinimalLength !== undefined && message.passwordMinimalLength !== 0) {
|
||||
writer.uint32(32).uint32(message.passwordMinimalLength);
|
||||
}
|
||||
if (
|
||||
message.passwordMustContainUpperAndLowerCase !== undefined &&
|
||||
message.passwordMustContainUpperAndLowerCase !== false
|
||||
) {
|
||||
writer.uint32(40).bool(message.passwordMustContainUpperAndLowerCase);
|
||||
}
|
||||
if (message.passwordMustContainDigits !== undefined && message.passwordMustContainDigits !== false) {
|
||||
writer.uint32(48).bool(message.passwordMustContainDigits);
|
||||
}
|
||||
if (
|
||||
message.passwordMustContainSpecialCharacters !== undefined &&
|
||||
message.passwordMustContainSpecialCharacters !== false
|
||||
) {
|
||||
writer.uint32(56).bool(message.passwordMustContainSpecialCharacters);
|
||||
}
|
||||
Object.entries(message.oauthProviders).forEach(([key, value]) => {
|
||||
AuthConfig_OauthProvidersEntry.encode({ key: key as any, value }, writer.uint32(90).fork()).join();
|
||||
});
|
||||
@@ -977,6 +1011,38 @@ export const AuthConfig: MessageFns<AuthConfig> = {
|
||||
message.disablePasswordAuth = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 32) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.passwordMinimalLength = reader.uint32();
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 40) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.passwordMustContainUpperAndLowerCase = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (tag !== 48) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.passwordMustContainDigits = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 7: {
|
||||
if (tag !== 56) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.passwordMustContainSpecialCharacters = reader.bool();
|
||||
continue;
|
||||
}
|
||||
case 11: {
|
||||
if (tag !== 90) {
|
||||
break;
|
||||
@@ -1004,6 +1070,18 @@ export const AuthConfig: MessageFns<AuthConfig> = {
|
||||
disablePasswordAuth: isSet(object.disablePasswordAuth)
|
||||
? globalThis.Boolean(object.disablePasswordAuth)
|
||||
: undefined,
|
||||
passwordMinimalLength: isSet(object.passwordMinimalLength)
|
||||
? globalThis.Number(object.passwordMinimalLength)
|
||||
: undefined,
|
||||
passwordMustContainUpperAndLowerCase: isSet(object.passwordMustContainUpperAndLowerCase)
|
||||
? globalThis.Boolean(object.passwordMustContainUpperAndLowerCase)
|
||||
: undefined,
|
||||
passwordMustContainDigits: isSet(object.passwordMustContainDigits)
|
||||
? globalThis.Boolean(object.passwordMustContainDigits)
|
||||
: undefined,
|
||||
passwordMustContainSpecialCharacters: isSet(object.passwordMustContainSpecialCharacters)
|
||||
? globalThis.Boolean(object.passwordMustContainSpecialCharacters)
|
||||
: undefined,
|
||||
oauthProviders: isObject(object.oauthProviders)
|
||||
? Object.entries(object.oauthProviders).reduce<{ [key: string]: OAuthProviderConfig }>((acc, [key, value]) => {
|
||||
acc[key] = OAuthProviderConfig.fromJSON(value);
|
||||
@@ -1024,6 +1102,24 @@ export const AuthConfig: MessageFns<AuthConfig> = {
|
||||
if (message.disablePasswordAuth !== undefined && message.disablePasswordAuth !== false) {
|
||||
obj.disablePasswordAuth = message.disablePasswordAuth;
|
||||
}
|
||||
if (message.passwordMinimalLength !== undefined && message.passwordMinimalLength !== 0) {
|
||||
obj.passwordMinimalLength = Math.round(message.passwordMinimalLength);
|
||||
}
|
||||
if (
|
||||
message.passwordMustContainUpperAndLowerCase !== undefined &&
|
||||
message.passwordMustContainUpperAndLowerCase !== false
|
||||
) {
|
||||
obj.passwordMustContainUpperAndLowerCase = message.passwordMustContainUpperAndLowerCase;
|
||||
}
|
||||
if (message.passwordMustContainDigits !== undefined && message.passwordMustContainDigits !== false) {
|
||||
obj.passwordMustContainDigits = message.passwordMustContainDigits;
|
||||
}
|
||||
if (
|
||||
message.passwordMustContainSpecialCharacters !== undefined &&
|
||||
message.passwordMustContainSpecialCharacters !== false
|
||||
) {
|
||||
obj.passwordMustContainSpecialCharacters = message.passwordMustContainSpecialCharacters;
|
||||
}
|
||||
if (message.oauthProviders) {
|
||||
const entries = Object.entries(message.oauthProviders);
|
||||
if (entries.length > 0) {
|
||||
@@ -1044,6 +1140,10 @@ export const AuthConfig: MessageFns<AuthConfig> = {
|
||||
message.authTokenTtlSec = object.authTokenTtlSec ?? 0;
|
||||
message.refreshTokenTtlSec = object.refreshTokenTtlSec ?? 0;
|
||||
message.disablePasswordAuth = object.disablePasswordAuth ?? false;
|
||||
message.passwordMinimalLength = object.passwordMinimalLength ?? 0;
|
||||
message.passwordMustContainUpperAndLowerCase = object.passwordMustContainUpperAndLowerCase ?? false;
|
||||
message.passwordMustContainDigits = object.passwordMustContainDigits ?? false;
|
||||
message.passwordMustContainSpecialCharacters = object.passwordMustContainSpecialCharacters ?? false;
|
||||
message.oauthProviders = Object.entries(object.oauthProviders ?? {}).reduce<{ [key: string]: OAuthProviderConfig }>(
|
||||
(acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
|
||||
@@ -323,7 +323,7 @@ function AuthSettingsForm(props: {
|
||||
<div class="flex flex-col gap-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2>Auth Settings</h2>
|
||||
<h2>Token Settings</h2>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
@@ -353,11 +353,21 @@ function AuthSettingsForm(props: {
|
||||
),
|
||||
})}
|
||||
</form.Field>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2>Password Settings</h2>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-4">
|
||||
<form.Field name="disablePasswordAuth">
|
||||
{buildOptionalBoolFormField({
|
||||
label: () => (
|
||||
<div class={labelWidth}>Disable Password Auth</div>
|
||||
<div class={labelWidth}>Disable Password Registration</div>
|
||||
),
|
||||
info: (
|
||||
<p>
|
||||
@@ -368,10 +378,104 @@ function AuthSettingsForm(props: {
|
||||
),
|
||||
})}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="passwordMinimalLength">
|
||||
{buildOptionalNumberFormField({
|
||||
label: () => <div class={labelWidth}>Min Length</div>,
|
||||
info: (
|
||||
<p>
|
||||
Minimal length for new passwords [Default 8]. Does not
|
||||
affect existing registrations unless users choose to
|
||||
change their password.
|
||||
</p>
|
||||
),
|
||||
})}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="passwordMustContainUpperAndLowerCase">
|
||||
{buildOptionalBoolFormField({
|
||||
label: () => (
|
||||
<div class={labelWidth}>
|
||||
Must Contain Upper {"&"} Lower Case
|
||||
</div>
|
||||
),
|
||||
info: (
|
||||
<p>
|
||||
Passwords must contain both, upper and lower case
|
||||
characters. Does not affect existing registrations unless
|
||||
users choose to change their password.
|
||||
</p>
|
||||
),
|
||||
})}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="passwordMustContainDigits">
|
||||
{buildOptionalBoolFormField({
|
||||
label: () => (
|
||||
<div class={labelWidth}>Must Contain Digits</div>
|
||||
),
|
||||
info: (
|
||||
<p>
|
||||
Passwords must additionally contain digits. Does not
|
||||
affect existing registrations unless users choose to
|
||||
change their password.
|
||||
</p>
|
||||
),
|
||||
})}
|
||||
</form.Field>
|
||||
|
||||
<form.Field name="passwordMustContainSpecialCharacters">
|
||||
{buildOptionalBoolFormField({
|
||||
label: () => (
|
||||
<div class={labelWidth}>Must Contain Special</div>
|
||||
),
|
||||
info: (
|
||||
<p>
|
||||
Passwords must additionally contain special, i.e.,
|
||||
non-alphanumeric characters.. Does not affect existing
|
||||
registrations unless users choose to change their
|
||||
password.
|
||||
</p>
|
||||
),
|
||||
})}
|
||||
</form.Field>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2>OAuth Providers</h2>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form.Field name="namedOauthProviders">
|
||||
{(_field) => {
|
||||
return (
|
||||
<Accordion multiple={false} collapsible class="w-full">
|
||||
<For each={values().namedOauthProviders}>
|
||||
{(provider, index) => {
|
||||
// Skip OIDC provider for now until we expand the form to render the extra fields.
|
||||
if (provider.provider.id === OAuthProviderId.OIDC0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProviderSettingsSubForm
|
||||
form={form}
|
||||
index={index()}
|
||||
provider={provider.provider}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Accordion>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2>Public Key</h2>
|
||||
@@ -420,39 +524,6 @@ function AuthSettingsForm(props: {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2>OAuth Providers</h2>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form.Field name="namedOauthProviders">
|
||||
{(_field) => {
|
||||
return (
|
||||
<Accordion multiple={false} collapsible class="w-full">
|
||||
<For each={values().namedOauthProviders}>
|
||||
{(provider, index) => {
|
||||
// Skip OIDC provider for now until we expand the form to render the extra fields.
|
||||
if (provider.provider.id === OAuthProviderId.OIDC0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProviderSettingsSubForm
|
||||
form={form}
|
||||
index={index()}
|
||||
provider={provider.provider}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</Accordion>
|
||||
);
|
||||
}}
|
||||
</form.Field>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<form.Subscribe
|
||||
selector={(state) => ({
|
||||
|
||||
@@ -60,9 +60,21 @@ message AuthConfig {
|
||||
/// Time-to-live in seconds for refresh tokens. Default: 30 days.
|
||||
optional int64 refresh_token_ttl_sec = 2;
|
||||
|
||||
/// Disables password-based sign-up.
|
||||
/// Disables password-based sign-up. Does not affect already registered users.
|
||||
optional bool disable_password_auth = 3;
|
||||
|
||||
/// Minimal password length. Defaults to 8.
|
||||
optional uint32 password_minimal_length = 4;
|
||||
|
||||
/// Password must contain lower and upper-case letters.
|
||||
optional bool password_must_contain_upper_and_lower_case = 5;
|
||||
|
||||
/// Password must contain digits in addition to alphabetic characters..
|
||||
optional bool password_must_contain_digits = 6;
|
||||
|
||||
/// Password must contain special, non-alphanumeric, characters.
|
||||
optional bool password_must_contain_special_characters = 7;
|
||||
|
||||
/// Map of configured OAuth providers.
|
||||
map<string, OAuthProviderConfig> oauth_providers = 11;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,17 @@ pub struct AuthOptions {
|
||||
impl AuthOptions {
|
||||
pub fn from_config(config: AuthConfig) -> Self {
|
||||
return Self {
|
||||
password_options: PasswordOptions::default(),
|
||||
password_options: PasswordOptions {
|
||||
min_length: config.password_minimal_length.unwrap_or(8) as usize,
|
||||
max_length: 128,
|
||||
must_contain_upper_and_lower_case: config
|
||||
.password_must_contain_upper_and_lower_case
|
||||
.unwrap_or(false),
|
||||
must_contain_digits: config.password_must_contain_digits.unwrap_or(false),
|
||||
must_contain_special_characters: config
|
||||
.password_must_contain_special_characters
|
||||
.unwrap_or(false),
|
||||
},
|
||||
oauth_providers: build_oauth_providers_from_config(config).unwrap_or_else(|err| {
|
||||
error!("Failed to derive configured OAuth providers from config: {err}");
|
||||
return Default::default();
|
||||
|
||||
@@ -9,7 +9,7 @@ pub struct PasswordOptions {
|
||||
pub min_length: usize,
|
||||
pub max_length: usize,
|
||||
|
||||
pub must_contain_lower_and_upper_case: bool,
|
||||
pub must_contain_upper_and_lower_case: bool,
|
||||
pub must_contain_digits: bool,
|
||||
pub must_contain_special_characters: bool,
|
||||
}
|
||||
@@ -19,7 +19,7 @@ impl Default for PasswordOptions {
|
||||
return PasswordOptions {
|
||||
min_length: 8,
|
||||
max_length: 128,
|
||||
must_contain_lower_and_upper_case: false,
|
||||
must_contain_upper_and_lower_case: false,
|
||||
must_contain_digits: false,
|
||||
must_contain_special_characters: false,
|
||||
};
|
||||
@@ -43,11 +43,16 @@ pub fn validate_password_policy(
|
||||
return Err(AuthError::BadRequest("Password too long"));
|
||||
}
|
||||
|
||||
if opts.must_contain_digits && !password.chars().any(|x| x.is_numeric()) {
|
||||
return Err(AuthError::BadRequest("Must contain numeric"));
|
||||
if opts.must_contain_digits {
|
||||
if !password.chars().any(|x| x.is_numeric()) {
|
||||
return Err(AuthError::BadRequest("Must contain digits"));
|
||||
}
|
||||
if password.chars().all(|x| x.is_numeric()) {
|
||||
return Err(AuthError::BadRequest("Must contain non-digits"));
|
||||
}
|
||||
}
|
||||
|
||||
if opts.must_contain_lower_and_upper_case
|
||||
if opts.must_contain_upper_and_lower_case
|
||||
&& !(password.chars().any(|x| x.is_lowercase()) && password.chars().any(|x| x.is_uppercase()))
|
||||
{
|
||||
return Err(AuthError::BadRequest("Must contain lower and upper case"));
|
||||
@@ -170,7 +175,7 @@ mod tests {
|
||||
// lower-upper
|
||||
let options = PasswordOptions {
|
||||
min_length: 2,
|
||||
must_contain_lower_and_upper_case: true,
|
||||
must_contain_upper_and_lower_case: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -189,6 +194,7 @@ mod tests {
|
||||
|
||||
assert!(test("aa", &options).is_err());
|
||||
assert!(test("2a", &options).is_ok());
|
||||
assert!(test("22", &options).is_err());
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user