Groups and Users
Security in Fullfinity is managed through groups that control access to features and data.
Security Groups
Section titled “Security Groups”Groups define sets of permissions that can be assigned to users.
Defining Groups
Section titled “Defining Groups”Create groups in your module’s security file:
{ "data_type": "Group", "identifier": "sales_user_group", "name": "User", "category": "Sales"}The
data_typeisGroup(the model’s verbose name is “User Group”). A group hasname,category(a plain text label, not a relation), an optionalexclusiveflag, andimplied_groups. There is nodescriptionfield on the group model.
Group Hierarchy
Section titled “Group Hierarchy”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_groupsis 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.
How Permission Inheritance Works
Section titled “How Permission Inheritance Works”When a user belongs to a group with implied_groups, they automatically receive:
- All ModelAccess permissions from implied groups (union of all permissions)
- All RecordRule access from implied groups
- All menu/view visibility controlled by implied groups
Example chain:
invoicing_admin → invoicing_bookkeeper → invoicing_user → core_internalA 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
Common Group Patterns
Section titled “Common Group Patterns”[ { "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"]] }]User Model
Section titled “User Model”The built-in User model stores user information:
| Field | Type | Description |
|---|---|---|
name | Char | Display name |
email | Char | Email (login) |
hashed_password | Char | Hashed password |
active | Boolean | Account active |
groups | ManyToMany | Assigned groups |
Assigning Groups to Users
Section titled “Assigning Groups to Users”# 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.
Default Groups
Section titled “Default 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 nomodule.namedotted form. As a Many-to-Many relation,groupstakes the[["L", "<identifier>"]]link-command form.
Built-in User Type Groups
Section titled “Built-in User Type Groups”These core groups define the user type and control route-level access:
| Identifier | Name | Category | Description |
|---|---|---|---|
core_internal | Internal | User Type | Backend users with full system access |
core_portal | Portal | User Type | External users with limited access (customers, vendors) |
core_public | Public | User Type | Guest/anonymous users |
Every user should belong to exactly one of these user type groups.
core_internal
Section titled “core_internal”Internal users have access to the full backend application. Use for employees and administrators.
core_portal
Section titled “core_portal”Portal users are external users (customers, vendors, contacts) with restricted access. They can only see their own records and portal-specific features.
core_public
Section titled “core_public”Public/guest users have minimal access. Used for anonymous website visitors.
Other Built-in Groups
Section titled “Other Built-in Groups”| Identifier | Name | Description |
|---|---|---|
core_admin | Settings | Access to system administration and settings |
Group Categories
Section titled “Group Categories”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"}Exclusive vs. Independent Selection
Section titled “Exclusive vs. Independent Selection”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 likeSalesoffering User / Manager where you hold exactly one tier. (Tiers still stack permissions viaimplied_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}Checking Permissions
Section titled “Checking Permissions”In Python Code
Section titled “In Python Code”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 groupif "sales_manager_group" in group_names: # Manager-only logic pass
# Check multiple groupsif any(g in group_names for g in ["sales_manager_group", "sales_admin_group"]): passIn Views
Section titled “In Views”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_companyIn Menu Items
Section titled “In Menu Items”- data_type: UiMenu name: Setup identifier: sales_config_menu parent: core_crm_menu action: configuration_sales_setup_action sequence: 100 groups: [sales_manager_group]Multi-Company Groups
Section titled “Multi-Company Groups”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: falseNext Steps
Section titled “Next Steps”- Route Authentication - Protecting API routes
- Model Access - CRUD permissions
- Record Rules - Row-level security