Skip to content

Commit

Permalink
Merge pull request #23 from Teknologforeningen/develop
Browse files Browse the repository at this point in the history
First production version
  • Loading branch information
tlangens authored Jul 24, 2017
2 parents 69b5582 + 1721c4e commit 018f27c
Show file tree
Hide file tree
Showing 90 changed files with 3,592 additions and 392 deletions.
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[run]
branch = True
omit =
*/migrations/*
*/__init__.py
*/tests*.py
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,6 @@ db.sqlite3

#Webstorm
.idea/

#VS Code
.vscode/
18 changes: 18 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
language: python
python:
- "3.4"
- "3.5"
- "3.6"
# command to install dependencies
install:
- "pip install -r requirements.txt"
- "pip install coveralls"
# command to run tests
script: cd teknologr && coverage run --source members,api manage.py test

after_success:
coveralls --rcfile=../.coveragerc

#Turn off mail notification
notifications:
email: false
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# teknologr.io
# teknologr.io [![Build Status](https://travis-ci.org/Teknologforeningen/teknologr.io.svg?branch=develop)](https://travis-ci.org/Teknologforeningen/teknologr.io) [![Coverage Status](https://coveralls.io/repos/github/Teknologforeningen/teknologr.io/badge.svg?branch=develop)](https://coveralls.io/github/Teknologforeningen/teknologr.io?branch=develop)
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:

1. Create virtualenv: `virtualenv3 venv`
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`
3. Install stuff from pip: `pip install -r requirements.txt`
3. Install stuff with pip: `pip install -r requirements.txt`

## Code style
pep8 check will be done when doing `python manage.py test`.
Linting only can be run with `python manage.py test test_pep8`.
9 changes: 9 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
dj-database-url==0.4.2
Django==1.9.2
django-ajax-selects==1.5.2
django-countries==3.4.1
django-dotenv==1.4.1
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
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pep8]
max-line-length = 119
46 changes: 46 additions & 0 deletions teknologr/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# This file contains configuration and settings variables that should be different in production and development environments.


# Secret key, change it maybe?
SECRET_KEY=SomeRandomSecretKey

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

# Database URL: https://github.com/kennethreitz/dj-database-url
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"
Empty file added teknologr/api/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions teknologr/api/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.contrib import admin

# Register your models here.
5 changes: 5 additions & 0 deletions teknologr/api/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class ApiConfig(AppConfig):
name = 'api'
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]
Empty file.
3 changes: 3 additions & 0 deletions teknologr/api/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.db import models

# Create your models here.
79 changes: 79 additions & 0 deletions teknologr/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from django_countries import Countries
from rest_framework import serializers
from members.models import *


class SerializableCountryField(serializers.ChoiceField):
def to_representation(self, value):
if value in ('', None):
return '' # instead of `value` as Country(u'') is not serializable
return super(SerializableCountryField, self).to_representation(value)

# Serializers define the API representation.

# Members


class MemberSerializer(serializers.ModelSerializer):
country = SerializableCountryField(allow_blank=True, choices=Countries(), required=False)
nationality = SerializableCountryField(allow_blank=True, choices=Countries(), required=False)

class Meta:
model = Member
fields = '__all__'


# Groups

class GroupSerializer(serializers.ModelSerializer):
class Meta:
model = Group
fields = '__all__'


class GroupTypeSerializer(serializers.ModelSerializer):
class Meta:
model = GroupType
fields = '__all__'


class GroupMembershipSerializer(serializers.ModelSerializer):
class Meta:
model = GroupMembership
fields = '__all__'


# Functionaries

class FunctionarySerializer(serializers.ModelSerializer):
class Meta:
model = Functionary
fields = '__all__'


class FunctionaryTypeSerializer(serializers.ModelSerializer):
class Meta:
model = FunctionaryType
fields = '__all__'


# Decorations

class DecorationSerializer(serializers.ModelSerializer):
class Meta:
model = Decoration
fields = '__all__'


class DecorationOwnershipSerializer(serializers.ModelSerializer):
class Meta:
model = DecorationOwnership
fields = '__all__'


# MemberTypes

class MemberTypeSerializer(serializers.ModelSerializer):
class Meta:
model = MemberType
fields = '__all__'
Loading

0 comments on commit 018f27c

Please sign in to comment.