diff --git a/README.md b/README.md index ef4bd22b..ad82da31 100644 --- a/README.md +++ b/README.md @@ -326,20 +326,30 @@ To override values on the front-end, modify these key-value pairs inside the `FR |---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------| | `ACCESS_TIME_LABELS` | Specifies the time access labels to use for dropdowns on the front end. Contains a JSON object of the format `{"NUM_SECONDS": "LABEL"}`. | `{"86400": "1 day", "604800": "1 week", "2592000": "1 month"}` | | `DEFAULT_ACCESS_TIME` | Specifies the default time access label to use for dropdowns on the front end. Contains a string with a number of seconds corresponding to a key in the access time labels. | `"86400"` | -| `NAME_VALIDATION_PATTERN` | Specifies the regex pattern to use for validating role, group, and tag names. Should include preceding `^` and trailing `$` but is not a regex literal so omit `/` at beginning and end of the pattern | `"^[a-zA-Z0-9-]*$"` | -| `NAME_VALIDATION_ERROR` | Specifies the error message to display when a name does not match the validation pattern. | `"Name must contain only letters, numbers, and underscores."` | +| `NAME_VALIDATION_PATTERN` | Specifies the regex pattern to use for validating role, group, and tag names. Should include preceding `^` and trailing `$` but is not a regex literal so omit `/` at beginning and end of the pattern | `"^[a-zA-Z0-9-]*$"` | +| `NAME_VALIDATION_ERROR` | Specifies the error message to display when a name does not match the validation pattern. | `"Name must contain only letters, numbers, and underscores."` | +| `APP_GROUP_NAME_PREFIX` | Specifies the prefix prepended to app group names. **Must match the `BACKEND` value.** | `"App-"` | +| `APP_NAME_GROUP_NAME_SEPARATOR` | Specifies the separator between the app name and the group name suffix within an app group name. **Must match the `BACKEND` value.** | `"-"` | +| `ROLE_GROUP_NAME_PREFIX` | Specifies the prefix prepended to role group names. **Must match the `BACKEND` value.** | `"Role-"` | +| `APP_OWNERS_GROUP_NAME_SUFFIX` | Specifies the suffix appended to the name of an app's owner group. **Must match the `BACKEND` value.** | `"Owners"` | The front-end config is loaded in [`vite.config.ts`](vite.config.ts). See [`src/config/loadAccessConfig.js`](src/config/loadAccessConfig.js) for more details. +> **Note:** Because the front-end config is injected at build time by Vite, you must restart the Vite dev server (or rebuild) after changing `config.default.json` or your override file for the new values to take effect. + #### Backend Configuration To override values on the back-end, modify these key-value pairs inside the `BACKEND` key in your custom config file. -| Name | Details | Example | -|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------| -| `NAME_VALIDATION_PATTERN` | PCRE regex used for validating role, group, and tag names. Should not explicitly declare pattern boundaries: depending on context, may be used with or without a preceding `^` and a trailing `$`. | `[A-Z][A-Za-z0-9-]*` | -| `NAME_VALIDATION_ERROR` | Error message to display when a name does not match the validation pattern. | `Name must start with a capital letter and contain only letters, numbers, and hypens.` | +| Name | Details | Example | +|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------| +| `NAME_VALIDATION_PATTERN` | PCRE regex used for validating role, group, and tag names. Should not explicitly declare pattern boundaries: depending on context, may be used with or without a preceding `^` and a trailing `$`. | `[A-Z][A-Za-z0-9-]*` | +| `NAME_VALIDATION_ERROR` | Error message to display when a name does not match the validation pattern. | `Name must start with a capital letter and contain only letters, numbers, and hypens.` | +| `APP_GROUP_NAME_PREFIX` | Prefix prepended to app group names. **Must match the `FRONTEND` value.** | `App-` | +| `APP_NAME_GROUP_NAME_SEPARATOR` | Separator between the app name and the group name suffix within an app group name. **Must match the `FRONTEND` value.** | `-` | +| `ROLE_GROUP_NAME_PREFIX` | Prefix prepended to role group names. **Must match the `FRONTEND` value.** | `Role-` | +| `APP_OWNERS_GROUP_NAME_SUFFIX` | Suffix appended to the name of an app's owner group. **Must match the `FRONTEND` value.** | `Owners` | The back-end config is loaded in [`api/access_config.py`](api/access_config.py). diff --git a/api/access_config.py b/api/access_config.py index 7aeb40ad..f412b648 100644 --- a/api/access_config.py +++ b/api/access_config.py @@ -9,6 +9,10 @@ BACKEND = "BACKEND" NAME_VALIDATION_PATTERN = "NAME_VALIDATION_PATTERN" NAME_VALIDATION_ERROR = "NAME_VALIDATION_ERROR" +APP_GROUP_NAME_PREFIX = "APP_GROUP_NAME_PREFIX" +APP_NAME_GROUP_NAME_SEPARATOR = "APP_NAME_GROUP_NAME_SEPARATOR" +ROLE_GROUP_NAME_PREFIX = "ROLE_GROUP_NAME_PREFIX" +APP_OWNERS_GROUP_NAME_SUFFIX = "APP_OWNERS_GROUP_NAME_SUFFIX" class UndefinedConfigKeyError(Exception): @@ -27,9 +31,21 @@ def __init__(self, error: str): class AccessConfig: - def __init__(self, name_pattern: str, name_validation_error: str): + def __init__( + self, + name_pattern: str, + name_validation_error: str, + app_group_name_prefix: str, + app_name_group_name_separator: str, + role_group_name_prefix: str, + app_owners_group_name_suffix: str, + ): self.name_pattern = name_pattern self.name_validation_error = name_validation_error + self.app_group_name_prefix = app_group_name_prefix + self.app_name_group_name_separator = app_name_group_name_separator + self.role_group_name_prefix = role_group_name_prefix + self.app_owners_group_name_suffix = app_owners_group_name_suffix def _get_config_value(config: dict[str, Any], key: str) -> Any: @@ -93,10 +109,18 @@ def _load_access_config() -> AccessConfig: name_pattern = _get_config_value(config, NAME_VALIDATION_PATTERN) name_validation_error = _get_config_value(config, NAME_VALIDATION_ERROR) + app_group_name_prefix = _get_config_value(config, APP_GROUP_NAME_PREFIX) + app_name_group_name_separator = _get_config_value(config, APP_NAME_GROUP_NAME_SEPARATOR) + role_group_name_prefix = _get_config_value(config, ROLE_GROUP_NAME_PREFIX) + app_owners_group_name_suffix = _get_config_value(config, APP_OWNERS_GROUP_NAME_SUFFIX) return AccessConfig( name_pattern=name_pattern, name_validation_error=name_validation_error, + app_group_name_prefix=app_group_name_prefix, + app_name_group_name_separator=app_name_group_name_separator, + role_group_name_prefix=role_group_name_prefix, + app_owners_group_name_suffix=app_owners_group_name_suffix, ) diff --git a/api/models/core_models.py b/api/models/core_models.py index b66c3911..397bbdac 100644 --- a/api/models/core_models.py +++ b/api/models/core_models.py @@ -1,6 +1,6 @@ from datetime import datetime from enum import StrEnum -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, ClassVar, Dict, List, Optional from sqlalchemy import ( BigInteger, @@ -21,6 +21,7 @@ from sqlalchemy_json import mutable_json_type from api import config +from api.access_config import get_access_config from api.extensions import Base @@ -553,7 +554,7 @@ def validate_group(self, key: str, group: OktaGroup) -> OktaGroup: class RoleGroup(OktaGroup): - ROLE_GROUP_NAME_PREFIX = "Role-" + ROLE_GROUP_NAME_PREFIX: ClassVar[str] # set from config at module load, see bottom of file __tablename__ = "role_group" id: Mapped[str] = mapped_column(Unicode(50), ForeignKey("okta_group.id"), primary_key=True) @@ -601,9 +602,9 @@ class RoleGroup(OktaGroup): class AppGroup(OktaGroup): - APP_GROUP_NAME_PREFIX = "App-" - APP_NAME_GROUP_NAME_SEPARATOR = "-" - APP_OWNERS_GROUP_NAME_SUFFIX = "Owners" + APP_GROUP_NAME_PREFIX: ClassVar[str] # set from config at module load, see bottom of file + APP_NAME_GROUP_NAME_SEPARATOR: ClassVar[str] # set from config at module load, see bottom of file + APP_OWNERS_GROUP_NAME_SUFFIX: ClassVar[str] # set from config at module load, see bottom of file __tablename__ = "app_group" id: Mapped[str] = mapped_column(Unicode(50), ForeignKey("okta_group.id"), primary_key=True) @@ -1242,3 +1243,16 @@ class AppTagMap(Base): lazy="raise_on_sql", innerjoin=True, ) + + +# Initialize group name prefix class constants from config so they stay in sync +# with the frontend (config/config.default.json) and can be overridden via ACCESS_CONFIG_FILE. +def _init_group_name_prefixes() -> None: + cfg = get_access_config() + RoleGroup.ROLE_GROUP_NAME_PREFIX = cfg.role_group_name_prefix + AppGroup.APP_GROUP_NAME_PREFIX = cfg.app_group_name_prefix + AppGroup.APP_NAME_GROUP_NAME_SEPARATOR = cfg.app_name_group_name_separator + AppGroup.APP_OWNERS_GROUP_NAME_SUFFIX = cfg.app_owners_group_name_suffix + + +_init_group_name_prefixes() diff --git a/config/config.default.json b/config/config.default.json index 650d6c21..b085dfaa 100644 --- a/config/config.default.json +++ b/config/config.default.json @@ -14,10 +14,18 @@ "NAME_VALIDATION_ERROR": "Name must start capitalized and contain only alphanumeric characters or hyphens.", "IDP_NAME": "", "IDP_USER_URL_TEMPLATE": "", - "IDP_GROUP_URL_TEMPLATE": "" + "IDP_GROUP_URL_TEMPLATE": "", + "APP_GROUP_NAME_PREFIX": "App-", + "APP_NAME_GROUP_NAME_SEPARATOR": "-", + "ROLE_GROUP_NAME_PREFIX": "Role-", + "APP_OWNERS_GROUP_NAME_SUFFIX": "Owners" }, "BACKEND": { "NAME_VALIDATION_PATTERN": "[A-Z][A-Za-z0-9-]*", - "NAME_VALIDATION_ERROR": "name must start capitalized and contain only alphanumeric characters or hyphens." + "NAME_VALIDATION_ERROR": "name must start capitalized and contain only alphanumeric characters or hyphens.", + "APP_GROUP_NAME_PREFIX": "App-", + "APP_NAME_GROUP_NAME_SEPARATOR": "-", + "ROLE_GROUP_NAME_PREFIX": "Role-", + "APP_OWNERS_GROUP_NAME_SUFFIX": "Owners" } } diff --git a/src/config/accessConfig.ts b/src/config/accessConfig.ts index 2bc66ae8..ea5d2f1a 100644 --- a/src/config/accessConfig.ts +++ b/src/config/accessConfig.ts @@ -6,6 +6,10 @@ export interface AccessConfig { IDP_NAME: string; IDP_USER_URL_TEMPLATE: string; IDP_GROUP_URL_TEMPLATE: string; + APP_GROUP_NAME_PREFIX: string; + APP_NAME_GROUP_NAME_SEPARATOR: string; + ROLE_GROUP_NAME_PREFIX: string; + APP_OWNERS_GROUP_NAME_SUFFIX: string; } // use the globally-injected ACCESS_CONFIG from src/globals.d.ts, typed to AccessConfig interface diff --git a/src/config/loadAccessConfig.js b/src/config/loadAccessConfig.js index d72bd767..f4cbf26f 100644 --- a/src/config/loadAccessConfig.js +++ b/src/config/loadAccessConfig.js @@ -11,6 +11,10 @@ const DEFAULT_ACCESS_TIME = 'DEFAULT_ACCESS_TIME'; const FRONTEND = 'FRONTEND'; const NAME_VALIDATION_PATTERN = 'NAME_VALIDATION_PATTERN'; const NAME_VALIDATION_ERROR = 'NAME_VALIDATION_ERROR'; +const APP_GROUP_NAME_PREFIX = 'APP_GROUP_NAME_PREFIX'; +const APP_NAME_GROUP_NAME_SEPARATOR = 'APP_NAME_GROUP_NAME_SEPARATOR'; +const ROLE_GROUP_NAME_PREFIX = 'ROLE_GROUP_NAME_PREFIX'; +const APP_OWNERS_GROUP_NAME_SUFFIX = 'APP_OWNERS_GROUP_NAME_SUFFIX'; class UndefinedConfigError extends Error { constructor(key, obj) { diff --git a/src/pages/group_requests/Create.tsx b/src/pages/group_requests/Create.tsx index 8fa95457..a56977eb 100644 --- a/src/pages/group_requests/Create.tsx +++ b/src/pages/group_requests/Create.tsx @@ -56,9 +56,9 @@ const GROUP_TYPE_ID_TO_LABELS: Record = { const GROUP_TYPE_OPTIONS = Object.entries(GROUP_TYPE_ID_TO_LABELS).map(([id, label]) => ({id, label})); -const APP_GROUP_PREFIX = 'App-'; -const APP_NAME_APP_GROUP_SEPARATOR = '-'; -const ROLE_GROUP_PREFIX = 'Role-'; +const APP_GROUP_PREFIX = accessConfig.APP_GROUP_NAME_PREFIX; +const APP_NAME_APP_GROUP_SEPARATOR = accessConfig.APP_NAME_GROUP_NAME_SEPARATOR; +const ROLE_GROUP_PREFIX = accessConfig.ROLE_GROUP_NAME_PREFIX; const UNTIL_ID_TO_LABELS: Record = { '43200': '12 Hours', diff --git a/src/pages/group_requests/Read.tsx b/src/pages/group_requests/Read.tsx index ebfd36e5..327ac1ff 100644 --- a/src/pages/group_requests/Read.tsx +++ b/src/pages/group_requests/Read.tsx @@ -88,9 +88,9 @@ const UNTIL_JUST_NUMERIC_ID_TO_LABELS: Record = Object.fromEntri Object.entries(UNTIL_ID_TO_LABELS).filter(([key]) => !isNaN(Number(key))), ); -const APP_GROUP_PREFIX = 'App-'; -const APP_NAME_APP_GROUP_SEPARATOR = '-'; -const ROLE_GROUP_PREFIX = 'Role-'; +const APP_GROUP_PREFIX = accessConfig.APP_GROUP_NAME_PREFIX; +const APP_NAME_APP_GROUP_SEPARATOR = accessConfig.APP_NAME_GROUP_NAME_SEPARATOR; +const ROLE_GROUP_PREFIX = accessConfig.ROLE_GROUP_NAME_PREFIX; interface OwnershipEndingFieldProps { ownershipTimeLimit: number | null; diff --git a/src/pages/groups/CreateUpdate.tsx b/src/pages/groups/CreateUpdate.tsx index 2a868129..371946e6 100644 --- a/src/pages/groups/CreateUpdate.tsx +++ b/src/pages/groups/CreateUpdate.tsx @@ -85,9 +85,9 @@ const GROUP_TYPE_OPTIONS = Object.entries(GROUP_TYPE_ID_TO_LABELS).map(([id, lab label: label, })); -const APP_GROUP_PREFIX = 'App-'; -const APP_NAME_APP_GROUP_SEPARATOR = '-'; -const ROLE_GROUP_PREFIX = 'Role-'; +const APP_GROUP_PREFIX = accessConfig.APP_GROUP_NAME_PREFIX; +const APP_NAME_APP_GROUP_SEPARATOR = accessConfig.APP_NAME_GROUP_NAME_SEPARATOR; +const ROLE_GROUP_PREFIX = accessConfig.ROLE_GROUP_NAME_PREFIX; function GroupDialog(props: GroupDialogProps) { const navigate = useNavigate(); diff --git a/tests/test_access_config.py b/tests/test_access_config.py index caad94b4..4999291b 100644 --- a/tests/test_access_config.py +++ b/tests/test_access_config.py @@ -18,6 +18,10 @@ _resolve_config_paths, NAME_VALIDATION_PATTERN, NAME_VALIDATION_ERROR, + APP_GROUP_NAME_PREFIX, + APP_NAME_GROUP_NAME_SEPARATOR, + ROLE_GROUP_NAME_PREFIX, + APP_OWNERS_GROUP_NAME_SUFFIX, ConfigValidationError, _validate_override_config, ) @@ -30,6 +34,10 @@ def mock_load_default_config() -> Generator[Any, Any, Any]: return_value={ NAME_VALIDATION_PATTERN: "name_pattern", NAME_VALIDATION_ERROR: "name_error", + APP_GROUP_NAME_PREFIX: "App-", + APP_NAME_GROUP_NAME_SEPARATOR: "-", + ROLE_GROUP_NAME_PREFIX: "Role-", + APP_OWNERS_GROUP_NAME_SUFFIX: "Owners", }, ): yield @@ -42,6 +50,10 @@ def mock_merge_override_config() -> Generator[Any, Any, Any]: { NAME_VALIDATION_PATTERN: "override_name_pattern", NAME_VALIDATION_ERROR: "override_name_error", + APP_GROUP_NAME_PREFIX: "Override-", + APP_NAME_GROUP_NAME_SEPARATOR: "_", + ROLE_GROUP_NAME_PREFIX: "OverrideRole-", + APP_OWNERS_GROUP_NAME_SUFFIX: "OverrideOwners", } ) yield mock_merge @@ -52,6 +64,10 @@ def test_load_config_default(mock_load_default_config: None) -> None: assert isinstance(config, AccessConfig) assert config.name_pattern == "name_pattern" assert config.name_validation_error == "name_error" + assert config.app_group_name_prefix == "App-" + assert config.app_name_group_name_separator == "-" + assert config.role_group_name_prefix == "Role-" + assert config.app_owners_group_name_suffix == "Owners" def test_load_config_with_override( @@ -66,6 +82,10 @@ def test_load_config_with_override( assert isinstance(config, AccessConfig) assert config.name_pattern == "override_name_pattern" assert config.name_validation_error == "override_name_error" + assert config.app_group_name_prefix == "Override-" + assert config.app_name_group_name_separator == "_" + assert config.role_group_name_prefix == "OverrideRole-" + assert config.app_owners_group_name_suffix == "OverrideOwners" def test_load_default_config() -> None: