Skip to content

Commit

Permalink
Make user role prefixes configurable (#974)
Browse files Browse the repository at this point in the history
See commits.
  • Loading branch information
owi92 authored Oct 24, 2023
2 parents afc8343 + 6c52b95 commit 2877f3c
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 15 deletions.
6 changes: 6 additions & 0 deletions backend/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ pub(crate) struct AuthConfig {
#[config(default = "ROLE_USER")]
pub(crate) user_realm_role: String,

/// List of prefixes that user roles can have. Used to distinguish user
/// roles from other roles. Should probably be the same as
/// `role_user_prefix` in `acl.default.create.properties` in OC.
#[config(default = ["ROLE_USER_"])]
pub(crate) user_role_prefixes: Vec<String>,

/// Duration of a Tobira-managed login session.
/// Note: This is only relevant if `auth.mode` is `login-proxy`.
#[config(default = "30d", deserialize_with = crate::config::deserialize_duration)]
Expand Down
1 change: 1 addition & 0 deletions backend/src/http/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ impl Assets {
"passwordLabel": config.auth.login_page.password_label,
"loginPageNote": config.auth.login_page.note,
"preAuthExternalLinks": config.auth.pre_auth_external_links,
"userRolePrefixes": config.auth.user_role_prefixes,
}).to_string());
variables.insert("upload".into(), json!({
"requireSeries": config.upload.require_series,
Expand Down
7 changes: 7 additions & 0 deletions docs/docs/setup/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,13 @@
# Default value: "ROLE_USER"
#user_realm_role = "ROLE_USER"

# List of prefixes that user roles can have. Used to distinguish user
# roles from other roles. Should probably be the same as
# `role_user_prefix` in `acl.default.create.properties` in OC.
#
# Default value: ["ROLE_USER_"]
#user_role_prefixes = ["ROLE_USER_"]

# Duration of a Tobira-managed login session.
# Note: This is only relevant if `auth.mode` is `login-proxy`.
#
Expand Down
1 change: 1 addition & 0 deletions frontend/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type AuthConfig = {
passwordLabel: TranslatedString | null;
loginPageNote: TranslatedString | null;
preAuthExternalLinks: boolean;
userRolePrefixes: string[];
};

type LogoConfig = {
Expand Down
36 changes: 21 additions & 15 deletions frontend/src/ui/Access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { SelectProps } from "./Input";
import { searchableSelectStyles, theme } from "./SearchableSelect";
import { FloatingBaseMenu } from "./FloatingBaseMenu";
import { AccessKnownRolesData$data } from "./__generated__/AccessKnownRolesData.graphql";
import CONFIG from "../config";



Expand Down Expand Up @@ -163,7 +164,7 @@ const AclSelect: React.FC<AclSelectProps> = ({ acl, kind }) => {
if (x.value === currentUserRole) {
return 0;
}
if (x.label.startsWith("ROLE_USER_")) {
if (x.label.startsWith("ROLE_")) {
return 2;
}
return 1;
Expand Down Expand Up @@ -233,7 +234,7 @@ const AclSelect: React.FC<AclSelectProps> = ({ acl, kind }) => {
isSearchable
placeholder={translations.placeholder}
isValidNewOption={input => {
const validUserRole = /^ROLE_USER_\w+/.test(input);
const validUserRole = isUserRole(input);
const validRole = /^ROLE_\w+/.test(input);
return kind === "Group" ? (validRole && !validUserRole) : validUserRole;
}}
Expand All @@ -260,15 +261,13 @@ const AclSelect: React.FC<AclSelectProps> = ({ acl, kind }) => {
padding: 6,
":first-of-type": {
paddingLeft: 12,
overflowWrap: "anywhere",
},
},
[screenWidthAtMost(480)]: {
fontSize: 14,
"> colgroup > col:nth-of-type(2)": { width: "unset" },
"> colgroup > col:nth-of-type(3)": { width: 35 },
"td:first-of-type": {
overflowWrap: "anywhere",
},
},
}}>
<colgroup>
Expand Down Expand Up @@ -344,12 +343,17 @@ const ListEntry: React.FC<ListEntryProps> = ({ remove, item, kind }) => {

let label: JSX.Element;
if (isUser) {
label = <><i>{t("manage.access.table.yourself")}</i>&nbsp;({item.label})</>;
} else if (kind === "User" && item.label.startsWith("ROLE_USER_")) {
const name = item.value.slice("ROLE_USER_".length)
label = <span><i>{t("manage.access.table.yourself")}</i>&nbsp;({item.label})</span>;
} else if (kind === "User" && isUserRole(item.label)) {
// We strip the user role prefix (we take the longest prefix that
// matches, though in almost all cases just a single one will match).
// We then clean it a bit before displaying.
const prefixes = CONFIG.auth.userRolePrefixes
.filter(prefix => item.label.startsWith(prefix));
const name = item.value.slice(Math.max(...prefixes.map(p => p.length)))
.toLocaleLowerCase(i18n.resolvedLanguage)
.replace("_", " ");
label = <>{name} (<i>{t("acl.unknown-user-note")}</i>)</>;
label = <span>{name} (<i>{t("acl.unknown-user-note")}</i>)</span>;
} else {
label = <>{item.label}</>;
}
Expand Down Expand Up @@ -587,24 +591,26 @@ const getLabel = (role: string, knownRoles: KnownRoles, i18n: i18n) => {
return knownRoles[role]?.(i18n) ?? role;
};

const isUserRole = (role: string) =>
CONFIG.auth.userRolePrefixes.some(prefix => role.startsWith(prefix));

/** Splits initial ACL into group and user roles. */
const splitAcl = (initialAcl: Acl) => {
const regEx = /^ROLE_USER_\w+/;
const groupAcl: Acl = {
readRoles: new Set([...initialAcl.readRoles].filter(role => !regEx.test(role))),
writeRoles: new Set([...initialAcl.writeRoles].filter(role => !regEx.test(role))),
readRoles: new Set([...initialAcl.readRoles].filter(role => !isUserRole(role))),
writeRoles: new Set([...initialAcl.writeRoles].filter(role => !isUserRole(role))),
};
const userAcl: Acl = {
readRoles: new Set([...initialAcl.readRoles].filter(role => regEx.test(role))),
writeRoles: new Set([...initialAcl.writeRoles].filter(role => regEx.test(role))),
readRoles: new Set([...initialAcl.readRoles].filter(role => isUserRole(role))),
writeRoles: new Set([...initialAcl.writeRoles].filter(role => isUserRole(role))),
};

return [groupAcl, userAcl];
};


export const getUserRole = (user: UserState) => {
const userRole = isRealUser(user) && user.roles.find(role => /^ROLE_USER\w+/.test(role));
const userRole = isRealUser(user) && user.roles.find(isUserRole);
return typeof userRole !== "string" ? "Unknown" : userRole;
};

Expand Down

0 comments on commit 2877f3c

Please sign in to comment.