Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,20 +298,28 @@ 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-"` |

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-` |

The back-end config is loaded in [`api/access_config.py`](api/access_config.py).

Expand Down
21 changes: 20 additions & 1 deletion api/access_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
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"


class UndefinedConfigKeyError(Exception):
Expand All @@ -27,9 +30,19 @@ 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,
):
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


def _get_config_value(config: dict[str, Any], key: str) -> Any:
Expand Down Expand Up @@ -76,10 +89,16 @@ 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)

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,
)


Expand Down
21 changes: 17 additions & 4 deletions api/models/core_models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, validates
from sqlalchemy.sql import expression
from sqlalchemy_json import mutable_json_type

from api import config
from api.access_config import get_access_config
from api.extensions import db


Expand Down Expand Up @@ -554,7 +555,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(db.Unicode(50), db.ForeignKey("okta_group.id"), primary_key=True)
Expand Down Expand Up @@ -602,8 +603,8 @@ class RoleGroup(OktaGroup):


class AppGroup(OktaGroup):
APP_GROUP_NAME_PREFIX = "App-"
APP_NAME_GROUP_NAME_SEPARATOR = "-"
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 = "Owners"
Copy link
Copy Markdown

@elaynelemos elaynelemos Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @eguerrant! I've addded issue #382 (and PR #388) for this customization. Is it possible to also support custom APP_OWNERS_GROUP_NAME_SUFFIX, OKTA_GROUP_NAME_PREFIX, and configurable Okta group sync filter?


__tablename__ = "app_group"
Expand Down Expand Up @@ -1274,3 +1275,15 @@ class AppTagMap(db.Model):
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


_init_group_name_prefixes()
10 changes: 8 additions & 2 deletions config/config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
},
"DEFAULT_ACCESS_TIME": "1209600",
"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-"
},
"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-"
}
Comment on lines 19 to 25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this duplication necessary? Is it possible to have FRONTEND and SHARED config to reduce the likelihood of these being out of sync?

}
3 changes: 3 additions & 0 deletions src/config/accessConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export interface AccessConfig {
DEFAULT_ACCESS_TIME: string;
NAME_VALIDATION_PATTERN: string;
NAME_VALIDATION_ERROR: string;
APP_GROUP_NAME_PREFIX: string;
APP_NAME_GROUP_NAME_SEPARATOR: string;
ROLE_GROUP_NAME_PREFIX: string;
}

// use the globally-injected ACCESS_CONFIG from src/globals.d.ts, typed to AccessConfig interface
Expand Down
3 changes: 3 additions & 0 deletions src/config/loadAccessConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ 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';

class UndefinedConfigError extends Error {
constructor(key, obj) {
Expand Down
6 changes: 3 additions & 3 deletions src/pages/group_requests/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ const GROUP_TYPE_ID_TO_LABELS: Record<string, string> = {

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 RFC822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ';

Expand Down
6 changes: 3 additions & 3 deletions src/pages/group_requests/Read.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ const UNTIL_JUST_NUMERIC_ID_TO_LABELS: Record<string, string> = 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;

const RFC822_FORMAT = 'ddd, DD MMM YYYY HH:mm:ss ZZ';

Expand Down
6 changes: 3 additions & 3 deletions src/pages/groups/CreateUpdate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,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();
Expand Down
15 changes: 15 additions & 0 deletions tests/test_access_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
_merge_override_config,
NAME_VALIDATION_PATTERN,
NAME_VALIDATION_ERROR,
APP_GROUP_NAME_PREFIX,
APP_NAME_GROUP_NAME_SEPARATOR,
ROLE_GROUP_NAME_PREFIX,
ConfigValidationError,
_validate_override_config,
)
Expand All @@ -29,6 +32,9 @@ 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-",
},
):
yield
Expand All @@ -41,6 +47,9 @@ 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-",
}
)
yield mock_merge
Expand All @@ -51,13 +60,19 @@ 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-"


def test_load_config_with_override(mock_load_default_config: None, mock_merge_override_config: None) -> None:
config = _load_access_config()
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-"


def test_load_default_config() -> None:
Expand Down