diff --git a/crates/policy/src/model.rs b/crates/policy/src/model.rs index aef4f3c08..4301b4165 100644 --- a/crates/policy/src/model.rs +++ b/crates/policy/src/model.rs @@ -32,6 +32,12 @@ pub enum Code { /// The username contains only numeric characters. UsernameAllNumeric, + /// The username is banned. + UsernameBanned, + + /// The username is not allowed. + UsernameNotAllowed, + /// The email domain is not allowed. EmailDomainNotAllowed, @@ -54,6 +60,8 @@ impl Code { Self::UsernameTooLong => "username-too-long", Self::UsernameInvalidChars => "username-invalid-chars", Self::UsernameAllNumeric => "username-all-numeric", + Self::UsernameBanned => "username-banned", + Self::UsernameNotAllowed => "username-not-allowed", Self::EmailDomainNotAllowed => "email-domain-not-allowed", Self::EmailDomainBanned => "email-domain-banned", Self::EmailNotAllowed => "email-not-allowed", diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index e69d14804..7289fec90 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -382,6 +382,35 @@ policy: # don't require clients to provide a client_uri. default: false allow_missing_client_uri: false + # Restrictions on user registration + registration: + # If specified, the username (localpart) *must* match one of the allowed + # usernames. If unspecified, all usernames are allowed. + allowed_usernames: + # Exact usernames that are allowed + literals: ["alice", "bob"] + # Substrings that match allowed usernames + substrings: ["user"] + # Regular expressions that match allowed usernames + regexes: ["^[a-z]+$"] + # Prefixes that match allowed usernames + prefixes: ["user-"] + # Suffixes that match allowed usernames + suffixes: ["-corp"] + # If specified, the username (localpart) *must not* match one of the + # banned usernames. If unspecified, all usernames are allowed. + banned_usernames: + # Exact usernames that are banned + literals: ["admin", "root"] + # Substrings that match banned usernames + substrings: ["admin", "root"] + # Regular expressions that match banned usernames + regexes: ["^admin$", "^root$"] + # Prefixes that match banned usernames + prefixes: ["admin-", "root-"] + # Suffixes that match banned usernames + suffixes: ["-admin", "-root"] + # Restrict what email addresses can be added to a user emails: # If specified, the email address *must* match one of the allowed addresses. diff --git a/policies/register/register.rego b/policies/register/register.rego index 6189c3926..6a36f9611 100644 --- a/policies/register/register.rego +++ b/policies/register/register.rego @@ -14,6 +14,14 @@ allow if { count(violation) == 0 } +username_allowed if { + not data.registration.allowed_usernames +} + +username_allowed if { + common.matches_string_constraints(input.username, data.registration.allowed_usernames) +} + # METADATA # entrypoint: true violation contains {"field": "username", "code": "username-too-short", "msg": "username too short"} if { @@ -39,6 +47,20 @@ violation contains { not regex.match(`^[a-z0-9.=_/-]+$`, input.username) } +violation contains { + "field": "username", "code": "username-banned", + "msg": "username is banned", +} if { + common.matches_string_constraints(input.username, data.registration.banned_usernames) +} + +violation contains { + "field": "username", "code": "username-not-allowed", + "msg": "username is not allowed", +} if { + not username_allowed +} + violation contains {"msg": "unspecified registration method"} if { not input.registration_method } diff --git a/policies/register/register_test.rego b/policies/register/register_test.rego index 51105ea39..040cdcc5d 100644 --- a/policies/register/register_test.rego +++ b/policies/register/register_test.rego @@ -75,6 +75,20 @@ test_numeric_username if { not register.allow with input as {"username": "1234", "registration_method": "upstream-oauth2"} } +test_allowed_username if { + register.allow with input as {"username": "hello", "registration_method": "upstream-oauth2"} + with data.registration.allowed_usernames.literals as ["hello"] + not register.allow with input as {"username": "hello", "registration_method": "upstream-oauth2"} + with data.registration.allowed_usernames.literals as ["world"] +} + +test_banned_username if { + not register.allow with input as {"username": "hello", "registration_method": "upstream-oauth2"} + with data.registration.banned_usernames.literals as ["hello"] + register.allow with input as {"username": "hello", "registration_method": "upstream-oauth2"} + with data.registration.banned_usernames.literals as ["world"] +} + test_ip_ban if { not register.allow with input as { "username": "hello", diff --git a/templates/components/field.html b/templates/components/field.html index d26fc3b66..455206854 100644 --- a/templates/components/field.html +++ b/templates/components/field.html @@ -69,6 +69,10 @@ {{ _("mas.errors.username_invalid_chars") }} {% elif error.code == "username-all-numeric" %} {{ _("mas.errors.username_all_numeric") }} + {% elif error.code == "username-banned" %} + {{ _("mas.errors.username_banned") }} + {% elif error.code == "username-not-allowed" %} + {{ _("mas.errors.username_not_allowed") }} {% elif error.code == "email-domain-not-allowed" %} {{ _("mas.errors.email_domain_not_allowed") }} {% elif error.code == "email-domain-banned" %} diff --git a/translations/en.json b/translations/en.json index 70794197d..39e2d46b7 100644 --- a/translations/en.json +++ b/translations/en.json @@ -299,23 +299,23 @@ }, "denied_policy": "Denied by policy: %(policy)s", "@denied_policy": { - "context": "components/errors.html:17:7-58, components/field.html:81:19-70" + "context": "components/errors.html:17:7-58, components/field.html:85:19-70" }, "email_banned": "Email is banned by the server policy", "@email_banned": { - "context": "components/field.html:79:19-47" + "context": "components/field.html:83:19-47" }, "email_domain_banned": "Email domain is banned by the server policy", "@email_domain_banned": { - "context": "components/field.html:75:19-54" + "context": "components/field.html:79:19-54" }, "email_domain_not_allowed": "Email domain is not allowed by the server policy", "@email_domain_not_allowed": { - "context": "components/field.html:73:19-59" + "context": "components/field.html:77:19-59" }, "email_not_allowed": "Email is not allowed by the server policy", "@email_not_allowed": { - "context": "components/field.html:77:19-52" + "context": "components/field.html:81:19-52" }, "field_required": "This field is required", "@field_required": { @@ -327,7 +327,7 @@ }, "password_mismatch": "Password fields don't match", "@password_mismatch": { - "context": "components/errors.html:13:7-40, components/field.html:84:17-50" + "context": "components/errors.html:13:7-40, components/field.html:88:17-50" }, "rate_limit_exceeded": "You've made too many requests in a short period. Please wait a few minutes and try again.", "@rate_limit_exceeded": { @@ -337,10 +337,20 @@ "@username_all_numeric": { "context": "components/field.html:71:19-55" }, + "username_banned": "Username is banned by the server policy", + "@username_banned": { + "context": "components/field.html:73:19-50", + "description": "Error message shown on registration, when the username matches a pattern that is banned by the server policy." + }, "username_invalid_chars": "Username contains invalid characters. Use lowercase letters, numbers, dashes and underscores only.", "@username_invalid_chars": { "context": "components/field.html:69:19-57" }, + "username_not_allowed": "Username is not allowed by the server policy", + "@username_not_allowed": { + "context": "components/field.html:75:19-55", + "description": "Error message shown on registration, when the username *does not match* any of the patterns that are allowed by the server policy." + }, "username_taken": "This username is already taken", "@username_taken": { "context": "components/field.html:62:17-47" @@ -424,7 +434,7 @@ }, "or_separator": "Or", "@or_separator": { - "context": "components/field.html:103:10-31", + "context": "components/field.html:107:10-31", "description": "Separator between the login methods" }, "policy_violation": {