From 5872cfaee191b55c60e9bf1682c1c759a067130a Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@jrother.eu>
Date: Sun, 9 Jan 2022 18:32:21 +0100
Subject: [PATCH] User BIND permission and rate limit error handling

API permissions errors on BIND (i.e. lack of scope "checkpassword") are now
reported as insufficientAccessRights (50) and rate limit errors as
unwillingToPerform (53). Previously other (80) was used in both cases, which
confused some clients (Nextcloud to be precise).
---
 uffd-ldapd | 27 ++++++++++++++++++++-------
 1 file changed, 20 insertions(+), 7 deletions(-)

diff --git a/uffd-ldapd b/uffd-ldapd
index 587c449..0b64cf4 100755
--- a/uffd-ldapd
+++ b/uffd-ldapd
@@ -11,7 +11,7 @@ from cachecontrol import CacheControl
 from cachecontrol.heuristics import ExpiresAfter
 
 import ldapserver
-from ldapserver.exceptions import LDAPInvalidCredentials
+from ldapserver.exceptions import LDAPInvalidCredentials, LDAPInsufficientAccessRights, LDAPUnwillingToPerform
 from ldapserver.schema import RFC2307BIS_SCHEMA, RFC2798_SCHEMA
 
 logger = logging.getLogger(__name__)
@@ -96,8 +96,15 @@ class UffdLDAPRequestHandler(ldapserver.LDAPRequestHandler):
 			return True
 		if not dn.is_direct_child_of(self.subschema.DN('ou=users') + self.dn_base) or len(dn[0]) != 1 or dn[0][0].attribute != 'uid':
 			raise LDAPInvalidCredentials()
-		if self.api.check_password(loginname=dn[0][0].value, password=password):
-			return True
+		try:
+			if self.api.check_password(loginname=dn[0][0].value, password=password):
+				return True
+		except requests.exceptions.HTTPError as exc:
+			if exc.response.status_code == 403: # We don't have "checkpassword" scope
+				raise LDAPInsufficientAccessRights() from exc
+			if exc.response.status_code == 429: # Ratelimited
+				raise LDAPUnwillingToPerform('Too Many Requests') from exc
+			raise exc
 		raise LDAPInvalidCredentials()
 
 	supports_sasl_plain = True
@@ -105,10 +112,16 @@ class UffdLDAPRequestHandler(ldapserver.LDAPRequestHandler):
 	def do_bind_sasl_plain(self, identity, password, authzid=None):
 		if authzid is not None and identity != authzid:
 			raise LDAPInvalidCredentials()
-		user = self.api.check_password(loginname=identity, password=password)
-		if user is None:
-			raise LDAPInvalidCredentials()
-		return user
+		try:
+			if self.api.check_password(loginname=identity, password=password):
+				return True
+		except requests.exceptions.HTTPError as exc:
+			if exc.response.status_code == 403: # We don't have "checkpassword" scope
+				raise LDAPInsufficientAccessRights() from exc
+			if exc.response.status_code == 429: # Ratelimited
+				raise LDAPUnwillingToPerform('Too Many Requests') from exc
+			raise exc
+		raise LDAPInvalidCredentials()
 
 	def do_search(self, baseobj, scope, filterobj):
 		yield from super().do_search(baseobj, scope, filterobj)
-- 
GitLab