Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more LDAP API endpoints #193

Merged
merged 2 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions teknologr/api/bill.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,10 @@ def __request(self, path):
if r.status_code != 200:
raise BILLException(f"BILL returned status code {r.status_code}")

number = 0
# Not a number, return as text
try:
number = int(r.text)
except ValueError:
# Not a number, return as text
return r.text

# A negative number means an error code
Expand All @@ -55,38 +54,39 @@ def create_bill_account(self, username):
raise BILLException(f"BILL returned error: {result}")

def delete_bill_account(self, bill_code):
info = self.get_bill_info(bill_code)
error = info.get('error')
if error:
raise BILLException(error)
info = self.get_account_by_code(bill_code)

# If the BILL account does not exist all is ok
if info.get('exists') is False:
if not info:
return

result = self.__request(f"del?type=user&acc={bill_code}")

if result != 0:
raise BILLException(f"BILL returned error: {result}")

def find_bill_code(self, username):
result = self.__request(f"get?type=user&id={username}")
return json.loads(result)["acc"]
def get_account_by_username(self, username):
'''
Get the info for a certain BILL account. Returns None if the account does not exist.
'''
try:
result = self.__request(f"get?type=user&id={username}")
return json.loads(result)
except BILLException as e:
s = str(e)
if s == BILLAccountManager.ERROR_ACCOUNT_DOES_NOT_EXIST:
return None
raise e

def get_bill_info(self, bill_code):
def get_account_by_code(self, bill_code):
'''
Get the info for a certain BILL account. Never throws.
Get the info for a certain BILL account. Returns None if the account does not exist.
'''
if not bill_code:
return {'acc': None, 'exists': False}
try:
result = self.__request(f"get?type=user&acc={bill_code}")
return {
**json.loads(result),
'exists': True,
}
return json.loads(result)
except BILLException as e:
s = str(e)
if s == BILLAccountManager.ERROR_ACCOUNT_DOES_NOT_EXIST:
return {'acc': bill_code, 'exists': False}
return {'acc': bill_code, 'error': s}
return None
raise e
83 changes: 64 additions & 19 deletions teknologr/api/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,6 @@ def LDAPError_to_string(e):
s += f' ({info})'
return s

def get_ldap_account(username):
'''
Get the info for a certain LDAP account. Never throws.
'''
if not username:
return {'username': None, 'exists': False, 'groups': []}
try:
with LDAPAccountManager() as lm:
exists = lm.check_account(username)
groups = lm.get_ldap_groups(username)
return {'username': username, 'exists': exists, 'groups': sorted(groups)}
except ldap.LDAPError as e:
return {'username': username, 'error': LDAPError_to_string(e)}

class LDAPAccountManager:
def __init__(self):
# Don't require certificates
Expand Down Expand Up @@ -173,8 +159,67 @@ def hash_user_password(self, password):
digest = hashlib.md5((password + salt).encode('utf-8')).hexdigest()
return b"{SMD5}" + base64.b64encode(binascii.unhexlify(digest) + salt.encode('utf-8'))

def get_ldap_groups(self, username):
dn = env("LDAP_GROUP_DN")
query = "(&(objectClass=posixGroup)(memberUid=%s))" % username
output = self.ldap.search_s(dn, ldap.SCOPE_SUBTREE, query, ['cn', ])
return [group[1]['cn'][0].decode('utf-8') for group in output]
def __get_key(self, d, key, default=None):
value = d.get(key)
if value is None:
return default
return value[0].decode('utf-8')

def get_user_list(self):
result = self.ldap.search_s(
env('LDAP_USER_DN'),
ldap.SCOPE_ONELEVEL,
attrlist=['uid'])
return sorted([self.__get_key(user[1], 'uid') for user in result])

def get_user_details(self, username):
result = self.ldap.search_s(
env('LDAP_USER_DN'),
ldap.SCOPE_ONELEVEL,
f'(uid={username})',
['uidNumber', 'givenName', 'sn', 'mail'])
if not result:
return None
user = result[0][1]
id = self.__get_key(user, 'uidNumber', '')
return {
'username': username,
'id': int(id) if id.isdecimal() else None,
'given_name': self.__get_key(user, 'givenName'),
'surname': self.__get_key(user, 'sn'),
'email': self.__get_key(user, 'mail'),
'groups': self.get_user_groups(username),
}

def get_user_groups(self, username):
result = self.ldap.search_s(
env('LDAP_GROUP_DN'),
ldap.SCOPE_SUBTREE,
f'(&(objectClass=posixGroup)(memberUid={username}))',
['cn'])
return sorted([self.__get_key(group[1], 'cn') for group in result])

def get_group_list(self):
result = self.ldap.search_s(
env('LDAP_GROUP_DN'),
ldap.SCOPE_ONELEVEL,
'(objectClass=posixGroup)',
['cn'])
return sorted([self.__get_key(group[1], 'cn') for group in result])

def get_group_details(self, group_name):
result = self.ldap.search_s(
env('LDAP_GROUP_DN'),
ldap.SCOPE_ONELEVEL,
f'(&(objectClass=posixGroup)(cn={group_name}))',
['gidNumber', 'description', 'memberUid'])
if not result:
return None
group = result[0][1]
id = self.__get_key(group, 'gidNumber', '')
return {
'name': group_name,
'description': self.__get_key(group, 'description'),
'id': int(id) if id.isdecimal() else None,
'members': group.get('memberUid', []),
}
6 changes: 5 additions & 1 deletion teknologr/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ def get_api_root_view(self, api_urls=None):
url(r'^multi-functionaries/$', multi_functionaries_save),
url(r'^multi-decorationownerships/$', multi_decoration_ownerships_save),
url(r'^multi-applicantsubmissions/$', multi_applicant_submissions),
url(r'^accounts/ldap/(\d+)/$', LDAPAccountView.as_view(), name='ldap'),
url(r'^ldap/users/$', get_ldap_user_list),
url(r'^ldap/users/(.+)/$', get_ldap_user_details, name='ldap_user'),
url(r'^ldap/groups/$', get_ldap_group_list),
url(r'^ldap/groups/(.+)/$', get_ldap_group_details, name='ldap_group'),
url(r'^accounts/ldap/(\d+)/$', LDAPAccountView.as_view(), name='ldap_account'),
url(r'^accounts/ldap/change_pw/(\d+)/$', change_ldap_password),
url(r'^accounts/bill/(\d+)/$', BILLAccountView.as_view(), name='bill'),
url(r'^applicants/make-member/(\d+)/$', ApplicantMembershipView.as_view()),
Expand Down
54 changes: 50 additions & 4 deletions teknologr/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from datetime import datetime
from api.serializers import *
from api.filters import *
from api.ldap import LDAPAccountManager, LDAPError_to_string, get_ldap_account
from api.ldap import LDAPAccountManager, LDAPError_to_string
from api.bill import BILLAccountManager, BILLException
from api.utils import assert_public_member_fields
from api.mailutils import mailNewPassword, mailNewAccount
Expand Down Expand Up @@ -260,7 +260,7 @@ def get_serializer(self, *args, **kwargs):
class LDAPAccountView(APIView):
def get(self, request, member_id):
member = get_object_or_404(Member, id=member_id)
return Response(get_ldap_account(member.username), status=200)
return get_ldap_user_details_method(member.username)

def post(self, request, member_id):
# Create LDAP account for given user
Expand Down Expand Up @@ -315,6 +315,46 @@ def delete(self, request, member_id):

return HttpResponse(status=200)

@api_view(['GET'])
def get_ldap_user_list(_):
try:
with LDAPAccountManager() as lm:
return Response(lm.get_user_list())
except LDAPError as e:
return Response({'detail': LDAPError_to_string(e)}, status=500)

def get_ldap_user_details_method(username):
try:
with LDAPAccountManager() as lm:
user = lm.get_user_details(username)
except LDAPError as e:
return Response({'detail': LDAPError_to_string(e)}, status=500)
if not user:
return Response({'detail': 'Could not find LDAP user.'}, status=404)
return Response(user)

@api_view(['GET'])
def get_ldap_user_details(_, username):
return get_ldap_user_details_method(username)

@api_view(['GET'])
def get_ldap_group_list(_):
try:
with LDAPAccountManager() as lm:
return Response(lm.get_group_list())
except LDAPError as e:
return Response({'detail': LDAPError_to_string(e)}, status=500)

@api_view(['GET'])
def get_ldap_group_details(_, group_name):
try:
with LDAPAccountManager() as lm:
group = lm.get_group_details(group_name)
except LDAPError as e:
return Response({'detail': LDAPError_to_string(e)}, status=500)
if not group:
return Response({'detail': 'Could not find LDAP group.'}, status=404)
return Response(group)

@api_view(['POST'])
def change_ldap_password(request, member_id):
Expand All @@ -340,7 +380,13 @@ def change_ldap_password(request, member_id):
class BILLAccountView(APIView):
def get(self, request, member_id):
member = get_object_or_404(Member, id=member_id)
return Response(BILLAccountManager().get_bill_info(member.bill_code), status=200)
try:
account = BILLAccountManager().get_account_by_code(member.bill_code)
except Exception as e:
return Response({'detail': str(e)}, status=500)
if not account:
return Response({'detail': 'Could not find BILL account.'}, status=404)
return Response(account)

def post(self, request, member_id):
member = get_object_or_404(Member, id=member_id)
Expand All @@ -355,7 +401,7 @@ def post(self, request, member_id):

# Check if there already is a BILL account with this LDAP name
try:
bill_code = bm.find_bill_code(member.username)
bill_code = bm.get_account_by_username(member.username).get('acc')
except:
pass

Expand Down
14 changes: 7 additions & 7 deletions teknologr/members/templates/member.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<a href="{% url 'api:member-detail' member.id %}">Member</a>
<a href="{% url 'api:dump_htk' member.id %}">HTK dump</a>
{% if member.username %}
<a href="{% url 'api:ldap' member.id %}">LDAP</a>
<a href="{% url 'api:ldap_account' member.id %}">LDAP</a>
{% endif %}
{% if member.bill_code %}
<a href="{% url 'api:bill' member.id %}">BILL</a>
Expand Down Expand Up @@ -188,22 +188,22 @@ <h5>LDAP</h5>

{% if LDAP.error %}
<div class="alert alert-danger">{{ LDAP.error }}</div>
{% elif not LDAP.exists %}
{% elif not LDAP.username %}
<div class="alert alert-danger">LDAP-konto "{{ member.username }}" existerar inte</div>
{% endif %}
<b>Användarnamn:</b> <span class="monospace">{{ member.username }}</span>
<br/>
<b>Grupper:</b>
<ul>
{% for group in LDAP.groups %}
<li>{{ group }}</li>
<li><a href="{% url 'api:ldap_group' group %}">{{ group }}</a></li>
{% endfor %}
</ul>
<button
class="btn btn-primary"
data-toggle="modal"
data-target="#changepw_modal"
{% if not LDAP.exists %}disabled title="LDAP-konto existerar inte"{% endif %}>
{% if not LDAP.username %}disabled title="LDAP-konto existerar inte"{% endif %}>
Ändra lösenord
</button>
{% include "modals/ldap_changepw.html" with modalname="changepw_modal" title="Ändra LDAP lösenord" member_id=member.id only %}
Expand Down Expand Up @@ -237,15 +237,15 @@ <h5>BILL</h5>

{% if BILL.error %}
<div class="alert alert-danger">{{ BILL.error }}</div>
{% elif not BILL.exists %}
{% elif not BILL.acc %}
<div class="alert alert-danger">BILL-konto "{{ member.bill_code }}" existerar inte</div>
{% endif %}

<b>Konto:</b>
<span class="monospace">{{ member.bill_code }}</span>
<br/>

{% if BILL.exists %}
{% if BILL.acc %}
<b>Saldo:</b> {{ BILL.balance }}€
<br/>
{% endif %}
Expand All @@ -259,7 +259,7 @@ <h5>BILL</h5>
</button>

{% else %}
<button id="add-bill-button" class="btn btn-primary" {% if not LDAP.exists %} disabled title="Skapa LDAP-konto först"{% endif %} data-id="{{ member.id }}">Skapa BILL-konto</button>
<button id="add-bill-button" class="btn btn-primary" {% if not LDAP.username %} disabled title="Skapa LDAP-konto först"{% endif %} data-id="{{ member.id }}">Skapa BILL-konto</button>
{% endif %}
</div>
</div>
Expand Down
19 changes: 13 additions & 6 deletions teknologr/members/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from members.programmes import DEGREE_PROGRAMME_CHOICES
from registration.models import Applicant
from registration.forms import RegistrationForm
from api.ldap import get_ldap_account
from api.ldap import LDAPAccountManager
from api.bill import BILLAccountManager
from getenv import env
from locale import strxfrm
Expand Down Expand Up @@ -116,15 +116,22 @@ def member(request, member_id):

# Get user account info
if member.username:
context['LDAP'] = get_ldap_account(member.username)
try:
with LDAPAccountManager() as lm:
context['LDAP'] = lm.get_user_details(member.username)
except LDAPError as e:
context['LDAP'] = {'error': LDAPError_to_string(e)}

if member.bill_code:
bm = BILLAccountManager()
context['bill_admin_url'] = bm.admin_url(member.bill_code)
context['BILL'] = bm.get_bill_info(member.bill_code)
username = context['BILL'].get('id')
if username and member.username != username:
context['BILL']['error'] = f'LDAP användarnamnen här ({member.username}) och i BILL ({username}) matchar inte'
try:
context['BILL'] = bm.get_account_by_code(member.bill_code) or {}
username = context['BILL'].get('id')
if username and member.username != username:
context['BILL']['error'] = f'LDAP användarnamnen här ({member.username}) och i BILL ({username}) matchar inte'
except Exception as e:
context['BILL'] = {'error': str(e)}

# load side list items
set_side_context(context, 'members', member)
Expand Down