From daeb15f2d2f88b8fc78a969dfcd8580eb89bc806 Mon Sep 17 00:00:00 2001 From: William Brown Date: Nov 14 2019 01:48:56 +0000 Subject: Ticket 2 - allow configuration of the portal Bug Description: Many sites will want to allow customising their portal experience by changing what attributes "can" and "can not" be edited, what is displayed, and of course the basedn of the site. Fix Description: Allow limited configuration of the instance via a config.ini file. https://pagure.io/389-ds-portal/issue/2 Author: William Brown Review by: mreynolds (Thanks!) --- diff --git a/.gitignore b/.gitignore index 5b0df7a..07d8e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ build/ *.egg-info/ .DS_Store +config.ini diff --git a/config.example.ini b/config.example.ini new file mode 100644 index 0000000..a99c77a --- /dev/null +++ b/config.example.ini @@ -0,0 +1,66 @@ +# Example 389-ds-portal configuration +# This allows you to customise and configure your portal +# and how it operates. +# +# This file, including the signing_key values should be shared amongst all +# 389-ds-portal instances in a load balanced environment. + +[dsportal] +ldapurl = ldapi://%%2fdata%%2frun%%2fslapd-localhost.socket +; basedn = ou=People,dc=example,dc=com +# This should be 64 bytes of random base64ed. +; fernet_cookie_key = +# This should be 16 bytes of random base64ed. +; cookie_signing_key = +# IE: +# python +# >>> import base64, os +# >>> base64.b64encode(os.urandom(16)) +# '...' + +[account] +class = nsaccount +attributes = uid uidNumber gidNumber homeDirectory mail displayName legalName loginShell nsSshPublicKey +name_attr = cn uid + +# This is a list of attributes and how to render them in the templates. Note that +# editable false is NOT a security control - it only controls if the attribute has +# an editable form in the html. You must configure your ACI's to match your expectations! +# +# The below definitions will work out-of-the-box with directory server 1.4.2 and +# represent current best practices. +[attr.uid] +multivalue = true +editable = false + +[attr.uidNumber] +multivalue = false +editable = false + +[attr.gidNumber] +multivalue = false +editable = false + +[attr.homeDirectory] +multivalue = false +editable = false + +[attr.mail] +multivalue = true +editable = false + +[attr.displayName] +multivalue = false +editable = true + +[attr.legalName] +multivalue = false +editable = true + +[attr.loginShell] +multivalue = false +editable = false + +[attr.nsSshPublicKey] +multivalue = true +editable = true diff --git a/server.py b/server.py index ff9bae8..daf8f4d 100644 --- a/server.py +++ b/server.py @@ -1,6 +1,8 @@ import os import base64 +import configparser import copy +import sys from cryptography.fernet import Fernet from cryptography.hazmat.backends import default_backend @@ -12,26 +14,36 @@ from flask import Flask, session, redirect, url_for, escape, request, render_tem from lib389 import DirSrv from lib389.idm.account import Accounts from lib389.idm.user import nsUserAccounts +from lib389._mapped_object import _gen_or, _gen_filter, _term_gen app = Flask(__name__) -# Set the secret key to some random bytes. Keep this really secret! -app.secret_key = os.urandom(16) -# app.secret_key = b'\xb5\x97\x93~!t\x97\r\x1e\xfd\xea\xf8\xe9\xd5\x91\xb0' +config = configparser.ConfigParser() +config.read('config.ini') -# url_for('static', filename='css/bootstrap.min.css') -# url_for('static', filename='js/bootstrap.min.js') -# url_for('static', filename='js/jquery-3.3.1.slim.min.js') -# url_for('static', filename='js/popper.min.js') +# Validate we have all our needed keys here ... +if not config.has_option('dsportal', 'ldapurl'): + app.logger.error("Missing config.ini option dsportal:ldapurl\n") + raise Exception("Missing config.ini option dsportal:ldapurl") +if not config.has_option('dsportal', 'basedn'): + app.logger.error("Missing config.ini option dsportal:basedn\n") + raise Exception("Missing config.ini option dsportal:basedn") +if not config.has_option('dsportal', 'fernet_cookie_key'): + app.logger.error("Missing config.ini option dsportal:fernet_cookie_key\n") + raise Exception("Missing config.ini option dsportal:fernet_cookie_key") +if not config.has_option('dsportal', 'cookie_signing_key'): + app.logger.error("Missing config.ini option dsportal:cookie_signing_key\n") + raise Exception("Missing config.ini option dsportal:cookie_signing_key") + + +app.secret_key = base64.b64decode(config['dsportal']['cookie_signing_key']) CONFIG = { - 'ldapurl': 'ldapi://%2fdata%2frun%2fslapd-localhost.socket', - 'basedn': 'ou=People,dc=example,dc=com', - # Find a way to load this from env better so it can be shared correctly. - 'cookie_key': os.urandom(64), + 'ldapurl': config['dsportal']['ldapurl'], + 'basedn': config['dsportal']['basedn'], + 'cookie_key': base64.b64decode(config['dsportal']['fernet_cookie_key']) } - KDF = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, @@ -42,55 +54,24 @@ KDF = PBKDF2HMAC( COOKIE_KEY = base64.urlsafe_b64encode(KDF.derive(CONFIG['cookie_key'])) FERNET = Fernet(COOKIE_KEY) +ACCOUNT = { + 'class': config['account']['class'], + 'name_attr': config['account']['name_attr'], + 'attributes': config['account']['attributes'].split(), +} + DISPLAY_ATTRS = [ { - "a": "uid", - "multi": "true", - "edit": "false", - }, - { - "a": "uidNumber", - "multi": "false", - "edit": "false", - }, - { - "a": "gidNumber", - "multi": "false", - "edit": "false", - }, - { - "a": "homeDirectory", - "multi": "false", - "edit": "false", - }, - { - "a": "mail", - "multi": "true", - "edit": "false", - }, - { - "a": "displayName", - "multi": "false", - "edit": "true", - }, - { - "a": "legalName", - "multi": "false", - "edit": "true", - }, - { - "a": "loginShell", - "multi": "false", - "edit": "false", - }, - { - "a": "nsSshPublicKey", - "multi": "true", - "edit": "true", - }, + # Split attr.name to name. + "a": sect.split('.', 1)[1], + "multi": config[sect].getboolean("multivalue"), + "edit": config[sect].getboolean("editable"), + } + for sect in config.sections() + if sect.startswith('attr.') and sect.split('.', 1)[1] in ACCOUNT['attributes'] ] -GET_DISPLAY_ATTRS = [ x["a"] for x in DISPLAY_ATTRS ] +GET_DISPLAY_ATTRS = ACCOUNT['attributes'] def _get_ds_instance(ldapurl, binddn, password): inst = DirSrv(verbose=True) @@ -103,7 +84,10 @@ def _get_name_to_dn(ldapurl, name, basedn, binddn=None, password=None): # Use the idm account module. accounts = Accounts(inst, basedn) # Use the filter type - cands = accounts.filter(f'(|(cn={name})(uid={name}))') + filt = _gen_or( + _gen_filter(ACCOUNT['attributes'], _term_gen(name)) + ) + cands = accounts.filter(filt) # Check there is only one? if len(cands) == 0: raise @@ -142,7 +126,7 @@ def account_dn_update(req_dn): # TODO: Put a session invalid message here. return redirect(url_for('login')) - print("Got: %s" % request.json) + app.logger.debug("Got: %s" % request.json) # Transform the request to the state dict expected by lib389 state = {} @@ -280,6 +264,7 @@ def index(): try: inst.open() except Exception as e: + app.logger.debug('open()') app.logger.debug(e) return redirect(url_for('error')) @@ -293,6 +278,7 @@ def index(): vs = avas[x["a"]] x.update({"v": vs}) except Exception as e: + app.logger.debug("Error during update of details") app.logger.debug(e) return redirect(url_for('error')) @@ -324,6 +310,7 @@ def login(): session['token'] = token return redirect(url_for('index')) except Exception as e: + app.logger.debug('login') app.logger.debug(e) return redirect(url_for('error')) return render_template('login.html') diff --git a/templates/portal.html b/templates/portal.html index d39ffcf..48f9371 100644 --- a/templates/portal.html +++ b/templates/portal.html @@ -27,10 +27,10 @@ - {% if multi == "true" %} + {% if multi %} {% for value in ava["v"] %} - {% if edit == "true" %} + {% if edit %}
@@ -47,7 +47,7 @@ {% endif %} {% endfor %}
- {% if edit == "true" %} + {% if edit %}
@@ -59,7 +59,7 @@ {% else %} {% set value = ava["v"][0] %} {% endif %} - {% if edit == "true" %} + {% if edit %}