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:

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:

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:

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:

Creation Policy View.

Fig. 5 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:

Registration Policy View.

Fig. 6 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:

Role Assignment View.

Fig. 7 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.

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.

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.