From 7dd5bc685e5dca562d0d7757348bd51141e7731b Mon Sep 17 00:00:00 2001 From: Laurent Mignon Date: Fri, 25 Jul 2014 15:32:53 +0200 Subject: [PATCH] [ADD] This module initialize the session by looking for the field HTTP_REMOTE_USER in the HEADER of the HTTP request and trying^Co bind the given value to a user --- auth_from_http_remote_user/__init__.py | 24 ++++ auth_from_http_remote_user/__openerp__.py | 133 ++++++++++++++++++ .../controllers/__init__.py | 22 +++ .../controllers/session.py | 129 +++++++++++++++++ auth_from_http_remote_user/res_config.py | 60 ++++++++ .../res_config_data.xml | 9 ++ .../res_config_view.xml | 39 +++++ auth_from_http_remote_user/res_users.py | 64 +++++++++ .../src/js/auth_from_http_remote_user.js | 36 +++++ auth_from_http_remote_user/tests/__init__.py | 28 ++++ .../tests/test_res_users.py | 83 +++++++++++ auth_from_http_remote_user/utils.py | 22 +++ 12 files changed, 649 insertions(+) create mode 100644 auth_from_http_remote_user/__init__.py create mode 100644 auth_from_http_remote_user/__openerp__.py create mode 100644 auth_from_http_remote_user/controllers/__init__.py create mode 100644 auth_from_http_remote_user/controllers/session.py create mode 100644 auth_from_http_remote_user/res_config.py create mode 100644 auth_from_http_remote_user/res_config_data.xml create mode 100644 auth_from_http_remote_user/res_config_view.xml create mode 100644 auth_from_http_remote_user/res_users.py create mode 100644 auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js create mode 100644 auth_from_http_remote_user/tests/__init__.py create mode 100644 auth_from_http_remote_user/tests/test_res_users.py create mode 100644 auth_from_http_remote_user/utils.py diff --git a/auth_from_http_remote_user/__init__.py b/auth_from_http_remote_user/__init__.py new file mode 100644 index 000000000..6e0a37c8b --- /dev/null +++ b/auth_from_http_remote_user/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import controllers +from . import res_config +from . import res_users diff --git a/auth_from_http_remote_user/__openerp__.py b/auth_from_http_remote_user/__openerp__.py new file mode 100644 index 000000000..317cdb073 --- /dev/null +++ b/auth_from_http_remote_user/__openerp__.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + 'name': 'Authenticate via HTTP Remote User', + 'version': '1.0', + 'category': 'Tools', + 'description': """ +Allow users to be automatically logged in. +========================================== + +This module initialize the session by looking for the field HTTP_REMOTE_USER in +the HEADER of the HTTP request and trying to bind the given value to a user +This module must be loaded at startup; Add the *--load* parameter to the startup +command: :: + + --load=web,web_kanban,auth_from_http_remote_user, ... + +If the field is not found or no user matches the given one, it can lets the +system redirect to the login page (default) or issue a login error page depending +of the configuration. + +How to test the module with Apache [#]_ +---------------------------------------- + +Apache can be used as a reverse proxy providing the authentication and adding the +required field in the Http headers. + +Install apache: :: + + $ sudo apt-get install apache2 + + +Define a new vhost to Apache by putting a new file in /etc/apache2/sites-available: :: + + $ sudo vi /etc/apache2/sites-available/MY_VHOST.com + +with the following content: :: + + + ServerName MY_VHOST.com + ProxyRequests Off + + AuthType Basic + AuthName "Test OpenErp auth_from_http_remote_user" + AuthBasicProvider file + AuthUserFile /etc/apache2/MY_VHOST.htpasswd + Require valid-user + + RewriteEngine On + RewriteCond %{LA-U:REMOTE_USER} (.+) + RewriteRule . - [E=RU:%1] + RequestHeader set Remote-User "%{RU}e" env=RU + + + ProxyPass / http://127.0.0.1:8069/ retry=10 + ProxyPassReverse / http://127.0.0.1:8069/ + ProxyPreserveHost On + + +.. important:: The *RequestHeader* directive is used to add the *Remote-User* field + in the http headers. By default an *'Http-'* prefix is added to the field name. + In OpenErp, header's fields name are normalized. As result of this normalization, + the 'Http-Remote-User' is available as 'HTTP_REMOTE_USER'. If you don't know how + your specified field is seen by OpenErp, run your server in debug mode once the + module is activated and look for an entry like: :: + + DEBUG openerp1 openerp.addons.auth_from_http_remote_user.controllers.session: + Field 'HTTP_MY_REMOTE_USER' not found in http headers + {'HTTP_AUTHORIZATION': 'Basic YWRtaW46YWRtaW4=', ..., 'HTTP_REMOTE_USER': 'demo') + +Enable the required apache modules: :: + + $ sudo a2enmod headers + $ sudo a2enmod proxy + $ sudo a2enmod rewrite + $ sudo a2enmod proxy_http + +Enable your new vhost: :: + + $ sudo a2ensite MY_VHOST.com + +Create the *htpassword* file used by the configured basic authentication: :: + + $ sudo htpasswd -cb /etc/apache2/MY_VHOST.htpasswd admin admin + $ sudo htpasswd -b /etc/apache2/MY_VHOST.htpasswd demo demo + +For local test, add the *MY_VHOST.com* in your /etc/vhosts file. + +Finally reload the configuration: :: + + $ sudo service apache2 reload + +Open your browser and go to MY_VHOST.com. If everything is well configured, you are prompted +for a login and password outside OpenErp and are automatically logged in the system. + +.. [#] Based on a ubuntu 12.04 env + +""", + 'author': 'Acsone SA/NV', + 'maintainer': 'ACSONE SA/NV', + 'website': 'http://www.acsone.eu', + 'depends': ['web'], + "license": "AGPL-3", + "js": ['static/src/js/auth_from_http_remote_user.js'], + 'data': [ + 'res_config_view.xml', + 'res_config_data.xml'], + "demo": [], + "test": [], + "active": False, + "license": "AGPL-3", + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/auth_from_http_remote_user/controllers/__init__.py b/auth_from_http_remote_user/controllers/__init__.py new file mode 100644 index 000000000..7705efcef --- /dev/null +++ b/auth_from_http_remote_user/controllers/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import session diff --git a/auth_from_http_remote_user/controllers/session.py b/auth_from_http_remote_user/controllers/session.py new file mode 100644 index 000000000..f1dc0450a --- /dev/null +++ b/auth_from_http_remote_user/controllers/session.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import SUPERUSER_ID + +from openerp.addons.web import http +from openerp.addons.web.controllers import main +from openerp.modules.registry import RegistryManager +from .. import utils + +import random +import logging +import openerp.tools.config as config + +_logger = logging.getLogger(__name__) + + +class Session(main.Session): + _cp_path = "/web/session" + + _REQUIRED_ATTRIBUTES = ['HTTP_REMOTE_USER'] + _OPTIONAL_ATTRIBUTES = [] + + def _get_db(self, db): + if db is not None and len(db) > 0: + return db + db = config['db_name'] + if db is None or len(db) == 0: + _logger.error("No db found for SSO. Specify one in the URL using parameter " + "db=? or provide a default one in the configuration") + raise http.AuthenticationError() + + def _get_user_id_from_attributes(self, res_users, cr, attrs): + login = attrs.get('HTTP_REMOTE_USER', None) + user_ids = res_users.search(cr, SUPERUSER_ID, [('login', '=', login), ('active', '=', True)]) + assert len(user_ids) < 2 + if user_ids: + return user_ids[0] + return None + + def _get_attributes_form_header(self, req): + attrs = {} + + all_attrs = self._REQUIRED_ATTRIBUTES + self._OPTIONAL_ATTRIBUTES + + headers = req.httprequest.headers.environ + + for attr in all_attrs: + value = headers.get(attr, None) + if value is not None: + attrs[attr] = value + + attrs_found = set(attrs.keys()) + attrs_missing = set(all_attrs) - attrs_found + if len(attrs_found) > 0: + _logger.debug("Fields '%s' not found in http headers\n %s", attrs_missing, headers) + + missings = set(self._REQUIRED_ATTRIBUTES) - attrs_found + if len(missings) > 0: + _logger.error("Required fields '%s' not found in http headers\n %s", missings, headers) + return attrs + + def _bind_http_remote_user(self, req, db_name): + db_name = self._get_db(db_name) + try: + registry = RegistryManager.get(db_name) + with registry.cursor() as cr: + modules = registry.get('ir.module.module') + installed = modules.search_count(cr, SUPERUSER_ID, ['&', + ('name', '=', 'auth_from_http_remote_user'), + ('state', '=', 'installed')]) == 1 + if not installed: + return + config = registry.get('auth_from_http_remote_user.config.settings') + # get parameters for SSO + default_login_page_disabled = config.is_default_login_page_disabled(cr, SUPERUSER_ID, None) + + # get the user + res_users = registry.get('res.users') + attrs = self._get_attributes_form_header(req) + user_id = self._get_user_id_from_attributes(res_users, cr, attrs) + + if user_id is None: + if default_login_page_disabled: + raise http.AuthenticationError() + return + + # generate a specific key for authentication + key = randomString(utils.KEY_LENGTH, '0123456789abcdef') + res_users.write(cr, SUPERUSER_ID, [user_id], {'sso_key': key}) + login = res_users.browse(cr, SUPERUSER_ID, user_id).login + req.session.bind(db_name, user_id, login, key) + except http.AuthenticationError, e: + raise e + except Exception, e: + _logger.error("Error binding Http Remote User session", exc_info=True) + raise e + + @http.jsonrequest + def get_http_remote_user_session_info(self, req, db): + if not req.session._login: + self._bind_http_remote_user(req, db) + return self.session_info(req) + +randrange = random.SystemRandom().randrange + + +def randomString(length, chrs): + """Produce a string of length random bytes, chosen from chrs.""" + n = len(chrs) + return ''.join([chrs[randrange(n)] for _ in xrange(length)]) diff --git a/auth_from_http_remote_user/res_config.py b/auth_from_http_remote_user/res_config.py new file mode 100644 index 000000000..5cbc9082d --- /dev/null +++ b/auth_from_http_remote_user/res_config.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import orm, fields +from openerp.tools.safe_eval import safe_eval +import types + + +class auth_from_http_remote_user_configuration(orm.TransientModel): + _name = 'auth_from_http_remote_user.config.settings' + _inherit = 'res.config.settings' + + _columns = { + 'default_login_page_disabled': fields.boolean("Disable login page", + help=""" +Disable the default login page. +If the HTTP_REMOTE_HEADER field is not found or no user matches the given one, +the system will display a login error page if the login page is disabled. +Otherwise the normal login page will be displayed. + """), + } + + def is_default_login_page_disabled(self, cr, uid, fields, context=None): + ir_config_obj = self.pool['ir.config_parameter'] + default_login_page_disabled = ir_config_obj.get_param(cr, + uid, + 'auth_from_http_remote_user.default_login_page_disabled') + if isinstance(default_login_page_disabled, types.BooleanType): + return default_login_page_disabled + return safe_eval(default_login_page_disabled) + + def get_default_default_login_page_disabled(self, cr, uid, fields, context=None): + default_login_page_disabled = self.is_default_login_page_disabled(cr, uid, fields, context) + return {'default_login_page_disabled': default_login_page_disabled} + + def set_default_default_login_page_disabled(self, cr, uid, ids, context=None): + config = self.browse(cr, uid, ids[0], context) + ir_config_parameter_obj = self.pool['ir.config_parameter'] + ir_config_parameter_obj.set_param(cr, + uid, + 'auth_from_http_remote_user.default_login_page_disabled', + repr(config.default_login_page_disabled)) diff --git a/auth_from_http_remote_user/res_config_data.xml b/auth_from_http_remote_user/res_config_data.xml new file mode 100644 index 000000000..ba9a2b1c1 --- /dev/null +++ b/auth_from_http_remote_user/res_config_data.xml @@ -0,0 +1,9 @@ + + + + + auth_from_http_remote_user.default_login_page_disabled + False + + + diff --git a/auth_from_http_remote_user/res_config_view.xml b/auth_from_http_remote_user/res_config_view.xml new file mode 100644 index 000000000..3869cb4ac --- /dev/null +++ b/auth_from_http_remote_user/res_config_view.xml @@ -0,0 +1,39 @@ + + + + + Auth HTTP_REMOTE_USER settings + auth_from_http_remote_user.config.settings + + +
+
+
+
+
+ + + + + +
+
+ + + Configure Auth HTTP_REMOTE_USER + ir.actions.act_window + auth_from_http_remote_user.config.settings + form + inline + + + + +
+
diff --git a/auth_from_http_remote_user/res_users.py b/auth_from_http_remote_user/res_users.py new file mode 100644 index 000000000..fef91596d --- /dev/null +++ b/auth_from_http_remote_user/res_users.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.modules.registry import RegistryManager +from openerp.osv import orm, fields +from openerp import SUPERUSER_ID +import openerp.exceptions +from openerp.addons.auth_from_http_remote_user import utils + + +class res_users(orm.Model): + _inherit = 'res.users' + + _columns = { + 'sso_key': fields.char('SSO Key', size=utils.KEY_LENGTH, + readonly=True), + } + + def copy(self, cr, uid, rid, defaults=None, context=None): + defaults = defaults or {} + defaults['sso_key'] = False + return super(res_users, self).copy(cr, uid, rid, defaults, context) + + def check_credentials(self, cr, uid, password): + try: + return super(res_users, self).check_credentials(cr, uid, password) + except openerp.exceptions.AccessDenied: + res = self.search(cr, SUPERUSER_ID, [('id', '=', uid), ('sso_key', '=', password)]) + if not res: + raise openerp.exceptions.AccessDenied() + + def check(self, db, uid, passwd): + try: + return super(res_users, self).check(db, uid, passwd) + except openerp.exceptions.AccessDenied: + if not passwd: + raise + with RegistryManager.get(db).cursor() as cr: + cr.execute('''SELECT COUNT(1) + FROM res_users + WHERE id=%s + AND sso_key=%s + AND active=%s''', (int(uid), passwd, True)) + if not cr.fetchone()[0]: + raise + self._uid_cache.setdefault(db, {})[uid] = passwd diff --git a/auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js b/auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js new file mode 100644 index 000000000..df34118cf --- /dev/null +++ b/auth_from_http_remote_user/static/src/js/auth_from_http_remote_user.js @@ -0,0 +1,36 @@ +openerp.auth_from_http_remote_user = function(instance) { + + instance.web.Session.include({ + session_load_response : function(response) { + //unregister the event since it must be called only if the rpc call + //is made by session_reload + this.off('response', this.session_load_response); + if (response.error && response.error.data.type === "session_invalid") { + $("body").html("

Access Denied

"); + } + + console.log("session_load_response called"); + }, + + session_reload : function() { + var self = this; + // we need to register an handler for 'response' since + // by default, the rpc doesn't call callback function + // if the response is of error type 'session_invalid' + this.on('response', this, this.session_load_response); + return this.rpc("/web/session/get_http_remote_user_session_info", { + db : $.deparam.querystring().db + }).done(function(result) { + // If immediately follows a login (triggered by trying to + // restore + // an invalid session or no session at all), refresh session + // data + // (should not change, but just in case...) + _.extend(self, result); + }).fail(function(result){ + $("body").html("

Server error

"); + }); + } + }); + +}; \ No newline at end of file diff --git a/auth_from_http_remote_user/tests/__init__.py b/auth_from_http_remote_user/tests/__init__.py new file mode 100644 index 000000000..bf16e2154 --- /dev/null +++ b/auth_from_http_remote_user/tests/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import test_res_users +fast_suite = [ +] + +checks = [ + test_res_users, +] diff --git a/auth_from_http_remote_user/tests/test_res_users.py b/auth_from_http_remote_user/tests/test_res_users.py new file mode 100644 index 000000000..e2ba2f890 --- /dev/null +++ b/auth_from_http_remote_user/tests/test_res_users.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.tests import common +import mock +import os +from contextlib import contextmanager +import unittest + + +@contextmanager +def mock_cursor(cr): + with mock.patch('openerp.sql_db.Connection.cursor') as mocked_cursor_call: + org_close = cr.close + org_autocommit = cr.autocommit + try: + cr.close = mock.Mock() + cr.autocommit = mock.Mock() + mocked_cursor_call.return_value = cr + yield + finally: + cr.close = org_close + cr.autocommit = org_autocommit + + +class test_res_users(common.TransactionCase): + + def test_login(self): + res_users_obj = self.registry('res.users') + uid = res = res_users_obj.login(common.DB, 'admin', 'admin') + self.assertTrue(res, "Basic login must works as expected") + token = "123456" + res = res_users_obj.login(common.DB, 'admin', token) + self.assertFalse(res) + # mimic what the new controller do when it find a value in + # the http header (HTTP_REMODE_USER) + res_users_obj.write(self.cr, self.uid, uid, {'sso_key': token}) + + # Here we need to mock the cursor since the login is natively done inside + # its own connection + with mock_cursor(self.cr): + # We can verifies that the given (uid, token) is authorized for the database + res_users_obj.check(common.DB, uid, token) + + # we are able to login with the new token + res = res_users_obj.login(common.DB, 'admin', token) + self.assertTrue(res) + + @unittest.skipIf(os.environ.get('TRAVIS'), + 'When run by travis, tests runs on a database with all required addons from server-tools and ' + 'their dependencies installed. Even if `auth_from_http_remote_user` does not require the `mail`' + 'module, The previous installation of the mail module has created the column ' + '`notification_email_send` as REQUIRED into the table res_partner. BTW, it\'s no more possible ' + 'to copy a res_user without an intefirty error') + def test_copy(self): + '''Check that the sso_key is not copied on copy + ''' + res_users_obj = self.registry('res.users') + vals = {'sso_key': '123'} + res_users_obj.write(self.cr, self.uid, self.uid, vals) + read_vals = res_users_obj.read(self.cr, self.uid, self.uid, ['sso_key']) + self.assertDictContainsSubset(vals, read_vals) + copy = res_users_obj.copy(self.cr, self.uid, self.uid) + read_vals = res_users_obj.read(self.cr, self.uid, copy, ['sso_key']) + self.assertFalse(read_vals.get('sso_key')) diff --git a/auth_from_http_remote_user/utils.py b/auth_from_http_remote_user/utils.py new file mode 100644 index 000000000..ee1eacf68 --- /dev/null +++ b/auth_from_http_remote_user/utils.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Author: Laurent Mignon +# Copyright 2014 'ACSONE SA/NV' +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +KEY_LENGTH = 16