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

Member signup #32

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
19 changes: 19 additions & 0 deletions MemberLifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ The requirements to vogelnest in this process are:
- notify ppl to activate the user
- invite the user to choose a password
- reflect the newly choosen username back to CiviCRM; CiviCRM will save the mail adress in the SOG domain

### Technical Documentation

CiviCRM drops a request to the endpoint *create_user* using POST.
The following info shall be delivered in a JSON body:
- First Name
- Last Name
- E-Mail
- Lokalgruppe (prefixed lowercase, like lg_aachen or lg_berlin)

Since only Civi may create users, the request has to be authenticated
by HTTP basic auth.
Username: *civicrm*
Password: As specified in CIVICRM_SECRET

This app will generate a username and return it as plain text
in the response body.

This app will issue error 500 if user creation is not possible.

## Other requirements in the member lifecycle
- Changes in the alternative e mail adress shall be reflected (see #28)
Expand Down
51 changes: 39 additions & 12 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from ldap3.utils import conv
from middleware import middleware
import token_handler
import group_request_handler
from os.path import join
from urllib.parse import unquote
import jwt
Expand Down Expand Up @@ -54,6 +55,42 @@ def login():
except Exception as e:
abort(403)

@app.route('/create_user', methods=['POST'])
def create_user():
# Handle Auth
if request.authorization["username"] != "civicrm" or request.authorization["password"] != config.CIVICRM_SECRET:
abort(403), "Invalid credentials"
firstName = sanitize(request.json.get('firstName'))
lastName = sanitize(request.json.get('lastName'))
email = sanitize(request.json.get('email'))
lokalgruppe = sanitize(request.json.get('lokalgruppe')) #like lg_aachen

# Abort 500 if lokalgruppe does not exist
try:
api.get_group(lokalgruppe)
except:
abort(500, "lokalgruppe " + lokalgruppe + " does not exist")

try:
username = api.create_member(firstName, lastName,email)

# Ask member to set password
password_reset_token = token_handler.create_initial_confirmation_jwt_token(username, email).decode("utf-8")
mail.send_email(email, "Willkommen bei SOG!", "emails/new_user_onboarding", {
"firstName" : firstName,
"name": username,
"link": join(config.FRONTEND_URL, "confirm?key=" + password_reset_token),
})

# request membership in LG. Is non existent LG be dealt with correctly?
group_request_handler.request_inactive_pending(api, lokalgruppe, username)
# Person becomes member of allgemein upon activation

return username
except Exception as e:
print(e)
abort(500)

@app.route('/inactive_info', methods=['GET'])
def inactive_info():
uid = token_handler.get_jwt_user(request.headers.get('Authorization'))
Expand Down Expand Up @@ -446,7 +483,7 @@ def add_guest_to_group(group_id):
api.add_group_member(group_id, uid)

group = api.get_group(group_id)
mail.send_email(str(owner.mail), "Du bist jetzt im Verteiler " + str(group.cn), \
mail.send_email(str(mail), "Du bist jetzt im Verteiler " + str(group.cn), \
"emails/guest_invite_email", {
"name": str(name),
"group_name": str(group.cn),
Expand All @@ -461,17 +498,7 @@ def request_access_to_group(group_id):
return abort(401)
if not api.is_active(my_uid):
return abort(401)
api.add_group_active_pending_member(group_id, my_uid)
group = api.get_group(group_id)
user = api.get_user(my_uid)
for owner in api.get_group_owners(group_id):
mail.send_email(str(owner.mail), "Neue Anfrage in " + str(group.cn), \
"emails/new_pending_member_mail", {
"name": str(owner.cn),
"group_name": str(group.cn),
"dashboard_url": config.FRONTEND_URL,
"new_member_name": str(user.cn)
})
group_request_handler.request_active_pending(api, group_id, my_uid)
return "ok"

@app.route('/groups/<group_id>/accept_pending_member', methods=['POST'])
Expand Down
2 changes: 2 additions & 0 deletions config.sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@
MAIL_SERVER = ''
MAIL_PASSWORD = ''

# Token used by civi to authenticate its requests
CIVICRM_SECRET = ''
8 changes: 8 additions & 0 deletions emails/new_lg_member.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<p>Hallo,</p>

<p>Soeben hat sich {new_member_name} für Deine Lokalgruppe {group_name} angemeldet.
Das neue Mitglied ist schon auf dem Lokalgruppen-Verteiler eingetragen und hat einen Account erhalten.</p>
<p><strong>Achtung:</strong> Der SOG-Account des Mitglieds muss erst von dir aktiviert werden.</p>
<p>Bitte bestätige am Besten jetzt <a href='{dashboard_url}'>direkt im Vogelnest</a>, dass {new_member_name} tatsächlich in eurer LG aktiv ist!</p>
<p>Viele Grüße,</p>
<p>Das SOG Vogelnest</p>
13 changes: 13 additions & 0 deletions emails/new_lg_member.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Hallo,

Soeben hat sich {new_member_name} für Deine Lokalgruppe {group_name} angemeldet.
Das neue Mitglied ist schon auf dem Lokalgruppen-Verteiler eingetragen und hat einen Account erhalten.
Achtung: Der SOG-Account des Mitglieds muss erst von dir aktiviert werden.

Bitte bestätige am Besten jetzt direkt im Vogelnest, dass {new_member_name} tatsächlich in eurer LG aktiv ist:

{dashboard_url}

Viele Grüße,

Das SOG Vogelnest
12 changes: 12 additions & 0 deletions emails/new_user_onboarding.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<p>Hallo {firstName},</p>
<p>Wir freuen uns sehr, dich als neues Mitglied bei Studieren Ohne Grenzen begrüßen zu dürfen.</p>
<p>Damit du direkt einsteigen und mitarbeiten kannst, haben wir dir automatisch einen Zugang für unsere Online-Plattformen erstellt. Über diese Plattformen tauschen wir wichtige Nachrichten, Informationen und Dateien aus und diskutieren auch Lokalgruppen-übergreifend.</p>
<p>Benutzername ist: {name}</p>
<p>Bitte öffne folgenden Link, um deine Mailadresse zu bestätigen und ein Passwort zu vergeben:
<a href="{link}">Account bestätigen</a></p>
<p>Dein Account wird freigeschaltet, sobald dein Lokalkoordinator:in bestätigt hat, dass du tatsächlich bei Studieren Ohne Grenzen aktiv bist.
Mit deinem Benutzernamen und dem von dir vergebenen Passwort kannst du dich auf allen SOG-Systeme einloggen.</p>

<p>Eine Übersicht deiner Daten und Gruppen gibt dir das Vogelnest: <a href="vogelnest.studieren-ohne-grenzen.org">vogelnest.studieren-ohne-grenzen.org</a></p>
<p>Viele Grüße,</p>
<p>Das SOG Vogelnest</p>
18 changes: 18 additions & 0 deletions emails/new_user_onboarding.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Hallo {firstName},

Wir freuen uns sehr, dich als neues Mitglied bei Studieren Ohne Grenzen begrüßen zu dürfen.

Damit du direkt einsteigen und mitarbeiten kannst, haben wir dir automatisch einen Zugang für unsere Online-Plattformen erstellt.
Über diese Plattformen tauschen wir wichtige Nachrichten, Informationen und Dateien aus und diskutieren auch Lokalgruppen-übergreifend.

Dein Benutzername ist: {name}
Bitte öffne folgenden Link, um deine Mailadresse zu bestätigen und ein Passwort zu vergeben:
{link}

Dein Account wird freigeschaltet, sobald dein Lokalkoordinator:in bestätigt hat, dass du tatsächlich bei Studieren Ohne Grenzen aktiv bist.

Mit deinem Benutzernamen und dem von dir vergebenen Passwort kannst du dich auf allen SOG-Systeme einloggen.

Eine Übersicht deiner Daten und Gruppen gibt dir das Vogelnest: vogelnest.studieren-ohne-grenzen.org
Viele Grüße,
Das SOG Vogelnest
27 changes: 27 additions & 0 deletions group_request_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import mail
import config

def request_active_pending(api, group_id, my_uid):
api.add_group_active_pending_member(group_id, my_uid)
group = api.get_group(group_id)
user = api.get_user(my_uid)
for owner in api.get_group_owners(group_id):
mail.send_email(str(owner.mail), "Neue Anfrage in " + str(group.cn), \
"emails/new_pending_member_mail", {
"name": str(owner.cn),
"group_name": str(group.cn),
"dashboard_url": config.FRONTEND_URL,
"new_member_name": str(user.cn)
})

def request_inactive_pending(api, group_id, my_uid):
api.add_group_inactive_pending_member(group_id, my_uid)
group = api.get_group(group_id)
user = api.get_inactive_user(my_uid)
for owner in api.get_group_owners(group_id):
mail.send_email(str(owner.mail), "Neue Anfrage in " + str(group.cn), \
"emails/new_lg_member", {
"group_name": str(group.cn),
"dashboard_url": config.FRONTEND_URL,
"new_member_name": str(user.cn)
})
38 changes: 38 additions & 0 deletions ldap_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,39 @@ def create_guest(self, name, mail):
raise RuntimeError(self.conn.result)
return uid

# Creates new (inactive) member
def create_member(self, firstName, lastName, mail):
username = self.generate_username("%s.%s" % (firstName, lastName))
dn = self.get_inactive_person_dn(username)
sog_mail = username + "@studieren-ohne-grenzen.org"
sog_alias = username + "@s-o-g.org"
self.conn.add(dn, [
'person',
'sogperson',
'organizationalPerson',
'inetOrgPerson',
'top',
'PostfixBookMailAccount',
'PostfixBookMailForward'
], {
'uid': username,
'cn': '%s %s' % (firstName, lastName),
'displayName': firstName,
'givenName': firstName,
'sn': lastName,
'mail': sog_mail,
'mailAlias': sog_alias,
'mail-alternative': mail,
'mailHomeDirectory': '/srv/vmail/%s' % sog_mail,
'mailStorageDirectory': 'maildir:/srv/vmail/%s/Maildir' % sog_mail,
'mailEnabled': 'TRUE',
'mailGidNumber': 5000,
'mailUidNumber': 5000
})
if self.conn.result['result'] != 0:
raise RuntimeError(self.conn.result)
return username

def activate_user(self, uid):
old_dn = self.get_inactive_person_dn(uid)
pending_groups = self.get_groups_as_inactive_pending_member(uid)
Expand Down Expand Up @@ -222,6 +255,11 @@ def get_groups_as_inactive_pending_member(self, uid):
self.conn.search(self.config.DN_GROUPS, '(&(objectClass=groupOfNames)(pending=%s))' % user_dn, attributes=GROUP_ATTRIBUTES_PENDING)
return self.conn.entries

def add_group_inactive_pending_member(self, group, uid):
group_dn = self.get_group_dn(group)
user_dn = self.find_inactive_user_dn(uid)
self.conn.modify(group_dn, {'pending': [(MODIFY_ADD, [user_dn])]})

def add_group_active_pending_member(self, group, uid):
group_dn = self.get_group_dn(group)
user_dn = self.find_user_dn(uid)
Expand Down
3 changes: 1 addition & 2 deletions middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ def __call__(self, environ, start_response):
authheader = request.headers.get('Authorization')

uid = token_handler.get_jwt_user(authheader)
print(request.url.replace(request.url_root, ""))
if uid == None and \
not request.url.replace(request.url_root, "") in ["login", "users/reset_password", "users/set_password_with_key"] and \
not request.url.replace(request.url_root, "") in ["login", "users/reset_password", "users/set_password_with_key", "create_user"] and \
not request.url.replace(request.url_root, "").startswith("confirm"):
res = Response(u'Authorization failed', mimetype= 'text/plain', status=401)
return res(environ, start_response)
Expand Down
8 changes: 8 additions & 0 deletions token_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def create_password_reset_jwt_token(username):
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}, config.JWT_SECRET, algorithm='HS256')

def create_initial_confirmation_jwt_token(username, email):
return jwt.encode({
'type': 'initial_confirmation',
'username': username,
'email': email,
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}, config.JWT_SECRET, algorithm='HS256')

# checks header and returns username if header is valid
# returns None if header is invalid
def get_jwt_user(header):
Expand Down