Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ For application-developer workflows and broader product integration guidance, us
## Code expectations

- Follow existing Django patterns and project conventions.
- Avoid per-object database queries in API serializers and admin list displays,
including queries hidden in computed model properties. Load related data with
`select_related()` or `prefetch_related()` in every applicable queryset path,
and consume the prefetched relations without issuing fresh queries such as
`values_list()` calls. Add focused query-count regression tests when exposing
relation-dependent computed fields in lists.
- Prefer the repository's configured Ruff-based formatting and linting rules.
- Use human-readable Ruff rule names in overrides, such as
`# ruff: ignore[assert]`; do not use or rewrite them to cryptic codes such as
Expand Down Expand Up @@ -119,6 +125,19 @@ For application-developer workflows and broader product integration guidance, us

## Weblate-specific guardrails

- Changes to built-in role permission defaults in `weblate/auth/data.py` require
a data migration. Run `./manage.py generate_role_migration`, review and format
the generated migration, and verify `./manage.py generate_role_migration --check`.
Keep old and new permission sets and required permission labels frozen in each
migration; never import runtime role definitions or model helpers into it.
Preserve existing snapshots so upgrades through skipped releases merge each
change in order. Use historical models and the migration database alias.
The post-migration hook must only create missing roles, never overwrite
permissions of existing roles. Built-in role names are persistent identifiers.
Upstream renames require an explicit migration preserving role IDs, team
assignments, and local permission differences. Map the old baseline to the
new name before merging and freeze the renamed defaults for future generation;
a generated remove/add migration does not preserve role identity.
- Be careful with repository, webhook, and file-handling code; validate inputs
and avoid introducing path traversal, command injection, or script injection
risks.
Expand Down
25 changes: 25 additions & 0 deletions ci/migrate-scripts/assert-editable-roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright © Michal Čihař <michal@weblate.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

from django.core.management import call_command

from weblate.auth.data import ROLES
from weblate.auth.models import Group, Role

team = Group.objects.get(name__startswith="Role migration assignment ")
role_id, custom_id = map(int, team.name.rsplit(" ", 1)[1].split(":"))
role = Role.objects.get(pk=role_id)
assert role.name == "Access repository"
assert team.roles.filter(pk=role_id).exists()
assert set(role.permissions.values_list("codename", flat=True)) == set(
dict(ROLES)[role.name]
)
custom = Role.objects.get(pk=custom_id, name="Role migration custom")
assert list(custom.permissions.values_list("codename", flat=True)) == ["comment.add"]
role.permissions.clear()
call_command("migrate", verbosity=0)
assert not role.permissions.exists()
assert team.roles.filter(pk=role_id).exists()
16 changes: 16 additions & 0 deletions ci/migrate-scripts/setup-editable-roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright © Michal Čihař <michal@weblate.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

from weblate.auth.models import Group, Permission, Role

role = Role.objects.get(name="Access repository")
team = Group.objects.create(name="Role migration assignment")
team.roles.add(role)
custom = Role.objects.create(name="Role migration custom")
custom.permissions.set(Permission.objects.filter(codename="comment.add"))
# Persist original primary keys across the checkout to verify identity preservation.
team.name = f"Role migration assignment {role.pk}:{custom.pk}"
team.save()
13 changes: 13 additions & 0 deletions ci/run-migrate
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ if [ "$bot_membership_cleanup" -eq 0 ]; then
check
fi

# Seed built-in role identity and custom roles before editable role synchronization.
semver_compare "$1" "<" "2026.11"
editable_roles_migrate=$?
if [ "$editable_roles_migrate" -eq 0 ]; then
./manage.py shell < "$MIGRATE_DIR/migrate-scripts/setup-editable-roles.py"
check
fi

# Add a pending unit
semver_compare "$1" "<" "5.12"
pending_migration_test=$?
Expand Down Expand Up @@ -351,3 +359,8 @@ if [ "$screenshot_extensions_migrate" -eq 0 ]; then
uv run --all-extras ./manage.py shell < "$MIGRATE_DIR/migrate-scripts/assert-screenshot-extensions.py"
check
fi

if [ "$editable_roles_migrate" -eq 0 ]; then
uv run --all-extras ./manage.py shell < "$MIGRATE_DIR/migrate-scripts/assert-editable-roles.py"
check
fi
52 changes: 42 additions & 10 deletions docs/admin/access.rst
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@
control <acl>` to utilize it. However you must have superuser privileges in
order to use it.

If you are not interested in details of implementation, and just want to create a

Check failure on line 365 in docs/admin/access.rst

View workflow job for this annotation

GitHub Actions / Linkcheck

https://hosted.weblate.org/: 403 Client Error: Forbidden for url: https://hosted.weblate.org/
simple-enough configuration based on the defaults, or don’t have a site-wide access
to the whole Weblate installation (like on `Hosted Weblate <https://hosted.weblate.org/>`_),
please refer to the :ref:`manage-acl` section.
Expand All @@ -384,9 +384,15 @@
the site-wide permissions provided by membership in the `Users` and `Reviewers` teams
won’t have any effect.

You may also grant some additional permissions of your choice to the default
teams. For example, you may want to give a permission to manage screenshots to all
the `Users`.
You may also grant additional permissions to the default teams. To change a
built-in role for every team using it, customize its permissions as described in
:ref:`default-roles`. For example, adding screenshot permissions to `Power user`
affects the `Users` team and any other teams assigned that role.

To change permissions for only one team, create a custom role and assign it to
that team. Custom roles remain independent of upstream defaults. To reduce a
team's permissions, replace or remove its existing roles as needed; adding a
narrower role does not revoke permissions granted by another role.

You can define some new custom teams as well. If you want to
keep managing your permissions site-wide for these teams, choose an
Expand Down Expand Up @@ -667,15 +673,41 @@
Bulk inviting through the user interface creates invitations. Existing
users still have to confirm the invitation before they become team members.

.. _default-roles:

Default teams and roles
-----------------------

After installation, a default set of teams is created (see :ref:`default-teams`).

These roles and teams are created upon installation. The built-in roles are
always kept up to date by the database migration when upgrading. You can’t
actually change them, please define a new role if you want to define your own
set of permissions.
These roles and teams are created upon installation. Administrators can edit
built-in role permissions in the management interface or through the REST API.
Built-in roles have fixed names and cannot be deleted. The role list identifies
built-in roles and indicates whether their permissions are customized. Editing a
role affects every team assigned that role. You no longer need to create a custom
role solely to change built-in defaults; use a custom role when you need an
independent permission set for selected teams.

During upgrades, database migrations merge changes to the built-in defaults with
your assigned permissions. Locally added permissions remain assigned, and locally
removed permissions remain excluded. New upstream permissions are added, and
permissions removed from upstream defaults are removed. Custom roles are unchanged.
The merge compares permission sets, not editing history: keeping an existing
default permission does not record a separate local addition. If an upstream
permission is removed and later reintroduced, it is treated as a new upstream
addition. A previous local removal does not survive the permission's intervening
absence from upstream defaults.

To discard customizations, select built-in roles and use :guilabel:`Reset built-in
roles to defaults` in the management interface, then confirm the reset. Resetting
applies the current upstream defaults to all teams using those roles.

.. versionchanged:: 2026.11

Built-in role permissions became editable. On the first upgrade to this
version, existing built-in roles are assumed to be synchronized and receive
the current defaults. Earlier changes made through the API or database are
replaced. Subsequent upgrades preserve local customizations.

.. _privileges:

Expand All @@ -689,9 +721,9 @@
instance when assigned through a site-wide team. The team's project
selection does not narrow these permissions.

Site-wide privileges are not granted to any default role. Treat custom roles
containing them as trusted administrative access because some are
effectively equivalent to superuser status. For example, ``group.edit``
Site-wide privileges are not granted to any default role. Treat any roles
containing them, including customized built-in roles, as trusted administrative
access because some are effectively equivalent to superuser status. For example, ``group.edit``
allows changing team scope to grant access to private projects, and
``user.edit`` allows users to change their own team memberships and grant
themselves superuser status. See :ref:`site-wide-team-management` and
Expand Down
18 changes: 15 additions & 3 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,8 @@ Roles
:type id: int
:>json string name: Role name
:>json array permissions: list of codenames of permissions
:>json boolean is_builtin: whether this is a built-in role (read-only)
:>json boolean is_customized: whether a built-in role differs from current defaults (read-only)

**Example JSON data:**

Expand All @@ -889,7 +891,7 @@ Roles

.. http:put:: /api/roles/(int:id)/

Changes the role parameters.
Changes the role parameters. Built-in role names cannot be changed.

:param id: Role's ID
:type id: int
Expand All @@ -898,7 +900,7 @@ Roles

.. http:patch:: /api/roles/(int:id)/

Changes the role parameters.
Changes the role parameters. Built-in role names cannot be changed.

When supplied, ``permissions`` replaces the role's complete permission set.
Omit it to keep the existing permissions unchanged, or pass an empty list to
Expand All @@ -911,7 +913,17 @@ Roles

.. http:delete:: /api/roles/(int:id)/

Deletes the role.
Deletes a custom role. Built-in roles cannot be deleted.

:param id: Role's ID
:type id: int


.. http:post:: /api/roles/(int:id)/reset/

Resets a built-in role to current upstream permission defaults and returns
the updated role. Requires the same role-management permission as editing.
Custom roles cannot be reset.

:param id: Role's ID
:type id: int
Expand Down
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Weblate 2026.11

.. rubric:: New features

* Built-in :ref:`roles <default-roles>` can be customized, with permission changes preserved across upgrades and a reset to upstream defaults.

* Added optional self-hosted :ref:`bot-protection` as a technology preview, with proactive anonymous proof of work and verified crawler exemptions following the robots.txt crawl policy.

* Added a :guilabel:`Languages` tab to :ref:`workspaces`, with translation and review across projects in a selected language.
Expand Down
27 changes: 27 additions & 0 deletions docs/contributing/internals.rst
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,30 @@ workload:
The backup tasks cannot be executed in parallel, and a single dedicated worker makes this easier.
``translate``
Automatic translation tasks are known to take long because they hit external services.


Built-in role defaults
----------------------

Changes to built-in roles in :file:`weblate/auth/data.py` require a data migration.
Run the following after changing the definitions, then review and format the
resulting migration::

./manage.py generate_role_migration
./manage.py generate_role_migration --check

Each generated migration freezes the old and new permission codenames and merges
them with assigned permissions using historical models. Keep these snapshots
independent of runtime definitions so upgrades through skipped releases apply each
change in order. The consistency test rejects definitions without a matching
migration. Role-update migrations are irreversible because merging can discard
information. The post-migration hook only creates missing roles.


Built-in role names are persistent identifiers, not freely changeable labels.
An upstream rename requires an explicit data migration that preserves the role's
primary key, team assignments, and local permission differences. Rename the
existing role and map its old upstream baseline to the new name before merging.
Freeze the renamed upstream defaults in that migration so subsequent generation
uses the correct baseline. A generated remove/add migration would create a new
role and leave teams assigned to the old one.
Loading
Loading