Geonode auth via LDAP

Create a demo LDAP

Basic Install Configuration

In order to test GeoNode support for LDAP authentication over the next section, we’ll create a simple local LDAP server. Follow this section if you are NOT using the Docker configuration.

# change machine hostname
sudo hostnamectl set-hostname ldap.example.com

sudo vim /etc/hosts
127.0.0.1 localhost ldap.example.com
sudo apt -y install slapd ldap-utils

# NOTE: You will be prompted to provide slapd an admin account password
# check if slapd install is ok
sudo slapcat

# You should receive a similar content response

> dn: dc=nodomain
  objectClass: top
  objectClass: dcObject
  objectClass: organization
  o: nodomain
  dc: nodomain
  structuralObjectClass: organization
  entryUUID: 8ec4b66c-5213-103d-95ee-63469b16e99e
  creatorsName: cn=admin,dc=nodomain
  createTimestamp: 20230308154220Z
  entryCSN: 20230308154220.746237Z#000000#000#000000
  modifiersName: cn=admin,dc=nodomain
  modifyTimestamp: 20230308154220Z

Let’s update the domain name and admin password

sudo dpkg-reconfigure slapd
  • When prompted for the domain and organization name provide: example.com

  • When prompted for an admin password provide: geonode

# Create base geonode users ldap file
mkdir -p /opt/geonode/ldap
cd /opt/geonode/ldap
vim geonodedn.ldif
# Create the following content and save the file afterward
dn: ou=users,dc=example,dc=com
objectClass: organizationalUnit
ou: users

dn: ou=groups,dc=example,dc=com
objectClass: organizationalUnit
ou: groups
# add an admin user to our ldap (use the ldap password from the install step)
sudo ldapadd -x -D cn=admin,dc=example,dc=com -W -f geonodedn.ldif
# generate a password for the user account to add
slappasswd

# password: geondoe
New password:
Re-enter new password:
{SSHA}+bLmAxCk1B6UUgSsWUh25pTteh+reQUL
# create an ldif file for geonode users
vim ldapusers.ldif
# add a similar content
# replace geonode with your username
dn: uid=geonode,ou=users,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
cn: geonode
sn: Wiz
userPassword: {SSHA}+bLmAxCk1B6UUgSsWUh25pTteh+reQUL
loginShell: /bin/bash
uidNumber: 2000
gidNumber: 2000
homeDirectory: /home/geonode
# create the ldap user
sudo ldapadd -x -D cn=admin,dc=example,dc=com -W -f ldapusers.ldif
# create an ldap group
vim ldapgroups.ldif
# add the following content
dn: cn=geonode,ou=groups,dc=example,dc=com
objectClass: posixGroup
cn: geonode
gidNumber: 2000
memberUid: uid=geonode,ou=users,dc=example,dc=com
# create the geonode ldap group
ldapadd -x -D cn=admin,dc=example,dc=com -W -f ldapgroups.ldif
sudo slapcat
dn: dc=example,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o: example.com
dc: example
structuralObjectClass: organization
entryUUID: 28470290-5214-103d-8539-1b685db57c30
creatorsName: cn=admin,dc=example,dc=com
createTimestamp: 20230308154638Z
entryCSN: 20230308154638.291561Z#000000#000#000000
modifiersName: cn=admin,dc=example,dc=com
modifyTimestamp: 20230308154638Z

dn: ou=users,dc=example,dc=com
objectClass: organizationalUnit
ou: users
structuralObjectClass: organizationalUnit
entryUUID: 77fc71b2-5214-103d-886d-73d2df8adfb0
creatorsName: cn=admin,dc=example,dc=com
createTimestamp: 20230308154852Z
entryCSN: 20230308154852.020628Z#000000#000#000000
modifiersName: cn=admin,dc=example,dc=com
modifyTimestamp: 20230308154852Z

dn: ou=groups,dc=example,dc=com
objectClass: organizationalUnit
ou: groups
structuralObjectClass: organizationalUnit
entryUUID: 77fd6d7e-5214-103d-886e-73d2df8adfb0
creatorsName: cn=admin,dc=example,dc=com
createTimestamp: 20230308154852Z
entryCSN: 20230308154852.027088Z#000000#000#000000
modifiersName: cn=admin,dc=example,dc=com
modifyTimestamp: 20230308154852Z

dn: uid=geonode,ou=users,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
cn: geonode
sn: Wiz
userPassword:: e1NTSEF9K2JMbUF4Q2sxQjZVVWdTc1dVaDI1cFR0ZWgrcmVRVUw=
loginShell: /bin/bash
uidNumber: 2000
gidNumber: 2000
homeDirectory: /home/geonode
structuralObjectClass: inetOrgPerson
uid: geonode
entryUUID: dad57432-5214-103d-886f-73d2df8adfb0
creatorsName: cn=admin,dc=example,dc=com
createTimestamp: 20230308155137Z
entryCSN: 20230308155137.859553Z#000000#000#000000
modifiersName: cn=admin,dc=example,dc=com
modifyTimestamp: 20230308155137Z

dn: cn=geonode,ou=groups,dc=example,dc=com
objectClass: posixGroup
cn: geonode
gidNumber: 2000
memberUid: uid=geonode,ou=users,dc=example,dc=com
structuralObjectClass: posixGroup
entryUUID: eb3a480c-5214-103d-8870-73d2df8adfb0
creatorsName: cn=admin,dc=example,dc=com
createTimestamp: 20230308155205Z
entryCSN: 20230308155205.363882Z#000000#000#000000
modifiersName: cn=admin,dc=example,dc=com
modifyTimestamp: 20230308155205Z

Django Auth-LDAP

This package provides utilities for using LDAP as an authentication and authorization backend for geonode.

The django_auth_ldap package is a very capable way to add LDAP integration with django projects. It provides a lot of flexibility in mapping LDAP users to geonode users and is able to manage user authentication.

However, in order to provide full support for mapping LDAP groups with geonode’s and enforce group permissions on resources, a custom geonode authentication backend is required. This contrib package provides such a backend, based on django_auth_ldap.

Installation

Installing this contrib package is a matter of:

  1. Installing system LDAP libraries (development packages needed)

  2. Cloning this repository locally

  3. Change to the ldap directory and install this contrib package

# install systemwide LDAP libraries
sudo apt install libldap2-dev libsasl2-dev

# get geonode/contribs code
cd /opt
sudo git clone https://github.com/GeoNode/geonode-contribs.git
sudo chown -Rf $USER: geonode-contribs

# optional: if our virtualenv is deactivated, activate it again
workon geonode

# install geonode ldap contrib package
cd geonode-contribs/ldap
pip install .

Configuration

  1. Add geonode_ldap.backend.GeonodeLdapBackend as an additional auth backend.

    # e.g. by updating your settings.py or local_settings.py
    AUTHENTICATION_BACKENDS += (
        "geonode_ldap.backend.GeonodeLdapBackend",
    )
    

    # open settings.py or local_settings.py and locate AUTHENTICATION_BACKENDS # add the new backend line as above

    cd /opt/geonode/geonode
    vim settings.py
    

    You may use additional auth backends, the django authentication framework tries them all according to the order listed in the settings. This means that geonode can be set up in such a way that it permits internal organization users to log in with their LDAP credentials, while at the same time allowing for casual users to use their Facebook login (as long as you enable Facebook social auth provider).

    Note

    The django’s django.contrib.auth.backends.ModelBackend must also be used in order to provide full geonode integration with LDAP. However, by default, this is included on GeoNode settings

    # The GeoNode default settings are the following
    AUTHENTICATION_BACKENDS = (
        'oauth2_provider.backends.OAuth2Backend',
        'django.contrib.auth.backends.ModelBackend',
        'guardian.backends.ObjectPermissionBackend',
        'allauth.account.auth_backends.AuthenticationBackend',
    )
    
  2. Set some additional configuration values. Some of these variables are prefixed with AUTH_LDAP (these are used directly by django_auth_ldap) while others are prefixed with GEONODE_LDAP (these are used by geonode_ldap). The geonode custom variables are:

    • GEONODE_LDAP_GROUP_PROFILE_FILTERSTR - This is an LDAP search fragment with the filter that allows querying for existing groups. See the example below

    • GEONODE_LDAP_GROUP_NAME_ATTRIBUTE - This is the name of the LDAP attribute that will be used for deriving the geonode group name. If not specified, it will default to cn, which means that the LDAP object’s common name will be used for generating the name of the geonode group

    • GEONODE_LDAP_GROUP_PROFILE_MEMBER_ATTR - This is the name of the LDAP attribute that will be used for deriving the geonode membership. If not specified it will default to member

Example configuration:

  • Add the following lines at the end of /opt/geonode/geonode/settings.py

# LDAP Configuration
from django_auth_ldap import config as ldap_config
from geonode_ldap.config import GeonodeNestedGroupOfNamesType
import ldap

AUTHENTICATION_BACKENDS += (
    'geonode_ldap.backend.GeonodeLdapBackend',
)

INSTALLED_APPS += ('geonode_ldap',)

# django_auth_ldap configuration
AUTH_LDAP_SERVER_URI = os.getenv("LDAP_SERVER_URL")
AUTH_LDAP_BASE_DN = os.getenv("LDAP_BASE_DN")
AUTH_LDAP_BIND_DN = os.getenv("LDAP_BIND_DN")
AUTH_LDAP_BIND_PASSWORD = os.getenv("LDAP_BIND_PASSWORD")
AUTH_LDAP_USER_SEARCH = ldap_config.LDAPSearch(
    os.getenv("LDAP_USER_SEARCH_DN"),
    ldap.SCOPE_SUBTREE,
    os.getenv("LDAP_USER_SEARCH_FILTERSTR")
)
# should LDAP groups be used to spawn groups in GeoNode?
AUTH_LDAP_MIRROR_GROUPS = strtobool(os.getenv("LDAP_MIRROR_GROUPS", 'True'))
AUTH_LDAP_GROUP_SEARCH = ldap_config.LDAPSearch(
    os.getenv("LDAP_GROUP_SEARCH_DN"),
    ldap.SCOPE_SUBTREE,
    os.getenv("LDAP_GROUP_SEARCH_FILTERSTR")
)
AUTH_LDAP_GROUP_TYPE = GeonodeNestedGroupOfNamesType()
AUTH_LDAP_USER_ATTR_MAP = {
    "first_name": "givenName",
    "last_name": "sn",
    "email": "mailPrimaryAddress"
}
AUTH_LDAP_FIND_GROUP_PERMS = True
AUTH_LDAP_MIRROR_GROUPS_EXCEPT = [
    "test_group"
]

# these are not needed by django_auth_ldap - we use them to find and match
# GroupProfiles and GroupCategories
GEONODE_LDAP_GROUP_NAME_ATTRIBUTE = os.getenv("LDAP_GROUP_NAME_ATTRIBUTE", default="cn")
GEONODE_LDAP_GROUP_PROFILE_FILTERSTR = os.getenv("LDAP_GROUP_PROFILE_FILTERSTR", default="(ou=research group)")
GEONODE_LDAP_GROUP_PROFILE_MEMBER_ATTR = os.getenv("LDAP_GROUP_PROFILE_MEMBER_ATTR", default="memberUid")

Example environment variables:

  • Add these to the /opt/geonode/geonode/.env_local

LDAP_SERVER_URL=ldap://localhost:389
LDAP_BASE_DN=dc=example,dc=com
LDAP_BIND_DN=cn=admin,dc=example,dc=com
LDAP_BIND_PASSWORD=geonode

LDAP_USER_SEARCH_DN=ou=users,dc=example,dc=com
LDAP_USER_SEARCH_FILTERSTR=(uid=%(user)s)

LDAP_GROUP_SEARCH_DN=ou=groups,dc=example,dc=com
LDAP_GROUP_NAME_ATTRIBUTE=cn
LDAP_GROUP_PROFILE_FILTERSTR=(objectClass=posixGroup)
LDAP_GROUP_PROFILE_MEMBER_ATTR=memberUid

The configuration seen in the example above will allow LDAP users to log in to geonode with their LDAP credentials.

On the first login, a geonode user is created from the LDAP user, and its LDAP attributes cn and sn are used to populate the geonode user’s first_name and last_name profile fields.

Any groups that the user is a member of in LDAP (under the cn=groups,dc=ad,dc=example,dc=com search base and belonging to one of (|(cn=abt1)(cn=abt2)(cn=abt3)(cn=abt4)(cn=abt5)(cn=abt6)) groups) will be mapped to the corresponding geonode groups, even creating these groups in geonode in case they do not yet exist. The geonode user is also made a member of these geonode groups.

Upon each login, the user’s geonode group memberships are re-evaluated according to the information extracted from LDAP. The AUTH_LDAP_MIRROR_GROUPS_EXCEPT setting can be used to specify groups whose memberships will not be re-evaluated.

If no LDAP groups shall be mirrored LDAP_MIRROR_GROUPS and LDAP_MIRROR_GROUPS_EXCEPT must be set to False.

Note

Users mapped from LDAP will be marked with an ldap tag. This will be used to keep them in sync.

Warning

If you remove the ldap tag, the users will be treated as pure internal GeoNode users.

You may also manually generate the geonode groups in advance, before users log in. In this case, when a user logs in and the mapped LDAP group already exists, the user is merely added to the geonode group

Be sure to check out django_auth_ldap for more information on the various configuration options.

Docker Install Configuration

If you are using the Docker-based installation please follow the next section. By default, the docker image we’ll be using is shipped with administrative credentials as:

Admin DN: cn=admin,dc=example,dc=com
password: admin

Create a new service entry on the main docker-compose.yml file.

## edit compose file
vim docker-compose.yml

## add a new LDAP service to compose stack
...
services:
  ldap:
    image: osixia/openldap:1.5.0
    restart: on-failure
    container_name: ldap4${COMPOSE_PROJECT_NAME}
    ports:
      - "389:389"
      - "636:636"

## run container
docker-compose up -d ldap

Once the container is up and running, let’s connect to it to configure our LDAP server.

## connect to Docker LDAP container
docker exec -ti ldap4my_geonode bash

## create OS user
useradd -b /home/geonode -s /sbin/nologin -u 5159 -U geonode

## add Geonode LDAP group
vim geonode_group.ldif

## paste the following content on geonode_group.ldif file and save it
dn: o=geonode,dc=example,dc=com
objectClass: organization
o: geonode

## add groupd to LDAP server
ldapadd -x -W -D "cn=admin,dc=example,dc=com" -f geonode_group.ldif
## You are asked to provide the administrative password `admin`

## add Geonode organizational unit
vim geonodeOU.ldif

## paste the following content on geonodeOU.ldif file and save it
dn: ou=geonodeOU,o=geonode,dc=example,dc=com
objectClass: organizationalUnit
ou: geonodeOU

## add organization unit
ldapadd -x -W -D "cn=admin,dc=example,dc=com" -f geonodeOU.ldif
## Again apply here the administrative password

## add LDAP user
vim testuser.ldif

## paste the following content on testuser.ldif file and save it
dn: uid=geonode,ou=geonodeOU,o=geonode,dc=example,dc=com
cn: geonode
givenName: User
sn: Geonode
uid: geonode
uidNumber: 5159
gidNumber: 5159
homeDirectory: /home/geonode
loginShell: /bin/bash
mail: geonode@example.com
objectClass: top
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
userPassword: {SSHA}x

## add user to LDAP server
ldapadd -cxWD cn=admin,dc=example,dc=com -f testuser.ldif
## Again apply here the administrative password

## set user LDAP password
ldappasswd -xWD "cn=admin,dc=example,dc=com" -S "uid=geonode,ou=geonodeOU,o=geonode,dc=example,dc=com"
## Set a password here for your new LDAP user. Last step apply the administrative password again

## close your connection to the container

Lastly change your main Django container .env and locate the LDAP configuration section. Change it in accordance with the following content:

vim .env

## Change .env file LDAP section
LDAP_SERVER_URL=ldap://ldap:389
LDAP_BIND_DN="cn=admin,dc=example,dc=com"
LDAP_BIND_PASSWORD=geonode
LDAP_USER_SEARCH_DN="o=geonode,dc=example,dc=com"
LDAP_USER_SEARCH_FILTERSTR="(cn=%(user)s)"
LDAP_GROUP_SEARCH_DN="ou=geonodeOU,o=geonode,dc=example,dc=com"
LDAP_GROUP_SEARCH_FILTERSTR="(cn=%(user)s)"

We now need to recreate the main Django container to apply the new configurations.

docker-compose up -d --force-recreate django

Synchronize Users and Groups on the Docker running container

Connect to the running django container and run the manage.py utility updateldapusers to synchronize our LDAP users with GeoNode

## connect to Docker Django container
docker exec -ti django4my_geonode bash

## sync LDAP users
python manage.py updateldapusers

Keep Users and Groups Synchronized

In order to constantly keep the remote LDAP Users and Groups synchronized with GeoNode, you will need to periodically run some specific management commands.

*/10 * * * * /opt/geonode/my-geonode/manage.sh updateldapgroups  >> /var/log/cron.log 2>&1
*/10 * * * * /opt/geonode/my-geonode/manage.sh updateldapusers   >> /var/log/cron.log 2>&1

Where the manage.sh is a bash script similar to the following one:

manage.sh

export $(grep -v '^#' /opt/geonode/my-geonode/.env | xargs -d '\n'); /home/<my_user>/.virtualenvs/geonode/bin/python /opt/geonode/my-geonode/manage.py $@

And the /opt/geonode/my-geonode/.env is something similar to the following one:

/opt/geonode/my-geonode/.env

DEBUG=False
DJANGO_ALLOWED_HOSTS=<geonode_public_host>,localhost,127.0.0.1
DJANGO_DATABASE_URL=postgis://my_geonode:**********@localhost:5432/my_geonode_db
DEFAULT_BACKEND_UPLOADER=geonode.importer
DEFAULT_FROM_EMAIL=geonode@example.com
DJANGO_EMAIL_HOST=smtp.example.com
DJANGO_EMAIL_HOST_PASSWORD=**********
DJANGO_EMAIL_HOST_USER=geonode
DJANGO_EMAIL_PORT=465
DJANGO_EMAIL_USE_SSL=True
DJANGO_SETTINGS_MODULE=my_geonode.settings
DJANGO_SECRET_KEY=**********
OAUTH2_API_KEY=**********
PROXY_URL=/proxy/?url=
EXIF_ENABLED=True
EMAIL_ENABLE=True
TIME_ENABLED=True
ACCOUNT_OPEN_SIGNUP=True
ACCOUNT_APPROVAL_REQUIRED=True
ACCOUNT_EMAIL_REQUIRED=True
ACCOUNT_EMAIL_VERIFICATION=optional
AVATAR_GRAVATAR_SSL=True
GEONODE_DB_URL=postgis://my_geonode:**********@localhost:5432/my_geonode_data
GEOSERVER_ADMIN_PASSWORD=**********
GEOSERVER_LOCATION=https://<geonode_public_host>/geoserver/
GEOSERVER_PUBLIC_HOST=<geonode_public_host>
GEOSERVER_PUBLIC_LOCATION=https://<geonode_public_host>/geoserver/
GEOSERVER_WEB_UI_LOCATION=https://<geonode_public_host>/geoserver/
LDAP_SERVER_URL=ldap://<the_ldap_server>
LDAP_BIND_DN=uid=ldapinfo,cn=users,dc=ad,dc=example,dc=com
LDAP_BIND_PASSWORD=<something_secret>
LDAP_USER_SEARCH_DN=dc=ad,dc=example,dc=com
LDAP_USER_SEARCH_FILTERSTR=(&(uid=%(user)s)(objectClass=person))
LDAP_MIRROR_GROUPS=True
LDAP_GROUP_SEARCH_DN=cn=groups,dc=ad,dc=example,dc=com
LDAP_GROUP_SEARCH_FILTERSTR=(|(cn=abt1)(cn=abt2)(cn=abt3)(cn=abt4)(cn=abt5)(cn=abt6))
LDAP_GROUP_PROFILE_MEMBER_ATTR=uniqueMember
OGC_REQUEST_MAX_RETRIES=3
OGC_REQUEST_POOL_CONNECTIONS=100
OGC_REQUEST_POOL_MAXSIZE=100
OGC_REQUEST_TIMEOUT=60
SITEURL=https://<geonode_public_host>/
SITE_HOST_NAME=<geonode_public_host>
FREETEXT_KEYWORDS_READONLY=False
# Advanced Workflow Settings
ADMIN_MODERATE_UPLOADS=False
GROUP_MANDATORY_RESOURCES=False
GROUP_PRIVATE_RESOURCES=False
RESOURCE_PUBLISHING=False

Note

You might want to use the same /opt/geonode/my-geonode/.env for your UWSGI configuration too:

[uwsgi]
socket = 0.0.0.0:8000
uid = <my_user>
gid = www-data

plugins = python3
virtualenv = /home/<my_user>/.virtualenvs/geonode

# set environment variables from .env file
env LANG=en_US.utf8
env LC_ALL=en_US.UTF-8
env LC_LANG=en_US.UTF-8

for-readline = /opt/geonode/my-geonode/.env
    env = %(_)
endfor =

chdir = /opt/geonode/my-geonode
module = my_geonode.wsgi:application

processes = 12
threads = 2
enable-threads = true
master = true

# logging
# path to where uwsgi logs will be saved
logto = /storage/my_geonode/logs/geonode.log
daemonize = /storage/my_geonode/logs/geonode.log
touch-reload = /opt/geonode/my-geonode/my_geonode/wsgi.py
buffer-size = 32768
max-requests = 500
harakiri = 300 # respawn processes taking more than 5 minutes (300 seconds)
# limit-as = 1024 # avoid Errno 12 cannot allocate memory
harakiri-verbose = true
vacuum = true
thunder-lock = true