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-guardian installed (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.py access to confirm GEONODE_SECURITY_BACKEND, AUTHENTICATION_BACKENDS, RESOURCE_PUBLISHING, and GROUPS_MANAGERS_GROUP_NAME.
  • Environment variables GEOSERVER_ADMIN_USER and GEOSERVER_ADMIN_PASSWORD set and matching the GeoServer service account.
  • Superuser (or manage.py shell) access to run the provisioning steps idempotently across environment promotions.
  • Read access to the log paths /var/log/geonode/celery_worker.log and geoserver.log for 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.

From Agency Role to Enforced GeoServer Permission Four agency roles form a taxonomy: AgencyAdmin holds view, change and delete; DataSteward holds change and download; Analyst holds view and download; Viewer holds view only. All four map into Django and django-guardian object-level permissions on resourcebase objects. A Celery sync task propagates those permissions into GeoServer ACLs. When that task times out or is lost it is redelivered with acks-late semantics and recorded in celery_worker.log, so access granted in Django is eventually enforced in GeoServer. AGENCY ROLE TAXONOMY AgencyAdmin view / change / delete DataSteward change / download Analyst view / download Viewer view only Django + django-guardian object-level permissions assign_perm on resourcebase Celery sync task async, acks-late GeoServer ACLs timeout / retry celery_worker.log

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.