#10 Ticket 2 - allow configuration of the portal
Merged 4 years ago by firstyear. Opened 4 years ago by firstyear.
firstyear/389-ds-portal 2-configuration  into  master

file modified
+1
@@ -14,3 +14,4 @@ 

  *.egg-info/

  .DS_Store

  

+ config.ini

file added
+66
@@ -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

file modified
+46 -59
@@ -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 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 @@ 

  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 @@ 

      # 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 @@ 

          # 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 @@ 

      try:

          inst.open()

      except Exception as e:

+         app.logger.debug('open()')

          app.logger.debug(e)

          return redirect(url_for('error'))

  
@@ -293,6 +278,7 @@ 

              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 @@ 

                  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')

file modified
+4 -4
@@ -27,10 +27,10 @@ 

                                                  <label for="input{{ attr }}">{{ attr }}</label>

                                          </td>

                                          <td>

-                                             {% if multi == "true" %}

+                                             {% if multi %}

                                                  <table id="{{ attr }}" class="w-100">

                                                      {% for value in ava["v"] %}

-                                                         {% if edit == "true" %}

+                                                         {% if edit %}

                                                              <tr id="{{ attr }}-{{ count[0] }}">

                                                                  <td>

                                                                      <input style="display: inline-block;" class="w-75" name="{{ attr }}[]" class="form-control" value="{{ value }}">
@@ -47,7 +47,7 @@ 

                                                          {% endif %}

                                                      {% endfor %}

                                                  </table>

-                                                 {% if edit == "true" %}

+                                                 {% if edit %}

                                                      <div class="d-flex justify-content-end">

                                                          <button type="button" class="btn btn-info" onclick="ds_add_row('{{ attr }}')" >+</button>

                                                      </div>
@@ -59,7 +59,7 @@ 

                                                      {% else %}

                                                          {% set value = ava["v"][0] %}

                                                      {% endif %}

-                                                         {% if edit == "true" %}

+                                                         {% if edit %}

                                                              <tr id="{{ attr }}-{{ count[0] }}">

                                                                  <td>

                                                                      <input style="display: inline-block;" class="w-75" name="{{ attr }}[]" class="form-control" value="{{ value }}">

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 william@blackhats.net.au

Review by: ???

Sorry I still can not get devel.sh to work with python3. Complains about flask, but I already have python3-flask installed.

$ ./devel.sh 
./devel.sh: line 2: flask: command not found

If I install python2-flask it starts (not sure how well it works), but this needs to really be working with python3 especially in RHEL 8.

Okay if I run devel.sh in a python venv it works as expected. I'll try and get this all reviewed asap!

Running with this patch I get an error when loading the page:

127.0.0.1 - - [29/Oct/2019 09:51:09] "GET / HTTP/1.1" 500 -
Traceback (most recent call last):
  File "/home/mareynol/source/389-ds-portal/server.py", line 23, in <module>
    app.secret_key = base64.b64decode(config['DEFAULT']['cookie_signing_key'])
  File "/usr/lib64/python3.7/configparser.py", line 1251, in __getitem__
    raise KeyError(key)
KeyError: 'cookie_signing_key'

Did you setup a configuration :)

Anyway, I probably should make the error handling "communicate better" in this case.

1 new commit added

  • Update example, and improve server to have better configuration errors
4 years ago

Okay, this makes the core errors clearer when you start the server:

> ./devel.sh
 * Serving Flask app "server" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: on
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
 * Restarting with stat
[2019-11-04 12:54:42,840] ERROR in server: Missing config.ini option dsportal:basedn

rebased onto daeb15f

4 years ago

Pull-Request has been merged by firstyear

4 years ago