app/lib/ldap.py (114 lines of code) (raw):
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""OAuth+OIDC wrapper for the Apache Software Foundation - middleware plugin"""
if not __debug__:
raise RuntimeError("This code requires assert statements to be enabled")
import asfpy.clitools
import re
LDAP_PEOPLE_BASE = "ou=people,dc=apache,dc=org"
LDAP_MEMBER_BASE = "cn=member,ou=groups,dc=apache,dc=org"
LDAP_CHAIRS_BASE = "cn=pmc-chairs,ou=groups,ou=services,dc=apache,dc=org"
LDAP_PMCS_BASE = "ou=project,ou=groups,dc=apache,dc=org"
LDAP_DN = "uid=%s,ou=people,dc=apache,dc=org"
LDAP_OWNER_FILTER = "(|(ownerUid=%s)(owner=uid=%s,ou=people,dc=apache,dc=org))"
LDAP_MEMBER_FILTER = "(|(memberUid=%s)(member=uid=%s,ou=people,dc=apache,dc=org))"
LDAP_ROOT_BASE = "cn=infrastructure-root,ou=groups,ou=services,dc=apache,dc=org"
LDAP_TOOLING_BASE = "cn=tooling,ou=groups,ou=services,dc=apache,dc=org"
class OAuthException(Exception):
"""Simple exception with a message and an optional origin exception (WIP)"""
def __init__(self, message, origin=None):
super().__init__(message)
self.origin = origin
def attr_to_list(attr):
"""Converts a list of bytestring attribute values to a unique list of strings"""
return list(set([value for value in attr or []]))
class Committer:
"""Verifies and loads a committers credentials via LDAP"""
def __init__(self, user):
# Verify correct user ID syntax, construct DN
if not re.match(r"^[-_a-z0-9]+$", user):
raise OAuthException("Invalid characters in User ID. Only lower-case alphanumerics, '-' and '_' allowed.")
self.user = user
async def verify(self):
# Verify the account exists
try:
result = await asfpy.clitools.ldapsearch_cli_async(ldap_base=LDAP_DN % self.user, ldap_scope="base")
assert result and len(result) == 1, "User not found in LDAP"
except Exception as ex: # TODO: narrow the check to Exceptions that are expected
raise OAuthException("An unknown error occurred, please retry later.") from ex
# So far so good, set uid
self.uid = self.user
self.dn = LDAP_DN % self.user
# Get full name etc
try:
fn = result[0].get("cn")
assert type(fn) is list and len(fn) == 1
self.fullname = fn[0]
self.email = "%s@apache.org" % self.user
# get emails used for forwarding
self.emails = attr_to_list(result[0].get("mail"))
# get alternative emails
self.altemails = attr_to_list(result[0].get("asf-altEmail"))
# Check for asf-banned parameter, bork if set.
if result[0].get("asf-banned"):
raise OAuthException(
"This account has been administratively locked. Please contact root@apache.org for further details."
)
except AssertionError as ex:
raise OAuthException("Common backend assertions failed, LDAP corruption?") from ex
# Get membership status
try:
result = await asfpy.clitools.ldapsearch_cli_async(ldap_base=LDAP_MEMBER_BASE, ldap_scope="base")
assert len(result) == 1
members = result[0].get("memberUid")
assert type(members) is list and len(members) > 100
self.isMember = self.user in members
except AssertionError as ex:
raise OAuthException("Common backend assertions failed, LDAP corruption?") from ex
# Get chair status
try:
result = await asfpy.clitools.ldapsearch_cli_async(ldap_base=LDAP_CHAIRS_BASE, ldap_scope="base")
assert len(result) == 1
members = result[0].get("member")
assert type(members) is list and len(members) > 100
self.isChair = LDAP_DN % self.user in members
except AssertionError as ex:
raise OAuthException("Common backend assertions failed, LDAP corruption?") from ex
# Get infra-root status
try:
result = await asfpy.clitools.ldapsearch_cli_async(ldap_base=LDAP_ROOT_BASE, ldap_scope="base")
assert len(result) == 1
members = result[0].get("member")
assert type(members) is list and len(members) > 3
self.isRoot = LDAP_DN % self.user in members
except AssertionError as ex:
raise OAuthException("Common backend assertions failed, LDAP corruption?") from ex
# Get PMC memberships
try:
self.pmcs = []
result = await asfpy.clitools.ldapsearch_cli_async(
ldap_base=LDAP_PMCS_BASE,
ldap_scope="sub",
ldap_query=LDAP_OWNER_FILTER % (self.user, self.user),
ldap_attrs=["cn"],
)
for hit in result:
assert type(hit) is dict
pmc = hit.get("cn")
assert type(pmc) is list and len(pmc) == 1
pmc = pmc[0]
assert pmc and type(pmc) is str
self.pmcs.append(pmc)
except AssertionError as ex:
raise OAuthException("Common backend assertions failed, LDAP corruption?") from ex
# Get committerships
try:
self.projects = []
result = await asfpy.clitools.ldapsearch_cli_async(
ldap_base=LDAP_PMCS_BASE,
ldap_scope="sub",
ldap_query=LDAP_MEMBER_FILTER % (self.user, self.user),
ldap_attrs=["cn"],
)
for hit in result:
assert type(hit) is dict
pmc = hit.get("cn")
assert type(pmc) is list and len(pmc) == 1
pmc = pmc[0]
assert pmc and type(pmc) is str
self.projects.append(pmc)
except AssertionError as ex:
raise OAuthException("Common backend assertions failed, LDAP corruption?") from ex
# Get tooling membership
try:
result = await asfpy.clitools.ldapsearch_cli_async(ldap_base=LDAP_TOOLING_BASE, ldap_scope="base")
assert len(result) == 1
members = result[0].get("member")
assert type(members) is list and len(members) > 1
if LDAP_DN % self.user in members:
self.pmcs.append("tooling")
self.projects.append("tooling")
except AssertionError as ex:
raise OAuthException("Common backend assertions failed, LDAP corruption?") from ex
return self.__dict__