How to Configure GeoNode User Roles for Agency Teams
This guide walks through provisioning, synchronizing, and troubleshooting GeoNode role-based access control so that separate agency teams share one portal without leaking data across tenant boundaries.
It is a hands-on companion to the parent reference Implementing RBAC for Multi-Tenant GIS Portals and the broader Core Portal Architecture & Security Boundaries program — read those first for the request-path placement and tenant-isolation model that the steps below assume. Where that material treats access control as a declarative, version-controlled boundary, this page reduces it to the concrete commands an administrator runs to stand up AgencyAdmin, DataSteward, Analyst, and Viewer roles and to keep them in sync with GeoServer.
GeoNode inherits Django’s group-based permission model and extends it through django-guardian object-level permissions plus automated GeoServer ACL propagation over Celery. Treat role configuration as a synchronized subsystem rather than a one-off admin task: every group you create maps to Django permissions such as view_resourcebase, change_resourcebase, delete_resourcebase, and download_resourcebase, and each of those must propagate cleanly into GeoServer’s security layer before an agency user actually sees the access you granted.
Prerequisites
Confirm the following before you assign a single permission. Mismatched versions or backends are the root cause of most “the group exists but access is denied” tickets.
- GeoNode 4.x with
django-guardianinstalled (ships by default) and Django 3.2+. - GeoServer 2.23+ reachable from the GeoNode app container, with REST enabled.
- A Celery worker and broker (Redis or RabbitMQ) running — permission sync is asynchronous.
settings.pyaccess to confirmGEONODE_SECURITY_BACKEND,AUTHENTICATION_BACKENDS,RESOURCE_PUBLISHING, andGROUPS_MANAGERS_GROUP_NAME.- Environment variables
GEOSERVER_ADMIN_USERandGEOSERVER_ADMIN_PASSWORDset and matching the GeoServer service account. - Superuser (or
manage.pyshell) access to run the provisioning steps idempotently across environment promotions. - Read access to the log paths
/var/log/geonode/celery_worker.logandgeoserver.logfor verification.
Step-by-step implementation
1. Verify the security backends are ordered correctly
Object-level permissions only take effect if both Django’s ModelBackend and guardian’s ObjectPermissionBackend are registered, in that order. Confirm settings.py contains:
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
"guardian.backends.ObjectPermissionBackend",
)
GEONODE_SECURITY_BACKEND = "geonode.security.backends.GeoNodeBackend"
# RESOURCE_PUBLISHING scopes resources to their owning group, acting as the
# tenant container that keeps one agency's layers out of another's catalog.
RESOURCE_PUBLISHING = True
GROUPS_MANAGERS_GROUP_NAME = "managers"
A misconfigured GEONODE_SECURITY_BACKEND is what produces permission drift during high-concurrency API calls and bulk metadata imports — the kind of bulk work covered in the metadata ingestion pipelines — so fix this before provisioning.
2. Create the agency groups idempotently
Define groups through the ORM, not the admin UI, so the same routine runs cleanly in dev, staging, and production. Using get_or_create keeps the step repeatable across environment promotions:
from django.contrib.auth.models import Group
AGENCY_ROLES = ["AgencyAdmin", "DataSteward", "Analyst", "Viewer"]
for role in AGENCY_ROLES:
group, created = Group.objects.get_or_create(name=role)
if created:
print(f"Created group: {role}")
3. Assign object-level permissions inside a transaction
Wrap bulk assign_perm calls in an explicit transaction.atomic() block. If a deployment is interrupted mid-assignment, the transaction rolls back instead of leaving orphaned permission rows that later read as silent denials:
from guardian.shortcuts import assign_perm
from django.contrib.auth.models import Group
from django.db import transaction
with transaction.atomic():
admins = Group.objects.get(name="AgencyAdmin")
analysts = Group.objects.get(name="Analyst")
for resource in agency_resources: # queryset scoped to the agency
assign_perm("view_resourcebase", admins, resource)
assign_perm("change_resourcebase", admins, resource)
assign_perm("delete_resourcebase", admins, resource)
assign_perm("view_resourcebase", analysts, resource)
assign_perm("download_resourcebase", analysts, resource)
Keep Viewer limited to view_resourcebase and DataSteward to the change/download set without delete_resourcebase; the principle of least privilege here is the same boundary discipline described in Security Boundary Mapping for OGC Services.
4. Tune Celery so sync survives peak ingestion
Permission changes propagate to GeoServer asynchronously. Under heavy concurrent load, a worker that dies mid-task can leave a layer read-only. Set acknowledgement-late semantics and a prefetch of one so an interrupted task is redelivered rather than lost:
# celeryconf.py / settings.py
CELERY_TASK_ACKS_LATE = True
CELERY_WORKER_PREFETCH_MULTIPLIER = 1
5. Force a synchronization when access must be immediate
If agency users report failures right after a role assignment, do not wait for the queue to drain — trigger the GeoServer sync directly and reload the store:
python manage.py sync_geoserver_data --reload-store
The mechanism below traces how an agency role becomes an enforced permission — from the Django taxonomy, through guardian object permissions, into the asynchronous Celery sync that writes GeoServer ACLs.
Verification
Confirm each layer of the stack agrees on the permissions you assigned before handing the portal back to the agency.
# 1. Confirm the groups exist
python manage.py shell -c "from django.contrib.auth.models import Group; \
print(list(Group.objects.values_list('name', flat=True)))"
# 2. Inspect object-level permissions for a known resource
python manage.py shell -c "from guardian.shortcuts import get_groups_with_perms; \
from geonode.base.models import ResourceBase; \
print(get_groups_with_perms(ResourceBase.objects.get(pk=42)))"
# 3. Watch the Celery sync land without timeouts
tail -f /var/log/geonode/celery_worker.log
# 4. Confirm GeoServer accepted the ACL write (expect 200, not 401/403)
grep "RuleAdminService" geoserver.log | tail -n 20
You can also cross-check the live matrix in GeoNode’s admin at /en/admin/guardian/, where the guardian_groupobjectpermission rows should match what step 3 assigned.
Troubleshooting matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| Silent permission denials despite a correct group | Global Django group permissions overriding object-level rules, or backends out of order | Confirm ModelBackend then ObjectPermissionBackend in AUTHENTICATION_BACKENDS; cross-reference guardian_groupobjectpermission against django_content_type |
| Intermittent 403s after a bulk role update | Stale cached permission evaluations | Flush with python manage.py invalidate all (django-cacheops) or restart the app server to force cache invalidation |
| One agency sees another’s layers | Tenant scoping bypassed by legacy SQL or non-group-aware endpoints | Audit people_profile and group membership; ensure RESOURCE_PUBLISHING = True is paired with strict group ownership; never modify permissions via direct SQL (it skips Django signals) |
| GeoServer REST sync fails | Service-account mismatch or role-name casing/whitespace differences | Check geoserver.log for 401/403 during ACL propagation; verify GEOSERVER_ADMIN_USER / GEOSERVER_ADMIN_PASSWORD and exact role-name parity |
| Access granted in Django but not enforced in GeoServer | Celery task timed out or was lost | Re-run sync_geoserver_data --reload-store; set CELERY_TASK_ACKS_LATE and CELERY_WORKER_PREFETCH_MULTIPLIER = 1; inspect celery_worker.log for REST timeouts |
Treat these role definitions as infrastructure-as-code: version-control the migration and assign_perm routines alongside your deployment pipeline — the same discipline applied to Syncing GeoNode Environments with Terraform — and add the verification commands above as a CI gate so permission drift is caught before it reaches an agency user.
Related
- Implementing RBAC for Multi-Tenant GIS Portals — parent guide: request-path placement and tenant-isolation model (up one level).
- Security Boundary Mapping for OGC Services — where these role checks sit relative to WMS/WFS trust zones.
- Optimizing PostgreSQL PostGIS Connection Limits — keep the database healthy under the query load RBAC checks add.
- Syncing GeoNode Environments with Terraform — version-control role configuration across environments.
- Core Portal Architecture & Security Boundaries — the full architecture and security reference.