[GH-ISSUE #299] Self-Hosted: Assign Projects to LDAP groups automatically #226

Closed
opened 2026-02-25 23:41:40 +03:00 by kerem · 1 comment
Owner

Originally created by @gganeshan on GitHub (Nov 4, 2019).
Original GitHub issue: https://github.com/healthchecks/healthchecks/issues/299

@cuu508 I am self hosting this app and have integrated it with my LDAP.

I wanted to know if there is a way to assign Projects automatically based on LDAP Groups.

For e.g., I can have a Project for a specific BU and would want anyone from that BU to be able to manage checks in that project automatically. Is there a way to do that??

If not, would you consider adding it as a feature for LDAP integrated implementations??

Originally created by @gganeshan on GitHub (Nov 4, 2019). Original GitHub issue: https://github.com/healthchecks/healthchecks/issues/299 @cuu508 I am self hosting this app and have integrated it with my LDAP. I wanted to know if there is a way to assign Projects automatically based on LDAP Groups. For e.g., I can have a Project for a specific BU and would want anyone from that BU to be able to manage checks in that project automatically. Is there a way to do that?? If not, would you consider adding it as a feature for LDAP integrated implementations??
kerem closed this issue 2026-02-25 23:41:41 +03:00
Author
Owner

@gganeshan commented on GitHub (Nov 19, 2019):

fyi - I used the following hack to achieve yaml based auto-provisioning and member assignment.

provision.yaml

---
# project name:
#   - members:
#     - List of DL or email address or All (gives acces to all users)
#     - if list provided no users get access
#   - badge_key: if no value provided, project name is used as badge key
#   - api_key: if no value provided, project name is used as api_key
ProjectA:
  - badge_key: "KeyA"
  - api_key: "KeyB"
  - members:
      - "*"

local_settings.py

import ldap
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType


# Baseline configuration.
AUTH_LDAP_SERVER_URI = 'ldap://ldap.example.com'

AUTH_LDAP_BIND_DN = 'cn=django-agent,dc=example,dc=com'
AUTH_LDAP_BIND_PASSWORD = 'password'
AUTH_LDAP_USER_SEARCH = LDAPSearch(
    'ou=users,dc=example,dc=com',
    ldap.SCOPE_SUBTREE,
    '(mail=%(user)s)',
)

# Set up the basic group parameters.
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    'ou=django,ou=groups,dc=example,dc=com',
    ldap.SCOPE_SUBTREE,
    '(objectClass=top)',
)
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr='cn')

# Populate the Django user from the LDAP directory.
AUTH_LDAP_USER_ATTR_MAP = {
    'first_name': 'givenName',
    'last_name': 'sn',
    'email': 'mail',
}

AUTH_LDAP_MIRROR_GROUPS = True

# Cache distinguised names and group memberships for an hour to minimize
# LDAP traffic.
AUTH_LDAP_CACHE_TIMEOUT = 3600

# Keep ModelBackend around for per-user permissions and maybe a local
# superuser.
AUTHENTICATION_BACKENDS = (
    'django_auth_ldap.backend.LDAPBackend',
    'django.contrib.auth.backends.ModelBackend',
)

MIDDLEWARE = (
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "hc.accounts.middleware.TeamAccessMiddleware",
    "hc.accounts.memberassignment.MemberAssignmentMiddleware",
)

hc/accounts/memberassignment.py

import yaml
from hc.accounts.models import Member, Project

class MemberAssignmentMiddleware(object):
    def __init__(self, get_response):
        # One-time configuration and initialization.
        self.get_response = get_response

    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        if not request.user.is_authenticated:
            return self.get_response(request)

        config_file = "provision.yaml"
        for project_name, project_attrs in yaml.load(open(config_file), Loader=yaml.FullLoader).items():
            project = get_project(project_name)
            project_members = get_project_attr_from_file(project_attrs, "members")
            if is_member_assignment_needed(project, request.user, project_members):
                member_assignment(request.user, project)

        # Code to be executed for each request/response after
        # the view is called.
        return self.get_response(request)

def is_member_assignment_needed(project, user, members):
    any_rules = ["All" in members,
                 "*" in members,
                 user.email in members,
                 True in [True for member in members if user.groups.filter(name=member).exists()],
                ]
    all_rules = ["None" not in members,
                 not Member.objects.filter(project=project, user=user),
                 members,
                ]
    if all(all_rules) and any(any_rules):
        return True
    return False

def member_assignment(member, project):
    Member.objects.create(user=member, project=project)

def get_project(project_name):
    return Project.objects.get(name=project_name)

def get_project_attr_from_file(project_attrs, key):
    for project_attr in project_attrs:
        if key in project_attr.keys():
            attr = project_attr[key]
    if attr is None:
        attr = ""
    return attr

Since this is a middleware, it intercepts all requests and performs the relevant tasks.
I need to optimize it further to be executed only once at login.
But for now this will do.

@cuu508 If you have any ideas on how to improve this further please let me know.

<!-- gh-comment-id:555536769 --> @gganeshan commented on GitHub (Nov 19, 2019): fyi - I used the following hack to achieve yaml based auto-provisioning and member assignment. `provision.yaml` ```yaml --- # project name: # - members: # - List of DL or email address or All (gives acces to all users) # - if list provided no users get access # - badge_key: if no value provided, project name is used as badge key # - api_key: if no value provided, project name is used as api_key ProjectA: - badge_key: "KeyA" - api_key: "KeyB" - members: - "*" ``` `local_settings.py` ```python import ldap from django_auth_ldap.config import LDAPSearch, GroupOfNamesType # Baseline configuration. AUTH_LDAP_SERVER_URI = 'ldap://ldap.example.com' AUTH_LDAP_BIND_DN = 'cn=django-agent,dc=example,dc=com' AUTH_LDAP_BIND_PASSWORD = 'password' AUTH_LDAP_USER_SEARCH = LDAPSearch( 'ou=users,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(mail=%(user)s)', ) # Set up the basic group parameters. AUTH_LDAP_GROUP_SEARCH = LDAPSearch( 'ou=django,ou=groups,dc=example,dc=com', ldap.SCOPE_SUBTREE, '(objectClass=top)', ) AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr='cn') # Populate the Django user from the LDAP directory. AUTH_LDAP_USER_ATTR_MAP = { 'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail', } AUTH_LDAP_MIRROR_GROUPS = True # Cache distinguised names and group memberships for an hour to minimize # LDAP traffic. AUTH_LDAP_CACHE_TIMEOUT = 3600 # Keep ModelBackend around for per-user permissions and maybe a local # superuser. AUTHENTICATION_BACKENDS = ( 'django_auth_ldap.backend.LDAPBackend', 'django.contrib.auth.backends.ModelBackend', ) MIDDLEWARE = ( "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "hc.accounts.middleware.TeamAccessMiddleware", "hc.accounts.memberassignment.MemberAssignmentMiddleware", ) ``` `hc/accounts/memberassignment.py` ```python import yaml from hc.accounts.models import Member, Project class MemberAssignmentMiddleware(object): def __init__(self, get_response): # One-time configuration and initialization. self.get_response = get_response def __call__(self, request): # Code to be executed for each request before # the view (and later middleware) are called. if not request.user.is_authenticated: return self.get_response(request) config_file = "provision.yaml" for project_name, project_attrs in yaml.load(open(config_file), Loader=yaml.FullLoader).items(): project = get_project(project_name) project_members = get_project_attr_from_file(project_attrs, "members") if is_member_assignment_needed(project, request.user, project_members): member_assignment(request.user, project) # Code to be executed for each request/response after # the view is called. return self.get_response(request) def is_member_assignment_needed(project, user, members): any_rules = ["All" in members, "*" in members, user.email in members, True in [True for member in members if user.groups.filter(name=member).exists()], ] all_rules = ["None" not in members, not Member.objects.filter(project=project, user=user), members, ] if all(all_rules) and any(any_rules): return True return False def member_assignment(member, project): Member.objects.create(user=member, project=project) def get_project(project_name): return Project.objects.get(name=project_name) def get_project_attr_from_file(project_attrs, key): for project_attr in project_attrs: if key in project_attr.keys(): attr = project_attr[key] if attr is None: attr = "" return attr ``` Since this is a middleware, it intercepts all requests and performs the relevant tasks. I need to optimize it further to be executed only once at login. But for now this will do. @cuu508 If you have any ideas on how to improve this further please let me know.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/healthchecks#226
No description provided.