Skip to content

Groups and Users

Security in Fullfinity is managed through groups that control access to features and data.

Groups define sets of permissions that can be assigned to users.

Create groups in your module’s security file:

{
"data_type": "Group",
"identifier": "sales_user_group",
"name": "User",
"category": "Sales"
}

The data_type is Group (the model’s verbose name is “User Group”). A group has name, category (a plain text label, not a relation), an optional exclusive flag, and implied_groups. There is no description field on the group model.

Groups can inherit from other groups:

{
"data_type": "Group",
"identifier": "sales_manager_group",
"name": "Manager",
"category": "Sales",
"implied_groups": [["L", "sales_user_group"]]
}

Users in sales_manager_group automatically get all permissions from sales_user_group.

implied_groups is a Many-to-Many relation, so it uses the relation-command form [["L", "<identifier>"]] (L = link by identifier) — not a bare list of identifier strings.

When a user belongs to a group with implied_groups, they automatically receive:

  1. All ModelAccess permissions from implied groups (union of all permissions)
  2. All RecordRule access from implied groups
  3. All menu/view visibility controlled by implied groups

Example chain:

invoicing_admin → invoicing_bookkeeper → invoicing_user → core_internal

A user in invoicing_admin gets permissions from all four groups. If core_internal has read-only access to Currency and invoicing_admin has full CRUD, the user gets full CRUD (permissions are merged with OR logic).

Important: When defining security for a module hierarchy:

  • Base group (e.g., mymodule_user) should have minimal permissions
  • Higher groups add permissions via their own ModelAccess rules
  • Users get the union of all permissions from their groups and all implied groups
[
{
"data_type": "Group",
"identifier": "crm_user_group",
"name": "User",
"category": "CRM"
},
{
"data_type": "Group",
"identifier": "crm_manager_group",
"name": "Manager",
"category": "CRM",
"implied_groups": [["L", "crm_user_group"]]
}
]

The built-in User model stores user information:

FieldTypeDescription
nameCharDisplay name
emailCharEmail (login)
hashed_passwordCharHashed password
activeBooleanAccount active
groupsManyToManyAssigned groups
# In code: assign groups via the relation-command form on update()
User = get_model("User")
user = await User.filter(id=user_id).get()
await user.update(groups=[["L", sales_manager_group_id]])

update() on the User model automatically resolves implied groups (via sync_implied_groups) so the user also receives every group reachable through implied_groups.

Create default user groups in data files:

{
"data_type": "User",
"identifier": "demo_user",
"name": "Demo User",
"email": "demo@example.com",
"groups": [["L", "core_internal"], ["L", "sales_user_group"]]
}

Group identifiers are bare (e.g. core_internal, sales_user_group) — there is no module.name dotted form. As a Many-to-Many relation, groups takes the [["L", "<identifier>"]] link-command form.

These core groups define the user type and control route-level access:

IdentifierNameCategoryDescription
core_internalInternalUser TypeBackend users with full system access
core_portalPortalUser TypeExternal users with limited access (customers, vendors)
core_publicPublicUser TypeGuest/anonymous users

Every user should belong to exactly one of these user type groups.

Internal users have access to the full backend application. Use for employees and administrators.

Portal users are external users (customers, vendors, contacts) with restricted access. They can only see their own records and portal-specific features.

Public/guest users have minimal access. Used for anonymous website visitors.

IdentifierNameDescription
core_adminSettingsAccess to system administration and settings

category is a plain text label on the group itself — there is no separate category model or record to define. Groups that share a category string are grouped together in the UI, and (when exclusive is true, the default) are treated as mutually exclusive within that category. Built-in categories include User Type, Administration, and Others; module groups typically use the app name (e.g. Sales, Invoicing).

{
"data_type": "Group",
"identifier": "sales_user_group",
"name": "User",
"category": "Sales"
}

The boolean exclusive flag on a group controls how it is presented and selected within its category. It defaults to true.

  • exclusive: true (default) → radio. Exclusive groups in the same category are mutually exclusive — the user-access UI renders them as a radio set, and picking one unlinks the others. This is the access-tier pattern: a category like Sales offering User / Manager where you hold exactly one tier. (Tiers still stack permissions via implied_groups — Manager implies User — but you select only the highest.)
  • exclusive: false → switch. A non-exclusive group renders as an independent toggle, so it can be granted on its own without affecting the other groups in the category. Use it for optional add-on capabilities (e.g. a “Show Full Accounting Features” flag) that aren’t part of a one-of-N tier.
{
"data_type": "Group",
"identifier": "account_show_full_features",
"name": "Show Full Accounting Features",
"category": "Invoicing",
"exclusive": false
}

The current user’s resolved group identifiers (including all implied groups) are available on the environment as group_names. Check membership with a plain in:

from fullfinity.engine.context import env_ctx
env = env_ctx.get()
group_names = env.user_data.get("group_names", [])
# Check if user has a specific group
if "sales_manager_group" in group_names:
# Manager-only logic
pass
# Check multiple groups
if any(g in group_names for g in ["sales_manager_group", "sales_admin_group"]):
pass

Use a groups list (bare identifiers) under an element’s properties to show/hide it:

- type: field
name: company
properties:
widget: DataCombo
label: Company
groups:
- core_multi_company
- data_type: UiMenu
name: Setup
identifier: sales_config_menu
parent: core_crm_menu
action: configuration_sales_setup_action
sequence: 100
groups: [sales_manager_group]

A built-in non-exclusive core_multi_company group gates multi-company features. Add it to a group’s implied_groups, or to view elements via groups: [core_multi_company]:

- data_type: Group
name: Multi Company
identifier: core_multi_company
category: Others
exclusive: false