Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:Teknologforeningen/teknologr.io …
Browse files Browse the repository at this point in the history
…into develop
  • Loading branch information
tlangens committed Jul 24, 2017
2 parents 413754e + 1721c4e commit afd8cc4
Show file tree
Hide file tree
Showing 17 changed files with 680 additions and 15 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ Membership management system tailored for TF use

## Installation

First make sure that you have Python 3 installed and virtualenv to go with it.
Install prerequisites:

sudo apt install libsasl2-dev python3-dev libldap2-dev libssl-dev

Make sure that you have Python 3 installed and virtualenv to go with it.

1. Create virtualenv: `virtualenv -p /usr/bin/python3 venv`
2. Activate venv: `source venv/bin/activate`
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ django-getenv==1.3.1
git+https://github.com/Work4Labs/django-test-pep8
djangorestframework==3.6.2
pep8==1.7.0
pyldap==2.4.28
requests==2.13.0
39 changes: 37 additions & 2 deletions teknologr/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,43 @@
# Secret key, change it maybe?
SECRET_KEY=SomeRandomSecretKey

#Should debug be enabled? True/False
# Should debug be enabled? True/False
DEBUG=True

# Database URL: https://github.com/kennethreitz/dj-database-url
DATABASE=sqlite:////path/to/tf-members/postgres
DATABASE=sqlite:////path/to/tf-members/postgres


# LDAP URL
LDAP_SERVER_URI=ldaps://localhost:45671

# LDAP User base dn
LDAP_USER_DN="ou=People,dc=teknologforeningen,dc=fi"

# LDAP Template dn for LDAP users
LDAP_USER_DN_TEMPLATE="uid=%(user)s,ou=People,dc=teknologforeningen,dc=fi"

# LDAP Group base dn
LDAP_GROUP_DN="ou=Group,dc=teknologforeningen,dc=fi"

# LDAP Member group dn
LDAP_MEMBER_GROUP_DN="cn=medlem,ou=Group,dc=teknologforeningen,dc=fi"

# LDAP staff group dn
LDAP_STAFF_GROUP_DN="cn=teknologr,ou=Group,dc=teknologforeningen,dc=fi"

# LDAP writer dn
LDAP_ADMIN_BIND_DN="cn=svaksvat,dc=teknologforeningen,dc=fi"

# LDAP writer password
LDAP_ADMIN_PW="testPass"


# BILL API URL
BILL_API_URL="https://bill.teknologforeningen.fi/api/"

# User to authenticate with to BILL API
BILL_API_USER="user"

# Password to authenticate with to BILL API
BILL_API_PW="hunter2"
63 changes: 63 additions & 0 deletions teknologr/api/bill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import requests
from getenv import env


class BILLException(Exception):
pass


class BILLAccountManager:

def __init__(self):
self.api_url = env("BILL_API_URL")
self.user = env("BILL_API_USER")
self.password = env("BILL_API_PW")

def create_bill_account(self, username):
try:
r = requests.post(self.api_url + "add?type=user&id=%s" % username, auth=(self.user, self.password))
except:
raise BILLException("Could not connect to BILL server")
if r.status_code != 200:
raise BILLException("BILL returned status: %d" % r.status_code)
try:
number = int(r.text)
except ValueError:
# Returned value not a BILL code or error code
raise BILLException("BILL returned error: " + r.text)
if number < 0:
raise BILLException("BILL returned error code: " + r.text)
return number

def delete_bill_account(self, bill_code):
try:
r = requests.post(self.api_url + "del?type=user&acc=%s" % bill_code, auth=(self.user, self.password))
except:
raise BILLException("Could not connect to BILL server")
if r.status_code != 200:
raise BILLException("BILL returned status: %d" % r.status_code)
try:
number = int(r.text)
except ValueError:
# Returned value not a number, unknown error occurred
raise BILLException("BILL returned error: " + r.text)
if number == 0:
pass # All is good
else:
raise BILLException("BILL returned error code: %d" % number)

def get_bill_info(self, bill_code):
import json
try:
r = requests.get(self.api_url + "get?type=user&acc=%s" % bill_code, auth=(self.user, self.password))
except:
raise BILLException("Could not connect to BILL server")
if r.status_code != 200:
raise BILLException("BILL returned status: %d" % r.status_code)
# BILL API does not use proper http status codes
try:
error = int(r.text)
except ValueError:
# The returned string is not an integer, so presumably we have the json we want
return json.loads(r.text)
raise BILLException("BILL returned error code: " + r.text)
119 changes: 119 additions & 0 deletions teknologr/api/ldap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import ldap
import ldap.modlist as modlist
from getenv import env

import time

'''All methods here can throw ldap.LDAPError'''


class LDAPAccountManager:
def __init__(self):
# Don't require certificates
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
# Attempts not connection, simply initializes the object.
self.ldap = ldap.initialize(env("LDAP_SERVER_URI"))

def __enter__(self):
self.ldap.simple_bind_s(
env("LDAP_ADMIN_BIND_DN"),
env("LDAP_ADMIN_PW")
)
return self

def __exit__(self, exc_type, exc_value, traceback):
self.ldap.unbind_s()

def add_account(self, member, username, password):
# Adds new account for the given member with the given username and password
dn = env("LDAP_USER_DN_TEMPLATE") % {'user': username}

uidnumber = self.get_next_uidnumber()
nt_pw = self.get_samba_password(password)

# Everything has to be byte string because why the fuck not?
attrs = {}
attrs['uid'] = [username.encode('utf-8')]
attrs['cn'] = [member.full_preferred_name.encode('utf-8')]
homedir = '/rhome/%s' % username
attrs['homeDirectory'] = [homedir.encode('utf-8')]
attrs['uidNumber'] = [str(uidnumber).encode('utf-8')]
attrs['mailHost'] = [b'smtp.ayy.fi']
attrs['gidNumber'] = [b'1000']
attrs['sn'] = [member.surname.encode('utf-8')]
attrs['givenName'] = [member.preferred_name.encode('utf-8')]
attrs['loginShell'] = [b'/bin/bash']
attrs['objectClass'] = [
b'kerberosSecurityObject',
b'inetOrgPerson',
b'posixAccount',
b'shadowAccount',
b'inetLocalMailRecipient',
b'top',
b'person',
b'organizationalPerson',
b'billAccount',
b'sambaSamAccount'
]
attrs['krbName'] = [username.encode('utf-8')]
attrs['mail'] = [member.email.encode('utf-8')]
attrs['userPassword'] = [password.encode('utf-8')]
sambasid = "S-1-0-0-%s" % str(uidnumber*2+1000)
attrs['sambaSID'] = [sambasid.encode('utf-8')]
attrs['sambaNTPassword'] = [nt_pw.encode('utf-8')]
attrs['sambaPwdLastSet'] = [str(int(time.time())).encode('utf-8')]

# Add the user to LDAP
ldif = modlist.addModlist(attrs)
self.ldap.add_s(dn, ldif)

# Add user to Members group
group_dn = env("LDAP_MEMBER_GROUP_DN")
self.ldap.modify_s(group_dn, [(ldap.MOD_ADD, 'memberUid', username.encode('utf-8'))])

def get_next_uidnumber(self):
# Returns the next free uidnumber greater than 1000
output = self.ldap.search_s(env("LDAP_USER_DN"), ldap.SCOPE_ONELEVEL, attrlist=['uidNumber'])
uidnumbers = [int(user[1]['uidNumber'][0]) for user in output]
uidnumbers.sort()

# Find first free uid over 1000.
last = 1000
for uid in uidnumbers:
if uid > last + 1:
break
last = uid
return last + 1

def delete_account(self, username):
# Remove user from members group
group_dn = env("LDAP_MEMBER_GROUP_DN")
self.ldap.modify_s(group_dn, [(ldap.MOD_DELETE, 'memberUid', username.encode('utf-8'))])

# Remove user
dn = env("LDAP_USER_DN_TEMPLATE") % {'user': username}
self.ldap.delete_s(dn)

def change_password(self, username, password):
# Changes both the user password and the samba password
dn = env("LDAP_USER_DN_TEMPLATE") % {'user': username}
nt_pw = self.get_samba_password(password)
mod_attrs = [
(ldap.MOD_REPLACE, 'userPassword', password.encode('utf-8')),
(ldap.MOD_REPLACE, 'sambaNTPassword', nt_pw.encode('utf-8'))
]
self.ldap.modify_s(dn, mod_attrs)

def get_samba_password(self, password):
# The password needs to be stored in a different format for samba
import codecs
import hashlib
return codecs.encode(
hashlib.new('md4', password.encode('utf-16le')).digest(), 'hex_codec'
).decode('utf-8').upper()

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] for group in output]
5 changes: 4 additions & 1 deletion teknologr/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^multiGroupMembership/', memberListSave),
url(r'^multiGroupMembership/$', memberListSave),
url(r'^memberTypesForMember/(?P<mode>username|studynumber)/(?P<query>[A-Za-z0-9]+)/$', memberTypesForMember),
url(r'^accounts/ldap/(\d+)/$', LDAPAccountView.as_view()),
url(r'^accounts/ldap/change_pw/(\d+)/$', change_ldap_password),
url(r'^accounts/bill/(\d+)/$', BILLAccountView.as_view()),
]
Loading

0 comments on commit afd8cc4

Please sign in to comment.