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
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ This tells the application to use `config.production.json` for configuration ove
To override values on the front-end, modify these key-value pairs inside the `FRONTEND` key in your custom config file.

| Name | Details | Example |
|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `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-]*$"` |
Expand All @@ -308,10 +308,16 @@ The front-end config is loaded in [`vite.config.ts`](vite.config.ts). See

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.` |
| `DEFAULT_GROUP_QUERY_PARAMS` | Query parameters passed to the Okta Groups API when listing groups. Accepts any valid [Okta Groups API](https://developer.okta.com/docs/api/openapi/okta-management/management/tags/group/other/listgroups) query parameters as a JSON object. Defaults to fetching all built-in and Okta-managed groups. Override to restrict which groups are synced into Access. | `{"filter": "type eq \"BUILT_IN\" or type eq \"OKTA_GROUP\""}` |
| `OKTA_GROUP_NAME_PREFIX` | Prefix applied to Okta group names managed directly by Access. Defaults to no prefix. | `""` |
| `ROLE_GROUP_NAME_PREFIX` | Prefix applied to role group names. | `"Role-"` |
| `APP_GROUP_NAME_PREFIX` | Prefix applied to app group names. | `"App-"` |
| `APP_NAME_GROUP_NAME_SEPARATOR` | Separator used between the app name and the group name for app groups. | `"-"` |
| `APP_OWNERS_GROUP_NAME_SUFFIX` | Suffix applied to the app owners group name. | `"Owners"` |

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

Expand Down
38 changes: 37 additions & 1 deletion api/access_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
BACKEND = "BACKEND"
NAME_VALIDATION_PATTERN = "NAME_VALIDATION_PATTERN"
NAME_VALIDATION_ERROR = "NAME_VALIDATION_ERROR"
DEFAULT_GROUP_QUERY_PARAMS = "DEFAULT_GROUP_QUERY_PARAMS"
OKTA_GROUP_NAME_PREFIX = "OKTA_GROUP_NAME_PREFIX"
ROLE_GROUP_NAME_PREFIX = "ROLE_GROUP_NAME_PREFIX"
APP_GROUP_NAME_PREFIX = "APP_GROUP_NAME_PREFIX"
APP_NAME_GROUP_NAME_SEPARATOR = "APP_NAME_GROUP_NAME_SEPARATOR"
APP_OWNERS_GROUP_NAME_SUFFIX = "APP_OWNERS_GROUP_NAME_SUFFIX"


class UndefinedConfigKeyError(Exception):
Expand All @@ -27,9 +33,25 @@ 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,
default_group_query_params: dict[str, str],
okta_group_name_prefix: str,
role_group_name_prefix: str,
app_group_name_prefix: str,
app_name_group_name_separator: str,
app_owners_group_name_suffix: str,
):
self.name_pattern = name_pattern
self.name_validation_error = name_validation_error
self.default_group_query_params = default_group_query_params
self.okta_group_name_prefix = okta_group_name_prefix
self.role_group_name_prefix = role_group_name_prefix
self.app_group_name_prefix = app_group_name_prefix
self.app_name_group_name_separator = app_name_group_name_separator
self.app_owners_group_name_suffix = app_owners_group_name_suffix


def _get_config_value(config: dict[str, Any], key: str) -> Any:
Expand Down Expand Up @@ -76,10 +98,24 @@ def _load_access_config() -> AccessConfig:

name_pattern = _get_config_value(config, NAME_VALIDATION_PATTERN)
name_validation_error = _get_config_value(config, NAME_VALIDATION_ERROR)
default_group_query_params = config.get(
DEFAULT_GROUP_QUERY_PARAMS, {"filter": 'type eq "BUILT_IN" or type eq "OKTA_GROUP"'}
)
okta_group_name_prefix = _get_config_value(config, OKTA_GROUP_NAME_PREFIX)
role_group_name_prefix = _get_config_value(config, ROLE_GROUP_NAME_PREFIX)
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)
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,
default_group_query_params=default_group_query_params,
okta_group_name_prefix=okta_group_name_prefix,
role_group_name_prefix=role_group_name_prefix,
app_group_name_prefix=app_group_name_prefix,
app_name_group_name_separator=app_name_group_name_separator,
app_owners_group_name_suffix=app_owners_group_name_suffix,
)


Expand Down
12 changes: 8 additions & 4 deletions api/models/core_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
from sqlalchemy_json import mutable_json_type

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

access_config = get_access_config()


class OktaUserGroupMember(db.Model):
# See https://stackoverflow.com/a/60840921
Expand Down Expand Up @@ -251,6 +254,7 @@ class OktaUser(db.Model):


class OktaGroup(db.Model):
OKTA_GROUP_NAME_PREFIX = access_config.okta_group_name_prefix
__tablename__ = "okta_group"
id: Mapped[str] = mapped_column(db.Unicode(50), primary_key=True, nullable=False)
type: Mapped[str] = mapped_column(db.Unicode(50), nullable=False)
Expand Down Expand Up @@ -539,7 +543,7 @@ def validate_group(self, key: str, group: OktaGroup) -> OktaGroup:


class RoleGroup(OktaGroup):
ROLE_GROUP_NAME_PREFIX = "Role-"
ROLE_GROUP_NAME_PREFIX = access_config.role_group_name_prefix

__tablename__ = "role_group"
id: Mapped[str] = mapped_column(db.Unicode(50), db.ForeignKey("okta_group.id"), primary_key=True)
Expand Down Expand Up @@ -587,9 +591,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 = access_config.app_group_name_prefix
APP_NAME_GROUP_NAME_SEPARATOR = access_config.app_name_group_name_separator
APP_OWNERS_GROUP_NAME_SUFFIX = access_config.app_owners_group_name_suffix

__tablename__ = "app_group"
id: Mapped[str] = mapped_column(db.Unicode(50), db.ForeignKey("okta_group.id"), primary_key=True)
Expand Down
6 changes: 4 additions & 2 deletions api/services/okta_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from okta.models.user_schema import UserSchema as OktaUserSchemaType
from okta.request_executor import RequestExecutor as OktaRequestExecutor

from api.access_config import get_access_config
from api.config import OKTA_GROUP_PROFILE_CUSTOM_ATTR
from api.models import OktaGroup, OktaUser

Expand Down Expand Up @@ -259,9 +260,10 @@ async def _get_group(groupId: str) -> Group:

return asyncio.run(_get_group(groupId))

DEFAULT_QUERY_PARAMS = {"filter": 'type eq "BUILT_IN" or type eq "OKTA_GROUP"'}
def list_groups(self, *, query_params: dict[str, str] | None = None) -> list[Group]:
if query_params is None:
query_params = get_access_config().default_group_query_params

def list_groups(self, *, query_params: dict[str, str] = DEFAULT_QUERY_PARAMS) -> list[Group]:
async def _list_groups(query_params: dict[str, str]) -> list[Group]:
async with self._get_sessioned_okta_request_executor() as _:
groups, resp, error = await OktaService._retry(self.okta_client.list_groups, query_params=query_params)
Expand Down
2 changes: 1 addition & 1 deletion api/views/schemas/core_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ class OktaGroupSchema(SQLAlchemyAutoSchema):
validate=validate.And(
validate.Length(min=1, max=255),
validate.Regexp(
f"^{access_config.name_pattern}$",
f"^{OktaGroup.OKTA_GROUP_NAME_PREFIX}{access_config.name_pattern}$",
error=f"Group {access_config.name_validation_error} Regex to match: /{{regex}}/",
),
),
Expand Down
16 changes: 14 additions & 2 deletions config/config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,22 @@
},
"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.",
"OKTA_GROUP_NAME_PREFIX": "",
"ROLE_GROUP_NAME_PREFIX": "Role-",
"APP_GROUP_NAME_PREFIX": "App-",
"APP_NAME_GROUP_NAME_SEPARATOR": "-"
},
"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.",
"DEFAULT_GROUP_QUERY_PARAMS": {
"filter": "type eq \"BUILT_IN\" or type eq \"OKTA_GROUP\""
},
"OKTA_GROUP_NAME_PREFIX": "",
"ROLE_GROUP_NAME_PREFIX": "Role-",
"APP_GROUP_NAME_PREFIX": "App-",
"APP_NAME_GROUP_NAME_SEPARATOR": "-",
"APP_OWNERS_GROUP_NAME_SUFFIX": "Owners"
}
}
4 changes: 4 additions & 0 deletions src/config/accessConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ export interface AccessConfig {
DEFAULT_ACCESS_TIME: string;
NAME_VALIDATION_PATTERN: string;
NAME_VALIDATION_ERROR: string;
OKTA_GROUP_NAME_PREFIX: string;
ROLE_GROUP_NAME_PREFIX: string;
APP_GROUP_NAME_PREFIX: string;
APP_NAME_GROUP_NAME_SEPARATOR: string;
}

// use the globally-injected ACCESS_CONFIG from src/globals.d.ts, typed to AccessConfig interface
Expand Down
14 changes: 9 additions & 5 deletions src/pages/groups/CreateUpdate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ 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 OKTA_GROUP_PREFIX = accessConfig.OKTA_GROUP_NAME_PREFIX;
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 Expand Up @@ -167,6 +168,7 @@ function GroupDialog(props: GroupDialogProps) {

switch (group.type) {
case 'okta_group':
group.name = OKTA_GROUP_PREFIX + group.name;
break;
case 'role_group':
group.name = ROLE_GROUP_PREFIX + group.name;
Expand Down Expand Up @@ -255,12 +257,14 @@ function GroupDialog(props: GroupDialogProps) {
flexDirection: 'row',
alignItems: 'center',
}}>
{groupType == 'app_group' || groupType == 'role_group' ? (
{groupType == 'app_group' || groupType == 'role_group' || (groupType == 'okta_group' && OKTA_GROUP_PREFIX !== '') ? (
<Box sx={{mx: 1}}>
<Typography noWrap={true} variant="h6">
{groupType == 'role_group'
? ROLE_GROUP_PREFIX
: APP_GROUP_PREFIX + (appName == '' ? '<App>' : appName) + APP_NAME_APP_GROUP_SEPARATOR}
: groupType == 'app_group'
? APP_GROUP_PREFIX + (appName == '' ? '<App>' : appName) + APP_NAME_APP_GROUP_SEPARATOR
: OKTA_GROUP_PREFIX}
</Typography>
</Box>
) : null}
Expand Down
Loading