diff --git a/README.rst b/README.rst index 1a580a8..5f9d9df 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,20 @@ Nice and simple application to manage users and groups in multiple directory services. +.. image:: https://travis-ci.org/kakwa/ldapcherry.svg?branch=master + :target: https://travis-ci.org/kakwa/ldapcherry + +.. image:: https://coveralls.io/repos/kakwa/ldapcherry/badge.svg + :target: https://coveralls.io/r/kakwa/ldapcherry + +.. image:: https://img.shields.io/pypi/dm/ldapcherry.svg + :target: https://pypi.python.org/pypi/ldapcherry + :alt: Number of PyPI downloads + +.. image:: https://img.shields.io/pypi/v/ldapcherry.svg + :target: https://pypi.python.org/pypi/ldapcherry + :alt: PyPI version + ---- :Doc: `ldapcherry documentation on ReadTheDoc `_ @@ -14,12 +28,6 @@ ---- -.. image:: https://travis-ci.org/kakwa/ldapcherry.svg?branch=master - :target: https://travis-ci.org/kakwa/ldapcherry - -.. image:: https://coveralls.io/repos/kakwa/ldapcherry/badge.svg - :target: https://coveralls.io/r/kakwa/ldapcherry - **************** Presentation **************** @@ -32,12 +40,12 @@ * roles management (as in "groups of groups") * autofill forms * password policy -* self modification of some selected fields by normal (non admin) users +* self modification of some selected fields by normal (non administrator) users * nice bootstrap interface -* modular through pluggable auth, password policy and backend modules +* modular through pluggable authentication, password policy and backend modules LdapCherry is not limited to ldap, it can handle virtually any user backend (ex: SQL database, htpasswd file, etc) -through the proper pluggin (provided that it is implemented ^^). +through the proper plugin (provided that it is implemented ^^). LdapCherry also aims to be as simple as possible to deploy: no crazy dependencies, few configuration files, extensive debug logs and full documentation. diff --git a/docs/assets/nature.css b/docs/assets/nature.css index f1e9316..a969f7a 100644 --- a/docs/assets/nature.css +++ b/docs/assets/nature.css @@ -158,10 +158,23 @@ div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 225%; color: #EEEEEE; background-color: #6f8c93;} div.body h2 { font-size: 140%; color: #EEEEEE; background-color: #6f8c93; } div.body h3 { font-size: 120%; color: #2C001E; background-color: #accbd3; } -div.body h4 { font-size: 100%; color: #EEEEEE; background-color: #accbd3; color: #000000} -div.body h5 { font-size: 100%; color: #EEEEEE; background-color: #accbd3; color: #000000} -div.body h6 { font-size: 100%; color: #EEEEEE; background-color: #accbd3; color: #000000} +div.body h4 { font-size: 100%; color: #000000; background-color: #accbd3; } +div.body h5 { font-size: 100%; color: #000000; background-color: #accbd3; } +div.body h6 { font-size: 100%; color: #000000; background-color: #accbd3; } +div.body dt { font-size: 110%; color: #2C001E; background-color: #accbd3;} + +.viewcode-link { + color: #000000; +} + +div.body tt { + color: #000000; + /* padding: 1px 2px; */ +} + + + a.headerlink { color: #333333; padding: 0 4px 0 4px; @@ -226,14 +239,6 @@ -moz-box-shadow: 1px 1px 1px #d8d8d8; } -tt { - background-color: #EFEFEF; - color: #222; - /* padding: 1px 2px; */ - font-size: 1.1em; - font-family: "Ubuntu Mono", Monaco, Consolas, "DejaVu Sans Mono", "Lucida Console", monospace; -} - .viewcode-back { font-family: Ubuntu, "DejaVu Sans", "Trebuchet MS", sans-serif; } diff --git a/docs/backend_api.rst b/docs/backend_api.rst index 9af8c00..7344769 100644 --- a/docs/backend_api.rst +++ b/docs/backend_api.rst @@ -1,18 +1,18 @@ -Implementing your own backend -============================= +Implementing cutom backends +=========================== API -~~~ +--- -To create your own backend, you must implement the following API: +The backend modules must respect the following API: -.. automodule:: ldapcherry.backend - :members: +.. autoclass:: ldapcherry.backend.Backend + :members: __init__, auth, add_user, del_user, set_attrs, add_to_groups, del_from_groups, search, get_user, get_groups :undoc-members: :show-inheritance: Configuration -~~~~~~~~~~~~~ +------------- Configuration for your backend is declared in the main ini file, inside [backends] section: @@ -42,13 +42,31 @@ 'param2': "my value 2", } +After having set **self.config** to **config** in the constructor, parameters can be recovered +by **self.get_param**: + +.. autoclass:: ldapcherry.backend.Backend + :members: get_param + :undoc-members: + :show-inheritance: + + Exceptions -~~~~~~~~~~ +---------- + The following exception can be used in your module -* -* -* -* +.. automodule:: ldapcherry.exceptions + :members: UserDoesntExist, UserAlreadyExists, GroupDoesntExist + :undoc-members: + :show-inheritance: These exceptions permit a nicer error handling and avoid a generic message to be thrown at the user. + +Example +------- + +Here is the ldap backend module that comes with LdapCherry: + +.. literalinclude:: ../ldapcherry/backend/backendLdap.py + :language: python diff --git a/docs/deploy.rst b/docs/deploy.rst index 3553d55..00a481d 100644 --- a/docs/deploy.rst +++ b/docs/deploy.rst @@ -92,6 +92,28 @@ backends: - : +Key attribute: +^^^^^^^^^^^^^^ + +One attribute must be used as a unique key across all backends: + +To set the key attribute, you must set **key** to **True** on this attribute. + +Example: + +.. sourcecode:: yaml + + uid: + description: "UID of the user" + display_name: "UID" + search_displayed: True + key: True # defining the attribute as "key" + type: string + weight: 50 + backends: + ldap: uid + ad: sAMAccountName + Authorize self modification ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -115,7 +137,7 @@ Autofill ^^^^^^^^ -LdapCherry has the possibility to autofill fields from other fields, +LdapCherry has the possibility to auto-fill fields from other fields, to use this functionnality **autofill** must be set. Example: @@ -139,12 +161,12 @@ backends: ldap: gidNumber -Arguments of the autofill function work as follow: +Arguments of the **autofill** function work as follow: * if argument starts with **$**, for example **$my_field**, the value of form input **my_field** will be passed to the function. * otherwise, it will be treated as a fixed argument. -Available autofill functions: +Available **autofill** functions: * lcUid: generate 8 characters uid from 2 other fields (first letter of the first field, 7 first letters of the second): @@ -205,6 +227,84 @@ Roles Configuration ~~~~~~~~~~~~~~~~~~~ +The roles configuration is done in a yaml file (roles.yml by default). + +Mandatory parameters +^^^^^^^^^^^^^^^^^^^^ + +Roles are seen as an aggregate of groups: + +.. sourcecode:: yaml + + : + display_name: + description: + backends_groups: # list of backends + : # list of groups in backend + - + - + : + - + - + +.. warning:: must be unique, LdapCherry won't start if it's not + +Defining LdapCherry Administrator role +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +One of the declared roles must be tagged to be LdapCherry administrators. + +Doing so is done by setting **LC_admins** to **True** for the selected role: + +.. sourcecode:: yaml + + : + display_name: + description: + + LC_admins: True + + backends_groups: # list of backends + : # list of groups in backend + - + - + : + - + - + +Nesting roles +^^^^^^^^^^^^^ + +LdapCherry handles roles nesting: + +.. sourcecode:: yaml + + parent_role: + display_name: Role parent + description: The parent role + backends_groups: + backend_id_1: + - b1_group_1 + - b1_group_2 + backend_id_2: + - b2_group_1 + - b2_group_2 + subroles: + child_role_1: + display_name: Child role 1 + description: The first Child Role + backends_groups: + backend_id_1: + - b1_group_3 + child_role_2: + display_name: Child role 2 + description: The second Child Role + backends_groups: + backend_id_1: + - b1_group_4 + +In that case, child_role_1 and child_role_2 will contain all groups of parent_role plus their own specific groups. + Main Configuration ------------------ @@ -259,6 +359,21 @@ Backends ~~~~~~~~ +Backends are configure in the **backends** section, the format is the following: + + +.. sourcecode:: ini + + [backends] + + # backend python module path + .module = 'python.module.path' + + # parameters of the module instance for backend . + . = + +It's possible to instanciate the same module several times. + Authentication and sessions ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -310,7 +425,7 @@ LdapCherry has two loggers, one for errors and applicative actions (login, del/add, logout...) and one for access logs. -Each logger can be configured to log to syslog, file or be desactivated. +Each logger can be configured to log to syslog, file or be disabled. Logging parameters: @@ -346,6 +461,12 @@ Other LdapCherry parameters ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ++---------------+-----------+--------------------------------+------------------------+ +| Parameter | Section | Description | Values | ++===============+===========+================================+========================+ +| template_dir | resources | LdapCherry template directory | path to template dir | ++---------------+-----------+--------------------------------+------------------------+ + .. sourcecode:: ini # resources parameters @@ -353,19 +474,3 @@ # templates directory template_dir = '/usr/share/ldapcherry/templates/' -LdapCherry full configuration file ----------------------------------- - -.. literalinclude:: ../conf/ldapcherry.ini - :language: ini - - -Init Script ------------ - -Sample init script for Debian: - -.. literalinclude:: ../goodies/init-debian - :language: bash - -This init script is available in **goodies/init-debian**. diff --git a/docs/forkme.rst b/docs/forkme.rst new file mode 100644 index 0000000..ec88f77 --- /dev/null +++ b/docs/forkme.rst @@ -0,0 +1,3 @@ +.. raw:: html + + Fork me on GitHub diff --git a/docs/full_configuration.rst b/docs/full_configuration.rst new file mode 100644 index 0000000..88fe3a8 --- /dev/null +++ b/docs/full_configuration.rst @@ -0,0 +1,23 @@ +Full Configuration +================== + +Main ini configuration file +--------------------------- + +.. literalinclude:: ../conf/ldapcherry.ini + :language: ini + + +Yaml Attributes configuration file +---------------------------------- + +.. literalinclude:: ../conf/attributes.yml + :language: yaml + + +Yaml Roles configuration file +----------------------------- + +.. literalinclude:: ../conf/roles.yml + :language: yaml + diff --git a/docs/goodies.rst b/docs/goodies.rst new file mode 100644 index 0000000..8028989 --- /dev/null +++ b/docs/goodies.rst @@ -0,0 +1,34 @@ +Some Goodies +============ + +Here are some goodies that might help deploying LdapCherry + +They are located in the **goodies/** directory. + +Init Script +----------- + +Sample init script for Debian: + +.. literalinclude:: ../goodies/init-debian + :language: bash + +This init script is available in **goodies/init-debian**. + +Apache Vhost +------------ + +.. literalinclude:: ../goodies/apache.conf + :language: xml + +Nginx Vhost +----------- + +.. literalinclude:: ../goodies/nginx.conf + :language: yaml + +Lighttpd Vhost +-------------- + +.. literalinclude:: ../goodies/lighttpd.conf + :language: yaml diff --git a/docs/index.rst b/docs/index.rst index 437deef..2193b7a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,8 +7,12 @@ install deploy + full_configuration backend_api ppolicy_api changelog + goodies .. include:: ../README.rst + +.. include:: forkme.rst diff --git a/docs/install.rst b/docs/install.rst index efc73e8..dc9371c 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -38,8 +38,8 @@ .. sourcecode:: bash - #optional, default sys.prefix (/usr/ on most Linux) - $ export DATAROOTDIR=/usr/local/ + #optional, default sys.prefix + 'share' (/usr/share/ on most Linux) + $ export DATAROOTDIR=/usr/local/share/ #optional, default /etc/ $ export SYSCONFDIR=/usr/local/etc/ diff --git a/docs/ppolicy_api.rst b/docs/ppolicy_api.rst index a6d2364..9679111 100644 --- a/docs/ppolicy_api.rst +++ b/docs/ppolicy_api.rst @@ -1,7 +1,34 @@ -Package ldapcherry.ppolicy -========================== +Implementing password policy modules +==================================== -.. automodule:: ldapcherry.ppolicy - :members: +API +--- + +The password policy modules must respect following API: + +.. autoclass:: ldapcherry.ppolicy.PPolicy + :members: check, info, __init__ :undoc-members: :show-inheritance: + +Configuration +------------- + +Parameters are declared in the main configuration file, inside the **ppolicy** section. + +After having set **self.config** to **config** in the constructor, parameters can be recovered +by **self.get_param**: + +.. autoclass:: ldapcherry.ppolicy.PPolicy + :members: get_param + :undoc-members: check + :show-inheritance: + +Example +------- + +Here is the simple default ppolicy module that comes with LdapCherry: + +.. literalinclude:: ../ldapcherry/ppolicy/simple.py + :language: python + diff --git a/goodies/apache.conf b/goodies/apache.conf new file mode 100644 index 0000000..2c0a6b7 --- /dev/null +++ b/goodies/apache.conf @@ -0,0 +1,8 @@ + + + + ProxyPass http://127.0.0.1:8080/ + ProxyPassReverse http://127.0.0.1:8080/ + + + diff --git a/goodies/init-debian b/goodies/init-debian new file mode 100755 index 0000000..1d8f056 --- /dev/null +++ b/goodies/init-debian @@ -0,0 +1,93 @@ +#! /bin/sh + +### BEGIN INIT INFO +# Provides: ldapcherryd +# Required-Start: $remote_fs $network $syslog +# Required-Stop: $remote_fs $network $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: +# Short-Description: ldapcherry +### END INIT INFO + +PIDFILE=/var/run/ldapcherryd/ldapcherryd.pid +CONF=/etc/ldapcherry/ldapcherry.ini +USER=www-data +GROUP=www-data +BIN=/usr/local/bin/ldapcherryd +OPTS="-d -c $CONF -p $PIDFILE" + +. /lib/lsb/init-functions + +if [ -f /etc/default/ldapcherryd ]; then + . /etc/default/ldapcherryd +fi + +start_ldapcherryd(){ + log_daemon_msg "Starting ldapcherryd" "ldapcherryd" || true + pidofproc -p $PIDFILE $BIN >/dev/null + status="$?" + if [ $status -eq 0 ] + then + log_end_msg 1 + log_failure_msg \ + "ldapcherryd already started" + return 1 + fi + mkdir -p `dirname $PIDFILE` -m 750 + chown $USER:$GROUP `dirname $PIDFILE` + if start-stop-daemon -c $USER:$GROUP --start \ + --quiet --pidfile $PIDFILE \ + --oknodo --exec $BIN -- $OPTS + then + log_end_msg 0 || true + return 0 + else + log_end_msg 1 || true + return 1 + fi + +} + +stop_ldapcherryd(){ + log_daemon_msg "Stopping ldapcherryd" "ldapcherryd" || true + if start-stop-daemon --stop --quiet \ + --pidfile $PIDFILE + then + log_end_msg 0 || true + return 0 + else + log_end_msg 1 || true + return 1 + fi +} + +case "$1" in + start) + start_ldapcherryd + exit $? + ;; + stop) + stop_ldapcherryd + exit $? + ;; + restart) + stop_ldapcherryd + while pidofproc -p $PIDFILE $BIN >/dev/null + do + sleep 0.5 + done + start_ldapcherryd + exit $? + ;; + status) + status_of_proc -p $PIDFILE $BIN "ldapcherryd" \ + && exit 0 || exit $? + ;; + *) + log_action_msg \ + "Usage: /etc/init.d/ldapcherryd {start|stop|restart|status}" \ + || true + exit 1 +esac + +exit 0 diff --git a/goodies/lighttpd.conf b/goodies/lighttpd.conf new file mode 100644 index 0000000..e5173ad --- /dev/null +++ b/goodies/lighttpd.conf @@ -0,0 +1,7 @@ +server.modules += ("mod_proxy") + +$HTTP["host"] == "ldapcherry.kakwa.fr" { + proxy.server = ( "" => + (( "host" => "127.0.0.1", "port" => 8080 )) + ) +} diff --git a/goodies/nginx.conf b/goodies/nginx.conf new file mode 100644 index 0000000..9789f93 --- /dev/null +++ b/goodies/nginx.conf @@ -0,0 +1,14 @@ +server { + listen 80 default_server; + + server_name $hostname; + #access_log /var/log/nginx/dnscherry_access_log; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for; + proxy_set_header Host $host:$server_port; + proxy_set_header X-Forwarded-Proto $remote_addr; + } +} diff --git a/ldapcherry/backend/__init__.py b/ldapcherry/backend/__init__.py index 1957589..77a6ed1 100644 --- a/ldapcherry/backend/__init__.py +++ b/ldapcherry/backend/__init__.py @@ -11,36 +11,124 @@ class Backend: def __init__(self, config, logger, name, attrslist, key): + """ Initialize the backend + + :param config: the configuration of the backend + :type config: dict {'config key': 'value'} + :param logger: the cherrypy error logger object + :type logger: python logger + :param name: id of the backend + :type name: string + :param attrslist: list of the backend attributes + :type attrslist: list of strings + :param key: the key attribute + :type key: string + """ raise Exception() def auth(self, username, password): + """ Check authentication against the backend + + :param username: 'key' attribute of the user + :type username: string + :param password: password of the user + :type password: string + :rtype: boolean (True is authentication success, False otherwise) + """ return False def add_user(self, attrs): + """ Add a user to the backend + + :param attrs: attributes of the user + :type attrs: dict ({: }) + + .. warning:: raise UserAlreadyExists if user already exists + """ pass def del_user(self, username): + """ Delete a user from the backend + + :param username: 'key' attribute of the user + :type username: string + + """ pass def set_attrs(self, username, attrs): + """ Set a list of attributes for a given user + + :param username: 'key' attribute of the user + :type username: string + :param attrs: attributes of the user + :type attrs: dict ({: }) + """ pass def add_to_groups(self, username, groups): + """ Add a user to a list of groups + + :param username: 'key' attribute of the user + :type username: string + :param groups: list of groups + :type groups: list of strings + """ pass def del_from_groups(self, username, groups): + """ Delete a user from a list of groups + + :param username: 'key' attribute of the user + :type username: string + :param groups: list of groups + :type groups: list of strings + + .. warning:: raise GroupDoesntExist if group doesn't exist + """ pass def search(self, searchstring): + """ Search backend for users + + :param searchstring: the search string + :type searchstring: string + :rtype: dict of dict ( {: {: }} ) + """ return {} def get_user(self, username): + """ Get a user's attributes + + :param username: 'key' attribute of the user + :type username: string + :rtype: dict ( {: } ) + + .. warning:: raise UserDoesntExist if user doesn't exist + """ return {} def get_groups(self, username): + """ Get a user's groups + + :param username: 'key' attribute of the user + :type username: string + :rtype: list of groups + """ return [] def get_param(self, param, default=None): + """ Get a parameter in config (handle default value) + + :param param: name of the parameter to recover + :type param: string + :param default: the default value, raises an exception + if param is not in configuration and default + is None (which is the default value). + :type default: string or None + :rtype: the value of the parameter or the default value if + not set in configuration + """ if param in self.config: return self.config[param] elif default is not None: diff --git a/ldapcherry/ppolicy/__init__.py b/ldapcherry/ppolicy/__init__.py index 5b4f066..4461487 100644 --- a/ldapcherry/ppolicy/__init__.py +++ b/ldapcherry/ppolicy/__init__.py @@ -13,15 +13,18 @@ def __init__(self, config, logger): """ Password policy constructor - :dict config: the configuration of the ppolicy - :logger logger: a python logger + :param config: the configuration of the ppolicy + :type config: dict {'config key': 'value'} + :param logger: the cherrypy error logger object + :type logger: python logger """ pass def check(self, password): """ Check if a password match the ppolicy - :str password: the password to check + :param password: the password to check + :type password: string :rtype: dict with keys 'match' a boolean (True if ppolicy matches, False otherwise) and 'reason', an explaination string @@ -30,7 +33,7 @@ return ret def info(self): - """ Gives information about the ppolicy + """ Give information about the ppolicy :rtype: a string describing the ppolicy """ @@ -39,10 +42,14 @@ def get_param(self, param, default=None): """ Get a parameter in config (handle default value) - :str param: name of the paramter to recover - :str default: the default value, raises an exception + :param param: name of the parameter to recover + :type param: string + :param default: the default value, raises an exception if param is not in configuration and default is None (which is the default value). + :type default: string or None + :rtype: the value of the parameter or the default value if + not set in configuration """ if param in self.config: return self.config[param] diff --git a/setup.py b/setup.py index 4b6b317..7a63178 100755 --- a/setup.py +++ b/setup.py @@ -136,6 +136,9 @@ 'Operating System :: POSIX', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', - 'Topic :: Internet :: LDAP' + "Topic :: System :: Systems Administration" + " :: Authentication/Directory :: LDAP", + "Topic :: System :: Systems Administration" + " :: Authentication/Directory", ], )