# Roles and Permissions

Vulnerability Lookup uses a fine-grained RBAC system to control who can read, create, modify, and delete resources. This guide explains how to configure it.
In Vulnerability Lookup, roles come in two flavours:
- **Global roles**: granted once and apply everywhere. A user with the `admin` role has administrative access across the entire instance.
- **Resource-scoped roles**: granted for a single object. A user with `bundle_owner` on bundle `abc-123` can modify that bundle but not any other bundle.

## Migrating an existing instance

Instances installed before the RBAC system stored user rights as three boolean
flags (`is_admin`, `is_commenter`, `is_reporter`). The migration replaces them
with roles and removes the columns.

To migrate, run the regular update command:

```bash
poetry run update
```

It backs up the PostgreSQL database, applies the migrations, and seeds the
RBAC defaults — no extra step is needed. If you prefer to run the steps
manually:

```bash
poetry run flask --app website.app db_backup
poetry run flask --app website.app db upgrade
poetry run flask --app website.app seed-rbac
```

The migration is automatic and preserves existing data:

- Every user's boolean flags are converted to the matching global roles
  (`admin`, `commenter`, `reporter`).
- Authors of existing comments, bundles, sightings, KEV entries,
  vulnerability disclosures, and CNA publications receive the scoped
  `*_owner` role for each of their resources, so they keep the ability to
  manage their own content.
- The default roles, permissions, creation policies, and the
  self-registration policy are created.

To verify the result, log in as an administrator and open **Admin → Roles**:
the built-in roles should be listed, and each user's previous flags should
appear as global roles on their user page.

Two intentional behaviour changes to be aware of after the migration:

- Reporters can now create and modify CVE records through the CNA API
  (`vulnerability:create` / `vulnerability:modify`), but no longer hold
  KEV-entry creation, CVE-ID reservation, record deletion, or the implicit
  commenter rights the old flag combination granted. Grant the
  corresponding roles or permissions explicitly where needed.
- API clients must use a PyVulnerabilityLookup version that matches the new
  user model (the boolean flags are gone from API responses).

Re-running `seed-rbac` at any time is safe: it only creates what is missing
and never modifies existing roles or policies. To restore the built-in roles
and policies to their shipped defaults (for example after locking yourself
out of `admin:access`), run:

```bash
poetry run flask --app website.app seed-rbac --reset-defaults
```

The admin interface guards against the most common lockouts — it refuses to
remove `admin:access` from the built-in `admin` role, to strip your own last
source of it, or to remove the instance's last administrator — so this reset
is mainly a recovery path for a database restored into an inconsistent state.

## Roles
Global roles are assigned per-user from the admin panel. You can create roles tailored to your instance and use-case. A user can hold multiple global roles simultaneously. Their effective permissions are the union of all permissions from all assigned roles.
> Note : Custom roles created through the UI are not seeded definitions — they exist only in the database. After a fresh install or database wipe you would need to recreate them.

### Default Roles
Default roles are seeded automatically (admin, commenter, reporter). They cannot be deleted through the UI, but their permission set can be adjusted if your instance has non-standard requirements.

### Owner Roles
Owner roles give a user control over a specific resource they created, without granting them global rights over all resources of that type. This separation is intentional.

Owner roles are assigned automatically by the creation policy for that resource type when the resource is saved. The creation policies can
be managed through the UI:

```{figure} /_static/img/creation_policies.png
:alt: Creation Policy View.
:target: /_static/img/creation_policies.png

Creation Policy View.
```

The policies are pre-configured for all built-in resource types but can be adjusted.


### Self-Registration Policy
When `SELF_REGISTRATION = True` is set in `config/website.py`, new users can register themselves via the API. The self-registration policy controls which global role they receive automatically.
The default assigns every self-registered user the `commenter` role, which lets them create comments and sightings — matching the behaviour of the previous `is_commenter` flag that defaulted to `True`.

The default can be changed by an admin:

```{figure} /_static/img/registration_policy.png
:alt: Registration Policy View.
:target: /_static/img/registration_policy.png

Registration Policy View.
```
Select a different globally-assignable role from the dropdown, or set it to `None` to create accounts with no permissions until an admin explicitly assigns roles.

Roles that grant `admin:access` are intentionally not offered here, and are rejected if submitted directly: because every account that registers receives this role, an administrator role would turn open self-registration into anonymous privilege escalation. If new users genuinely need administrative rights, assign the role to each account by hand after review.

Note that the role only becomes usable once the account is confirmed. Both the website login and the API require a confirmed account, so a self-registered user cannot exercise the default role's permissions — including through their auto-generated API key — until they confirm their email.

### Resource Scoped Assignments
An admin can grant resource specific roles using scoped assignments:

```{figure} /_static/img/role_assignments.png
:alt: Role Assignment View.
:target: /_static/img/role_assignments.png

Role Assignment View.
```

## Permissions
Permissions follow a `namespace:action` naming convention. Each namespace groups operations on one type of object. Permissions are the atomic unit so they are never assigned to users directly. They are only attached to roles.


## Developer Guide

All seeding data lives in `website/lib/rbac.py`. The default state is described by the following structures:

- `DEFAULT_PERMISSIONS` — every permission string the system knows about.
- `DEFAULT_ROLES` — maps role names to the list of permissions they include.
- `ROLE_ASSIGNMENT_RULES` — declares whether each role is globally assignable and which resource types it may be scoped to.
- `CREATION_POLICY_RESOURCES` — maps resource types to the owner role assigned on creation.

Route protection decorators live in `website/web/permissions.py`. The user model's `has_permission()` and `has_permission_for_resource()` methods are the underlying check.

### Adding a new Permission

1. Add one entry to `DEFAULT_PERMISSIONS` in `website/lib/rbac.py`. Use the `namespace:action` convention.
2. Then assign it to one or more roles in `DEFAULT_ROLES` (optionally)
3. run `poetry run flask --app website.app seed-rbac` to create the new permission. Existing roles are not modified by a plain seed run: to push the updated default permission sets onto the built-in roles, run it with `--reset-defaults` (this overwrites any customization of those roles) or grant the new permission through the admin interface.
> Info: The admin role is defined as `list(DEFAULT_PERMISSIONS.keys())`, so any new permission is granted to admins on a fresh seed or a `--reset-defaults` run

### Adding a new default Role

Add the role name and its permission list to `DEFAULT_ROLES`, then add an entry to `ROLE_ASSIGNMENT_RULES` to declare where it may be assigned.

For an owner role that only makes sense scoped to a single resource, set global to `False` and list the resource type in resources.


### Protecting routes

Import from `website.web.permissions` and apply a decorator above the view function.

```python
from flask_login import login_required
from website.web.permissions import permission_required

# login_required runs first, then permission check
@bp.route("/reports/export")
@login_required
@permission_required("report:export")
def export_report():
    ...
```

Some actions only make sense relative to a specific resource — for example, editing the bundle you created. Use the `resource_permission_required` decorator.

If the logic is more conditional or when you need to check a parent resource you can use `has_permission_for_resource()` or a corresponding
wrapper around that from `website/lib/authorization`.


### Resource Creation

When a new resource is saved, call `assign_creator_role_from_policy()` to trigger the creation policy for that resource type. The function looks up the configured owner role and adds a scoped `RoleAssignment` to the session; the caller owns the commit so the assignment stays atomic with the resource creation.

```python
from website.lib.authorization import assign_creator_role_from_policy

def create_report(data, author):
    report = Report(**data, author_id=author.id)
    db.session.add(report)
    db.session.flush()  # get report.id before assigning the role

    assign_creator_role_from_policy(
        user_id=author.id,
        resource_type="report",
        resource_id=report.id,
    )

    db.session.commit()
```

To register a new resource type with a creation policy, add it to `CREATION_POLICY_RESOURCES` in `website/lib/rbac.py`. Then run seed-rbac to write the policy row.
