# How RomM Implements User Roles and Permissions for Library Access Control

> Discover how RomM implements user roles and permissions for robust library access control. Learn about its layered system combining admin/user flags, groups, and overrides.

- Repository: [The RomM Project/romm](https://github.com/rommapp/romm)
- Tags: how-to-guide
- Published: 2026-07-06

---

**RomM controls library access through a layered permission system that combines a binary admin/user role flag with granular permission groups, per-user overrides, and hidden entity lists.**

RomM, the self-hosted ROM and emulator management platform, implements a sophisticated authorization architecture that allows administrators to precisely control who can view, modify, or delete library content. According to the `rommapp/romm` source code, the system balances simple role-based access with fine-grained capability grants, making it suitable for both small personal collections and larger shared libraries.

## The Two-Level Role Model

At the foundation of RomM's access control is a simple enum defined in [`backend/models/user.py`](https://github.com/rommapp/romm/blob/main/backend/models/user.py) (lines 25-31) that distinguishes between administrators and regular users:

```python
class Role(enum.StrEnum):
    USER = "user"
    ADMIN = "admin"

```

**Admins** bypass every permission check automatically, granting them unrestricted access to all platforms, ROMs, and administrative functions. **Users**, conversely, must have their specific capabilities defined through permission groups and overrides. This binary distinction allows the system to short-circuit permission lookups for administrators while enforcing granular controls for everyone else.

## Granular Permission Groups

For non-administrative users, RomM employs a `PermissionGroup` model defined in [`backend/models/permission.py`](https://github.com/rommapp/romm/blob/main/backend/models/permission.py) (lines 62-88) that creates named permission sets:

```python
class PermissionGroup(BaseModel):
    name: Mapped[str] = mapped_column(String(255), unique=True, index=True)
    grants: Mapped[list[PermissionGroupGrant]] = relationship(...)

```

Each `PermissionGroupGrant` specifies:
- **Entity**: The resource type (e.g., `ROMS`, `PLATFORMS`, `COLLECTIONS`)
- **Action**: The operation allowed (`READ`, `WRITE`, `DELETE`)
- **Ownership scope**: Whether the grant applies only to resources the user owns (`own_only`) or to all resources of that type

This design allows administrators to create reusable role templates such as "Editor" or "Viewer" and assign them to multiple users without configuring individual permissions for each account.

## Per-User Permission Overrides

When a permission group does not provide exactly the right access level, RomM supports individual capability overrides through the `UserPermissionOverride` model in [`backend/models/permission.py`](https://github.com/rommapp/romm/blob/main/backend/models/permission.py) (lines 116-136). These overrides modify the effective permissions calculated from the user's group assignment and take precedence over group grants.

As shown in the RomM implementation, overrides can **grant** additional capabilities not present in the user's group or **revoke** specific permissions that would otherwise be allowed. The model tracks the user ID, target entity, action type, and whether the exception applies only to owned resources.

## Hidden Entities for Content Restriction

Beyond action-based permissions, RomM can restrict visibility of specific platforms or ROMs through the `HiddenEntity` model in [`backend/models/permission.py`](https://github.com/rommapp/romm/blob/main/backend/models/permission.py) (lines 138-172). This feature allows administrators to hide individual resources from specific users or entire permission groups:

```python
hidden = HiddenEntity(
    entity=PermEntity.PLATFORMS,
    entity_id=42,          # platform PK

    user_id=new_user.id,   # hide only for this user

)

```

When `user_id` is set, the entity is hidden only for that specific user. When `permission_group_id` is set, the restriction applies to all members of that group. This mechanism enables sophisticated content filtering, such as hiding mature-rated platforms from certain user accounts or restricting access to specific ROM collections.

## Permission Resolution Logic

The actual enforcement of permissions occurs in [`backend/handler/auth/permissions.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/permissions.py) (lines 11-27) through the `resolve_permissions()` function. This function builds a `ResolvedPermissions` object that represents the user's effective capabilities:

1. **Admin short-circuit**: If `user.role == Role.ADMIN`, the function immediately returns full permissions without database lookups
2. **Group resolution**: For regular users, the function fetches the assigned permission group (or the server-wide default)
3. **Override merging**: Individual user overrides are applied on top of group grants
4. **Hidden entity filtering**: Lists of restricted platforms and ROMs are attached to the result

The resolved permissions object provides helper methods like `can_see_rom(rom_id, platform_id)` that endpoints use to validate requests before processing.

## Legacy OAuth Scope Integration

RomM maintains backward compatibility with an older scope-based authorization system. The `compute_oauth_scopes()` function in [`backend/handler/auth/permissions.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/permissions.py) (lines 65-84) projects granular permission grants onto legacy OAuth scopes (such as `Scope.ME_READ` or `Scope.ROMS_WRITE`).

The mapping logic in [`backend/handler/auth/permissions_map.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/permissions_map.py) (lines 1-24) translates specific entity-action combinations into coarse OAuth scopes:

```python
def grants_to_scopes(grants: list[PermissionGroupGrant]) -> list[str]:
    scopes = set()
    for grant in grants:
        if grant.entity == PermEntity.ROMS and grant.action == PermAction.WRITE:
            scopes.add("roms.write")
    return list(scopes)

```

This translation layer ensures that API clients using legacy authentication tokens receive appropriate access levels while the internal system moves toward the more granular permission model.

## Kiosk Mode Restrictions

When RomM operates in kiosk mode, the permission system enforces additional restrictions. The `KIOSK_MODE` configuration flag forces all non-admin users to read-only access regardless of their permission group assignments. This override occurs in both the `resolve_permissions()` function and the OAuth scope calculation (`_compute_non_admin_scopes()`), ensuring that kiosk users cannot modify library content even if their user account otherwise has write permissions.

## Practical Implementation Examples

Creating a user with custom permissions involves several steps through RomM's database handlers:

```python

# Create a regular user with default role

new_user = User(
    username="alice",
    hashed_password="...",
    role=Role.USER,
    enabled=True,
)
db_user_handler.create_user(new_user)

# Assign to a permission group

editor_group = db_permission_handler.get_group_by_name("Editor")
db_user_handler.update_user(new_user.id, {"permission_group_id": editor_group.id})

# Add a specific override for ROM write access

override = UserPermissionOverride(
    user_id=new_user.id,
    entity=PermEntity.ROMS,
    action=PermAction.WRITE,
    granted=True,
    own_only=False,
)
db_permission_handler.create_user_override(override)

# Hide a specific platform from the user

hidden = HiddenEntity(
    entity=PermEntity.PLATFORMS,
    entity_id=42,
    user_id=new_user.id,
)
db_permission_handler.create_hidden_entity(hidden)

```

In API endpoints, permissions are checked using the dependency injection pattern:

```python
from backend.handler.auth.permissions import resolve_permissions

@app.get("/roms/{rom_id}")
def get_rom(rom_id: int, user: User = Depends(get_current_user)):
    perms = resolve_permissions(user)
    if not perms.can_see_rom(rom_id, platform_id):
        raise HTTPException(status_code=404, detail="Not found")
    return db_rom_handler.get_rom(rom_id)

```

## Summary

- **Binary role foundation**: RomM uses a simple `admin`/`user` enum in [`backend/models/user.py`](https://github.com/rommapp/romm/blob/main/backend/models/user.py) to provide immediate full access or trigger granular permission checks
- **Permission groups**: Named collections of entity-action grants in [`backend/models/permission.py`](https://github.com/rommapp/romm/blob/main/backend/models/permission.py) serve as reusable templates for user capabilities
- **Per-user overrides**: The `UserPermissionOverride` model allows fine-tuning of group permissions for individual accounts
- **Content hiding**: The `HiddenEntity` model restricts visibility of specific platforms or ROMs per-user or per-group
- **Resolution logic**: The `resolve_permissions()` function in [`backend/handler/auth/permissions.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/permissions.py) calculates effective permissions by merging groups, overrides, and hidden entities
- **Legacy support**: OAuth scopes are computed from granular grants via [`backend/handler/auth/permissions_map.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/permissions_map.py) for backward compatibility
- **Kiosk mode**: When enabled, forces read-only access for all non-admin users regardless of assigned permissions

## Frequently Asked Questions

### How do I grant a user full access to all ROMs without making them an admin?

Assign the user to a permission group that includes a grant with `entity=PermEntity.ROMS`, `action=PermAction.WRITE`, and `own_only=False`. As implemented in [`backend/models/permission.py`](https://github.com/rommapp/romm/blob/main/backend/models/permission.py), setting `own_only=False` grants access to all ROMs of that entity type rather than just those owned by the user. If no existing group provides these permissions, create a new permission group and assign the user to it.

### What is the difference between permission groups and user overrides?

**Permission groups** are reusable templates defined in [`backend/models/permission.py`](https://github.com/rommapp/romm/blob/main/backend/models/permission.py) that assign the same set of capabilities to multiple users. **User overrides** are specific to individual accounts and modify the effective permissions calculated from the user's group. Overrides can add capabilities not present in the group or revoke specific permissions that the group would otherwise allow. The resolver in [`backend/handler/auth/permissions.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/permissions.py) applies group grants first, then processes overrides to determine the final permission set.

### How does kiosk mode affect existing user permissions?

When `KIOSK_MODE` is enabled, the permission resolution logic in [`backend/handler/auth/permissions.py`](https://github.com/rommapp/romm/blob/main/backend/handler/auth/permissions.py) forces all non-admin users to read-only access regardless of their permission group assignments or individual overrides. This affects both the internal `ResolvedPermissions` object and the OAuth scopes returned by `compute_oauth_scopes()`. Users retain their group memberships and override settings in the database, but the system ignores write and delete permissions while kiosk mode remains active.

### Can I hide specific platforms from certain users while allowing access to others?

Yes. Create a `HiddenEntity` record in [`backend/models/permission.py`](https://github.com/rommapp/romm/blob/main/backend/models/permission.py) specifying the platform ID and either the `user_id` to hide it from a specific user or the `permission_group_id` to hide it from an entire group. The permission resolver checks these hidden entity lists during `resolve_permissions()` and filters out restricted platforms from search results and API responses. This operates independently from action-based permissions, meaning a user could have `READ` permission for platforms generally but still be blocked from seeing a specific hidden platform.