diff --git a/frontend/src/components/admin/groups/GroupConfig.svelte b/frontend/src/components/admin/groups/GroupConfig.svelte index efafbfe9..7c2aac0b 100644 --- a/frontend/src/components/admin/groups/GroupConfig.svelte +++ b/frontend/src/components/admin/groups/GroupConfig.svelte @@ -28,7 +28,7 @@ let formErrors = {}; const schema = yup.object().shape({ - name: yup.string().trim().matches(REGEX_ROLES, "Can only contain: 'a-z0-9-_/', length: 2-128"), + name: yup.string().trim().matches(REGEX_ROLES, "Can only contain: 'a-z0-9-_/:', length: 2-64"), }); function handleKeyPress(event) { diff --git a/frontend/src/components/admin/roles/RoleConfig.svelte b/frontend/src/components/admin/roles/RoleConfig.svelte index fe097091..c2ed90f5 100644 --- a/frontend/src/components/admin/roles/RoleConfig.svelte +++ b/frontend/src/components/admin/roles/RoleConfig.svelte @@ -28,7 +28,7 @@ let formErrors = {}; const schema = yup.object().shape({ - name: yup.string().trim().matches(REGEX_ROLES, "Can only contain: 'a-z0-9-_/', length: 2-128"), + name: yup.string().trim().matches(REGEX_ROLES, "Can only contain: 'a-z0-9-_/:', length: 2-64"), }); function handleKeyPress(event) { diff --git a/frontend/src/components/admin/scopes/ScopeConfig.svelte b/frontend/src/components/admin/scopes/ScopeConfig.svelte index cecd41ad..d9c6563c 100644 --- a/frontend/src/components/admin/scopes/ScopeConfig.svelte +++ b/frontend/src/components/admin/scopes/ScopeConfig.svelte @@ -37,7 +37,7 @@ let formErrors = {}; const schema = yup.object().shape({ - name: yup.string().trim().matches(REGEX_ROLES, "Can only contain: 'a-z0-9-_/', length: 2-128"), + name: yup.string().trim().matches(REGEX_ROLES, "Can only contain: 'a-z0-9-_/:', length: 2-64"), }); function handleKeyPress(event) { diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index e45f0357..d36c6711 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -27,7 +27,7 @@ export const REGEX_CONTACT = /^[a-zA-Z0-9+.@/:]{0,48}$/gm; export const REGEX_LOWERCASE_SPACE = /^[a-z0-9-_\/\s]{2,128}$/gm; export const REGEX_PROVIDER_SCOPE = /^[a-z0-9-_\/:\s]{0,128}$/gm; export const REGEX_PEM = /^(-----BEGIN CERTIFICATE-----)[a-zA-Z0-9+/=\n]+(-----END CERTIFICATE-----)$/gm; -export const REGEX_ROLES = /^[a-z0-9\-_/]{2,128}$/gm; +export const REGEX_ROLES = /^[a-z0-9\-_/:]{2,64}$/gm; export const REGEX_URI = /^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]*$/gm; export const REGEX_URI_SPACE = /^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%\s]+$/m; export const REGEX_IP_V4 = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/gm; diff --git a/migrations/postgres/24_roles_groups_scopes_limit.sql b/migrations/postgres/24_roles_groups_scopes_limit.sql new file mode 100644 index 00000000..108ae8ce --- /dev/null +++ b/migrations/postgres/24_roles_groups_scopes_limit.sql @@ -0,0 +1,4 @@ +-- remove the limit checks on id and name fields for roles, groups, scopes +-- to allow longer scope names like i.e. 'urn:ietf:params:oauth:grant-type:device_code' + +-- noop on postgres -> has been done with past migrations already \ No newline at end of file diff --git a/migrations/sqlite/24_roles_groups_scopes_limit.sql b/migrations/sqlite/24_roles_groups_scopes_limit.sql new file mode 100644 index 00000000..982c49a2 --- /dev/null +++ b/migrations/sqlite/24_roles_groups_scopes_limit.sql @@ -0,0 +1,55 @@ +-- remove the limit checks on id and name fields for roles, groups, scopes +-- to allow longer scope names like i.e. 'urn:ietf:params:oauth:grant-type:device_code' + +create table scopes_dg_tmp +( + id varchar(36) not null + constraint scopes_pk + primary key, + name varchar not null, + attr_include_access varchar, + attr_include_id varchar +); + +insert into scopes_dg_tmp(id, name, attr_include_access, attr_include_id) +select id, name, attr_include_access, attr_include_id +from scopes; + +drop table scopes; + +alter table scopes_dg_tmp + rename to scopes; + +create table groups_dg_tmp +( + id varchar not null + constraint groups_pk + primary key, + name varchar not null +); + +insert into groups_dg_tmp(id, name) +select id, name +from groups; + +drop table groups; + +alter table groups_dg_tmp + rename to groups; + +create table roles_dg_tmp +( + id varchar not null + constraint roles_pk + primary key, + name varchar not null +); + +insert into roles_dg_tmp(id, name) +select id, name +from roles; + +drop table roles; + +alter table roles_dg_tmp + rename to roles; diff --git a/rauthy-common/src/constants.rs b/rauthy-common/src/constants.rs index aad986ee..64f49b6d 100644 --- a/rauthy-common/src/constants.rs +++ b/rauthy-common/src/constants.rs @@ -99,7 +99,6 @@ lazy_static! { pub static ref RE_ATTR: Regex = Regex::new(r"^[a-zA-Z0-9-_/]{2,32}$").unwrap(); pub static ref RE_ATTR_DESC: Regex = Regex::new(r"^[a-zA-Z0-9-_/\s]{0,128}$").unwrap(); - pub static ref RE_SCOPE: Regex = Regex::new(r"^[a-z0-9-_/:\s]{0,128}$").unwrap(); pub static ref RE_ALNUM: Regex = Regex::new(r"^[a-zA-Z0-9]+$").unwrap(); pub static ref RE_ALNUM_24: Regex = Regex::new(r"^[a-zA-Z0-9]{24}$").unwrap(); pub static ref RE_ALNUM_48: Regex = Regex::new(r"^[a-zA-Z0-9]{48}$").unwrap(); @@ -118,14 +117,17 @@ lazy_static! { pub static ref RE_DATE_STR: Regex = Regex::new(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$").unwrap(); pub static ref RE_GRANT_TYPES: Regex = Regex::new(r"^(authorization_code|client_credentials|urn:ietf:params:oauth:grant-type:device_code|password|refresh_token)$").unwrap(); pub static ref RE_GRANT_TYPES_EPHEMERAL: Regex = Regex::new(r"^(authorization_code|client_credentials|password|refresh_token)$").unwrap(); + pub static ref RE_GROUPS: Regex = Regex::new(r"^[a-z0-9-_/,:]{2,64}$").unwrap(); pub static ref RE_LOWERCASE: Regex = Regex::new(r"^[a-z0-9-_/]{2,128}$").unwrap(); pub static ref RE_LOWERCASE_SPACE: Regex = Regex::new(r"^[a-z0-9-_/\s]{2,128}$").unwrap(); - pub static ref RE_GROUPS: Regex = Regex::new(r"^[a-z0-9-_/,]{2,32}$").unwrap(); pub static ref RE_MFA_CODE: Regex = Regex::new(r"^[a-zA-Z0-9]{48}$").unwrap(); pub static ref RE_PEM: Regex = Regex::new(r"^(-----BEGIN CERTIFICATE-----)[a-zA-Z0-9+/=\n]+(-----END CERTIFICATE-----)$").unwrap(); pub static ref RE_PHONE: Regex = Regex::new(r"^\+[0-9]{0,32}$").unwrap(); - pub static ref RE_STREET: Regex = Regex::new(r"^[a-zA-Z0-9À-ÿ-.\s]{0,48}$").unwrap(); + // we have a pretty high upper limit for characters here just to be sure that even if + // multiple values like 'urn:ietf:params:oauth:grant-type:device_code' would not fail + pub static ref RE_SCOPE_SPACE: Regex = Regex::new(r"^[a-z0-9-_/:\s]{0,512}$").unwrap(); pub static ref RE_SEARCH: Regex = Regex::new(r"^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%@]+$").unwrap(); + pub static ref RE_STREET: Regex = Regex::new(r"^[a-zA-Z0-9À-ÿ-.\s]{0,48}$").unwrap(); pub static ref RE_URI: Regex = Regex::new(r"^[a-zA-Z0-9,.:/_\-&?=~#!$'()*+%]+$").unwrap(); pub static ref RE_USER_NAME: Regex = Regex::new(r"^[a-zA-Z0-9À-ÿ-\s]{2,32}$").unwrap(); pub static ref RE_TOKEN_68: Regex = Regex::new(r"^[a-zA-Z0-9-._~+/]+=*$").unwrap(); diff --git a/rauthy-models/src/request.rs b/rauthy-models/src/request.rs index 0dd6114a..05b06961 100644 --- a/rauthy-models/src/request.rs +++ b/rauthy-models/src/request.rs @@ -7,11 +7,11 @@ use actix_web::http::header; use actix_web::HttpRequest; use css_color::Srgb; use rauthy_common::constants::{ - RE_ALNUM, RE_ALNUM_48, RE_ALNUM_64, RE_ALNUM_SPACE, RE_API_KEY, RE_APP_ID, RE_ATTR, - RE_ATTR_DESC, RE_CHALLENGE, RE_CITY, RE_CLIENT_ID_EPHEMERAL, RE_CLIENT_NAME, RE_CODE_CHALLENGE, - RE_CODE_VERIFIER, RE_CONTACT, RE_DATE_STR, RE_GRANT_TYPES, RE_GROUPS, RE_LOWERCASE, - RE_LOWERCASE_SPACE, RE_MFA_CODE, RE_PEM, RE_PHONE, RE_SCOPE, RE_SEARCH, RE_STREET, - RE_TOKEN_ENDPOINT_AUTH_METHOD, RE_URI, RE_USER_NAME, + RE_ALNUM, RE_ALNUM_48, RE_ALNUM_64, RE_API_KEY, RE_APP_ID, RE_ATTR, RE_ATTR_DESC, RE_CHALLENGE, + RE_CITY, RE_CLIENT_ID_EPHEMERAL, RE_CLIENT_NAME, RE_CODE_CHALLENGE, RE_CODE_VERIFIER, + RE_CONTACT, RE_DATE_STR, RE_GRANT_TYPES, RE_GROUPS, RE_LOWERCASE, RE_MFA_CODE, RE_PEM, + RE_PHONE, RE_SCOPE_SPACE, RE_SEARCH, RE_STREET, RE_TOKEN_ENDPOINT_AUTH_METHOD, RE_URI, + RE_USER_NAME, }; use rauthy_common::error_response::{ErrorResponse, ErrorResponseType}; use rauthy_common::utils::base64_decode; @@ -76,8 +76,8 @@ pub struct AuthRequest { /// Validation: `[a-z0-9-_/]{2,128}` #[validate(regex(path = "RE_LOWERCASE", code = "[a-z0-9-_/]{2,128}"))] pub response_type: String, - /// Validation: `[a-zA-Z0-9À-ÿ-s]{2,128}` - #[validate(regex(path = "RE_CLIENT_NAME", code = "[a-zA-Z0-9À-ÿ-s]{2,128}"))] + /// Validation: `[a-z0-9-_/:\s]{0,512}` + #[validate(regex(path = "RE_SCOPE_SPACE", code = "[a-z0-9-_/:\\s]{0,512}"))] #[serde(default = "default_scope")] pub scope: String, /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` @@ -163,8 +163,8 @@ pub struct DeviceGrantRequest { /// Validation: max length is 256 #[validate(length(max = 256))] pub client_secret: Option, - /// Validation: `[a-z0-9-_/:\s]{0,128}` - #[validate(regex(path = "RE_SCOPE", code = "[a-z0-9-_/:\\s]{0,128}"))] + /// Validation: `[a-z0-9-_/:\s]{0,512}` + #[validate(regex(path = "RE_SCOPE_SPACE", code = "[a-z0-9-_/:\\s]{0,512}"))] pub scope: Option, } @@ -225,8 +225,8 @@ pub struct EphemeralClientRequest { /// Validation: `60 <= access_token_lifetime <= 86400` #[validate(range(min = 60, max = 86400))] pub default_max_age: Option, - /// Validation: `Vec<^[a-z0-9-_/\s]{2,128}$>` - #[validate(regex(path = "RE_LOWERCASE_SPACE", code = "[a-z0-9-_/\\s]{2,128}"))] + /// Validation: `[a-z0-9-_/:\s]{0,512}` + #[validate(regex(path = "RE_SCOPE_SPACE", code = "[a-z0-9-_/:\\s]{0,512}"))] pub scope: Option, pub require_auth_time: Option, @@ -296,8 +296,8 @@ pub struct LoginRequest { /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` #[validate(regex(path = "RE_URI", code = "[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$"))] pub redirect_uri: String, - /// Validation: `Vec<^[a-zA-Z0-9\\s]$>` - #[validate(custom(function = "validate_vec_scope"))] + /// Validation: `Vec<^[a-z0-9-_/,:]{2,64}$>` + #[validate(custom(function = "validate_vec_scopes"))] pub scopes: Option>, /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` #[validate(regex(path = "RE_URI", code = "[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$"))] @@ -324,8 +324,8 @@ pub struct LoginRefreshRequest { /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` #[validate(regex(path = "RE_URI", code = "[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$"))] pub redirect_uri: String, - /// Validation: `Vec<^[a-zA-Z0-9\\s]$>` - #[validate(custom(function = "validate_vec_scope"))] + /// Validation: `Vec<^[a-z0-9-_/,:]{2,64}$>` + #[validate(custom(function = "validate_vec_scopes"))] pub scopes: Option>, /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` #[validate(regex(path = "RE_URI", code = "[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$"))] @@ -458,8 +458,8 @@ pub struct DynamicClientRequest { #[derive(Serialize, Deserialize, Validate, ToSchema)] pub struct NewGroupRequest { - /// Validation: `^[a-z0-9-_/,]{2,32}$` - #[validate(regex(path = "RE_GROUPS", code = "^[a-z0-9-_/,]{2,32}$"))] + /// Validation: `^[a-z0-9-_/,:]{2,64}$` + #[validate(regex(path = "RE_GROUPS", code = "^[a-z0-9-_/,:]{2,64}$"))] pub group: String, } @@ -545,8 +545,8 @@ pub struct ProviderRequest { /// Validation: max length is 256 #[validate(length(max = 256))] pub client_secret: Option, - /// Validation: `[a-z0-9-_/:\s]{0,128}` - #[validate(regex(path = "RE_SCOPE", code = "[a-z0-9-_/:\\s]{0,128}"))] + /// Validation: `[a-z0-9-_/:\s]{0,512}` + #[validate(regex(path = "RE_SCOPE_SPACE", code = "[a-z0-9-_/:\\s]{0,512}"))] pub scope: String, /// Validation: `(-----BEGIN CERTIFICATE-----)[a-zA-Z0-9+/=\n]+(-----END CERTIFICATE-----)` #[validate(regex( @@ -600,8 +600,8 @@ pub struct ProviderLoginRequest { /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` #[validate(regex(path = "RE_URI", code = "[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$"))] pub redirect_uri: String, - /// Validation: `Vec<^[a-zA-Z0-9\\s]$>` - #[validate(custom(function = "validate_vec_scope"))] + /// Validation: `Vec<^[a-z0-9-_/,:]{2,64}$>` + #[validate(custom(function = "validate_vec_scopes"))] pub scopes: Option>, /// Validation: `[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$` #[validate(regex(path = "RE_URI", code = "[a-zA-Z0-9,.:/_-&?=~#!$'()*+%]+$"))] @@ -657,11 +657,11 @@ pub struct NewUserRequest { #[validate(regex(path = "RE_USER_NAME", code = "[a-zA-Z0-9À-ÿ-\\s]{2,32}"))] pub given_name: String, pub language: Language, - /// Validation: `Vec<^[a-z0-9-_/,]{2,128}$>` + /// Validation: `Vec<^[a-z0-9-_/,:]{2,64}$>` #[validate(custom(function = "validate_vec_groups"))] pub groups: Option>, - /// Validation: `Vec<^[a-z0-9-_/,]{2,128}$>` - #[validate(custom(function = "validate_vec_groups"))] + /// Validation: `Vec<^[a-z0-9-_/,:]{2,64}$>` + #[validate(custom(function = "validate_vec_roles"))] pub roles: Vec, #[validate(range(min = 1672527600, max = 4070905200))] pub user_expires: Option, @@ -687,8 +687,8 @@ pub struct NewUserRegistrationRequest { #[derive(Serialize, Deserialize, Validate, ToSchema)] pub struct NewRoleRequest { - /// Validation: `^[a-z0-9-_/,]{2,32}$` - #[validate(regex(path = "RE_GROUPS", code = "^[a-z0-9-_/,]{2,32}$"))] + /// Validation: `^[a-z0-9-_/,:]{2,64}$` + #[validate(regex(path = "RE_GROUPS", code = "^[a-z0-9-_/,:]{2,64}$"))] pub role: String, } @@ -711,8 +711,9 @@ pub struct PasskeyRequest { #[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)] pub struct ScopeRequest { - /// Validation: `^[a-z0-9-_/,]{2,32}$` - #[validate(regex(path = "RE_GROUPS", code = "^[a-z0-9-_/,]{2,32}$"))] + // `RE_GROUPS` is correct here + /// Validation: `^[a-z0-9-_/,:]{2,64}$` + #[validate(regex(path = "RE_GROUPS", code = "^[a-z0-9-_/,:]{2,64}$"))] pub scope: String, /// Validation: `^[a-zA-Z0-9-_/]{2,128}$` #[validate(custom(function = "validate_vec_attr"))] @@ -861,11 +862,11 @@ pub struct UpdateClientRequest { /// Validation: `10 <= access_token_lifetime <= 86400` #[validate(range(min = 10, max = 86400))] pub access_token_lifetime: i32, - /// Validation: `Vec<^[a-z0-9-_/]{2,128}$>` - #[validate(custom(function = "validate_vec_lowercase"))] + /// Validation: `Vec<^[a-z0-9-_/,:]{2,64}$>` + #[validate(custom(function = "validate_vec_scopes"))] pub scopes: Vec, - /// Validation: `Vec<^[a-z0-9-_/]{2,128}$>` - #[validate(custom(function = "validate_vec_lowercase"))] + /// Validation: `Vec<^[a-z0-9-_/:\s]{0,64}$>` + #[validate(custom(function = "validate_vec_scopes"))] pub default_scopes: Vec, /// Validation: `Vec<^(plain|S256)$>` #[validate(custom(function = "validate_vec_challenge"))] @@ -893,11 +894,11 @@ pub struct UpdateUserRequest { pub language: Option, /// Validation: Applies password policy pub password: Option, - /// Validation: `Vec<^[a-z0-9-_/]{2,128}$>` - #[validate(custom(function = "validate_vec_lowercase"))] + /// Validation: `Vec<^[a-z0-9-_/,:]{2,64}$>` + #[validate(custom(function = "validate_vec_roles"))] pub roles: Vec, - /// Validation: `Vec<^[a-z0-9-_/]{2,128}$>` - #[validate(custom(function = "validate_vec_lowercase"))] + /// Validation: `Vec<^[a-z0-9-_/,:]{2,64}$>` + #[validate(custom(function = "validate_vec_groups"))] pub groups: Option>, pub enabled: bool, pub email_verified: bool, @@ -1103,11 +1104,11 @@ fn validate_vec_grant_types(value: &[String]) -> Result<(), ValidationError> { Ok(()) } -fn validate_vec_lowercase(value: &[String]) -> Result<(), ValidationError> { +fn validate_vec_uri(value: &[String]) -> Result<(), ValidationError> { let mut err = None; value.iter().for_each(|v| { - if !RE_LOWERCASE.is_match(v) { - err = Some("^[a-z0-9-_/]{2,128}$"); + if !RE_URI.is_match(v) { + err = Some("^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$"); } }); if let Some(e) = err { @@ -1116,11 +1117,11 @@ fn validate_vec_lowercase(value: &[String]) -> Result<(), ValidationError> { Ok(()) } -fn validate_vec_uri(value: &[String]) -> Result<(), ValidationError> { +fn validate_vec_grant_type(value: &[String]) -> Result<(), ValidationError> { let mut err = None; value.iter().for_each(|v| { - if !RE_URI.is_match(v) { - err = Some("^[a-zA-Z0-9,.:/_\\-&?=~#!$'()*+%]+$"); + if !RE_GRANT_TYPES.is_match(v) { + err = Some("authorization_code|client_credentials|password|refresh_token"); } }); if let Some(e) = err { @@ -1129,11 +1130,14 @@ fn validate_vec_uri(value: &[String]) -> Result<(), ValidationError> { Ok(()) } -fn validate_vec_grant_type(value: &[String]) -> Result<(), ValidationError> { +// validate_vec_groups, _roles and _scopes do the same thing but are 3 functions just to +// be clear in the validation fields above that it does not create confusion, even if they +// all use the same `RE_GROUPS` regex. +fn validate_vec_groups(value: &[String]) -> Result<(), ValidationError> { let mut err = None; value.iter().for_each(|v| { - if !RE_GRANT_TYPES.is_match(v) { - err = Some("authorization_code|client_credentials|password|refresh_token"); + if !RE_GROUPS.is_match(v) { + err = Some("^[a-z0-9-_/,:]{2,64}$"); } }); if let Some(e) = err { @@ -1142,11 +1146,11 @@ fn validate_vec_grant_type(value: &[String]) -> Result<(), ValidationError> { Ok(()) } -fn validate_vec_groups(value: &[String]) -> Result<(), ValidationError> { +fn validate_vec_roles(value: &[String]) -> Result<(), ValidationError> { let mut err = None; value.iter().for_each(|v| { if !RE_GROUPS.is_match(v) { - err = Some("^[a-z0-9-_/,]{2,128}$"); + err = Some("^[a-z0-9-_/,:]{2,64}$"); } }); if let Some(e) = err { @@ -1155,11 +1159,11 @@ fn validate_vec_groups(value: &[String]) -> Result<(), ValidationError> { Ok(()) } -fn validate_vec_scope(value: &[String]) -> Result<(), ValidationError> { +fn validate_vec_scopes(value: &[String]) -> Result<(), ValidationError> { let mut err = None; value.iter().for_each(|v| { - if !RE_ALNUM_SPACE.is_match(v) { - err = Some("^[a-zA-Z0-9\\s]$"); + if !RE_GROUPS.is_match(v) { + err = Some("^[a-z0-9-_/,:]{2,64}$"); } }); if let Some(e) = err {