diff --git a/.gitignore b/.gitignore index b9ae4ff..f2aee66 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ Thumbs.db Desktop.ini .ropeproject +_build/ diff --git a/README.rst b/README.rst index a7fff78..97c5c6d 100644 --- a/README.rst +++ b/README.rst @@ -4,49 +4,8 @@ gvaldap This is the GNUViech Admin LDAP administration tool project. -Working Environment -=================== +GNUViech Admin is a suite of tools for server management used for hosting +customer management at `Jan Dittberner IT-Consulting & -Solutions +`_. -You have several options in setting up your working environment. We recommend -using virtualenv to separate the dependencies of your project from your -system's python environment. If on Linux or Mac OS X, you can also use -virtualenvwrapper to help manage multiple virtualenvs across different -projects. - -Virtualenv Only ---------------- - -First, make sure you are using virtualenv (http://www.virtualenv.org). Once -that's installed, create your virtualenv:: - - $ virtualenv --distribute gvaldap - -You will also need to ensure that the virtualenv has the project directory -added to the path. Adding the project directory will allow `django-admin.py` to -be able to change settings using the `--settings` flag. - -Virtualenv with virtualenvwrapper ------------------------------------- - -In Linux and Mac OSX, you can install virtualenvwrapper -(http://virtualenvwrapper.readthedocs.org/en/latest/), which will take care of -managing your virtual environments and adding the project path to the -`site-directory` for you:: - - $ mkdir gvaldap - $ mkvirtualenv -a gvaldap gvaldap-dev - $ cd gvaldap && add2virtualenv `pwd` - - -Installation of Dependencies -============================= - -Depending on where you are installing dependencies: - -In development:: - - $ pip install -r requirements/local.txt - -For production:: - - $ pip install -r requirements.txt +Read the :doc:`Installation instructions ` to get started locally. diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..db3d09e --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,4 @@ +Changelog +========= + +* :feature:`-` intial support for creating LDAP users and groups diff --git a/docs/code.rst b/docs/code.rst new file mode 100644 index 0000000..9001a7b --- /dev/null +++ b/docs/code.rst @@ -0,0 +1,97 @@ +================== +Code documentation +================== + +.. index:: Django + +gvaldap is implemented as `Django`_ project and provides some `Celery`_ tasks. + +.. _Django: https://www.djangoproject.com/ +.. _Celery: http://www.celeryproject.org/ + + +The project module :py:mod:`gvaldap` +==================================== + +.. automodule:: gvaldap + + +:py:mod:`gvaldap.celery` +------------------------ + +.. automodule:: gvaldap.celery + :members: + + +:py:mod:`gvaldap.urls` +---------------------- + +.. automodule:: gvaldap.urls + + +:py:mod:`gvaldap.wsgi` +---------------------- + +.. automodule:: gvaldap.wsgi + :members: + + +:py:mod:`gvaldap.settings` +-------------------------- + +.. automodule:: gvaldap.settings + +:py:mod:`gvaldap.settings.base` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: gvaldap.settings.base + :members: + +:py:mod:`gvaldap.settings.local` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: gvaldap.settings.local + +:py:mod:`gvaldap.settings.production` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: gvaldap.settings.production + +:py:mod:`gvaldap.settings.test` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: gvaldap.settings.test + + + +:py:mod:`ldapentities` app +========================== + +.. automodule:: ldapentities + + +:py:mod:`ldapenties.admin` +-------------------------- + +.. automodule:: ldapentities.admin + :members: + + +:py:mod:`ldapenties.models` +--------------------------- + +.. automodule:: ldapentities.models + :members: + + +:py:mod:`osusers` app +===================== + +.. automodule:: osusers + +:py:mod:`osusers.tasks` +----------------------- + +.. automodule:: osusers.tasks + :members: + :undoc-members: diff --git a/docs/conf.py b/docs/conf.py index c0cb2c1..472a042 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,12 +12,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -#import sys, os +import sys +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +sys.path.insert(0, os.path.abspath(os.path.join('..', 'gvaldap'))) # -- General configuration ----------------------------------------------------- @@ -26,11 +27,15 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [] +extensions = ['releases', 'sphinx.ext.autodoc', 'celery.contrib.sphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] +releases_issue_uri = 'https://dev.gnuviech-server.de/gvaldap/ticket/%s' + +releases_release_uri = 'https://dev.gnuviech-server.de/gvaldap/milestone/%s' + # The suffix of source filenames. source_suffix = '.rst' diff --git a/docs/deploy.rst b/docs/deploy.rst index 1e642c7..9fd944b 100644 --- a/docs/deploy.rst +++ b/docs/deploy.rst @@ -1,4 +1,10 @@ Deploy ======== -This is where you describe how the project is deployed in production. +The production deployment for gvaldap is performed using saltstack and consists +of the following steps: + +* installation of native dependencies +* setup of a virtualenv +* installation of gvaldap production dependencies inside the virtualenv +* setup of celery worker under control of supervisord diff --git a/docs/index.rst b/docs/index.rst index 3267489..0297002 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,6 +3,8 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +.. include:: ../README.rst + Welcome to gvaldap's documentation! ==================================== @@ -13,8 +15,8 @@ Contents: install deploy - tests - + code + changelog Indices and tables diff --git a/docs/install.rst b/docs/install.rst index 7ebed79..23a17ee 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,4 +1,84 @@ +.. index:: installation + +======= Install ======= -This is where you write how to get a new laptop to run this project. +Working Environment +=================== + +You have several options in setting up your working environment. We recommend +using virtualenv to separate the dependencies of your project from your +system's python environment. If on Linux or Mac OS X, you can also use +virtualenvwrapper to help manage multiple virtualenvs across different +projects. + +.. index:: virtualenv + +Virtualenv Only +--------------- + +First, make sure you are using `virtualenv`_. Once that's installed, create +your virtualenv: + +.. code-block:: sh + + $ virtualenv --distribute gvaldap + +.. _virtualenv: https://virtualenv.pypa.io/en/latest/ + +You will also need to ensure that the virtualenv has the project directory +added to the path. Adding the project directory will allow `django-admin.py` to +be able to change settings using the `--settings` flag. + +.. index:: virtualenvwrapper + +Virtualenv with virtualenvwrapper +------------------------------------ + +In Linux and Mac OSX, you can install `virtualenvwrapper +`_, which will take care +of managing your virtual environments and adding the project path to the +`site-directory` for you: + +.. code-block:: sh + + $ mkdir gvaldap + $ mkvirtualenv -a gvaldap gvaldap-dev + $ cd gvaldap && add2virtualenv `pwd` + + +.. index:: pip, requirements, dependencies + +Installation of Dependencies +============================= + +Depending on where you are installing dependencies: + +In development: + +.. code-block:: sh + + $ pip install -r requirements/local.txt + +For production: + +.. code-block:: sh + + $ pip install -r requirements.txt + +.. index:: celery, worker, ldap queue + +Running the Celery worker +========================= + +gvaldap uses the `Celery`_ distributed task queue system. The gvaldap logix is +executed by a celery worker. After all dependencies are installed you can go +into the gvaldap directory and run the celery worker with: + +.. code-block:: sh + + $ cd gvaldap + $ celery -A gvaldap worker -Q ldap -l info + +.. _Celery: http://www.celeryproject.org/ diff --git a/gvaldap/gvaldap/__init__.py b/gvaldap/gvaldap/__init__.py index e69de29..62a61ef 100644 --- a/gvaldap/gvaldap/__init__.py +++ b/gvaldap/gvaldap/__init__.py @@ -0,0 +1,3 @@ +""" +This is the gvaldap project module. +""" diff --git a/gvaldap/gvaldap/celery.py b/gvaldap/gvaldap/celery.py index fd9a616..e9a2f75 100644 --- a/gvaldap/gvaldap/celery.py +++ b/gvaldap/gvaldap/celery.py @@ -1,3 +1,9 @@ +""" +This module defines the Celery_ app for gvaldap. + +.. _Celery: http://www.celeryproject.org/ + +""" from __future__ import absolute_import import os @@ -10,12 +16,8 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gvaldap.settings.production') +#: The Celery application app = Celery('gvaldap') app.config_from_object('django.conf:settings') app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) - - -@app.task(bind=True) -def debug_task(self): - print('Request: {0!r}'.format(self.request)) diff --git a/gvaldap/gvaldap/settings/__init__.py b/gvaldap/gvaldap/settings/__init__.py index 8b13789..4f53a5e 100644 --- a/gvaldap/gvaldap/settings/__init__.py +++ b/gvaldap/gvaldap/settings/__init__.py @@ -1 +1,3 @@ - +""" +This module contains settings for various environments. +""" diff --git a/gvaldap/gvaldap/settings/base.py b/gvaldap/gvaldap/settings/base.py index 70de0e7..46c8740 100644 --- a/gvaldap/gvaldap/settings/base.py +++ b/gvaldap/gvaldap/settings/base.py @@ -1,5 +1,9 @@ -"""Common settings and globals.""" +# -*- coding: utf-8 -*- # pymode:lint_ignore=E501 +""" +Common settings and globals. + +""" from os.path import abspath, basename, dirname, join, normpath @@ -12,7 +16,14 @@ from django.core.exceptions import ImproperlyConfigured def get_env_setting(setting): - """ Get the environment setting or return exception """ + """ + Get the environment setting or return exception. + + :param str setting: name of an environment setting + :raises ImproperlyConfigured: if the environment setting is not defined + :return: environment setting value + :rtype: str + """ try: return environ[setting] except KeyError: diff --git a/gvaldap/gvaldap/settings/local.py b/gvaldap/gvaldap/settings/local.py index 3329842..8a8d441 100644 --- a/gvaldap/gvaldap/settings/local.py +++ b/gvaldap/gvaldap/settings/local.py @@ -1,5 +1,8 @@ -"""Development settings and globals.""" # pymode:lint_ignore=W0401,E501 +""" +Development settings and globals based on :py:mod:`gvaldap.settings.base`. + +""" from __future__ import absolute_import diff --git a/gvaldap/gvaldap/settings/production.py b/gvaldap/gvaldap/settings/production.py index 7c9b280..288e1db 100644 --- a/gvaldap/gvaldap/settings/production.py +++ b/gvaldap/gvaldap/settings/production.py @@ -1,5 +1,8 @@ -"""Production settings and globals.""" # pymode:lint_ignore=W0401,E501 +""" +Production settings and globals based on :py:mod:`gvaldap.settings.base`. + +""" from __future__ import absolute_import diff --git a/gvaldap/gvaldap/settings/test.py b/gvaldap/gvaldap/settings/test.py index e510233..c39ee49 100644 --- a/gvaldap/gvaldap/settings/test.py +++ b/gvaldap/gvaldap/settings/test.py @@ -1,4 +1,8 @@ # pymode:lint_ignore=W0401 +""" +Test settings based on :py:mod:`gvaldap.settings.base`. + +""" from __future__ import absolute_import from .base import * diff --git a/gvaldap/gvaldap/urls.py b/gvaldap/gvaldap/urls.py index 120582d..37d1d10 100644 --- a/gvaldap/gvaldap/urls.py +++ b/gvaldap/gvaldap/urls.py @@ -1,3 +1,8 @@ +""" +This module defines the main URLConf for gvaldap. + +""" + from django.conf.urls import patterns, include, url from django.conf import settings diff --git a/gvaldap/gvaldap/wsgi.py b/gvaldap/gvaldap/wsgi.py index 29b3554..af02caa 100644 --- a/gvaldap/gvaldap/wsgi.py +++ b/gvaldap/gvaldap/wsgi.py @@ -26,10 +26,10 @@ path.append(SITE_ROOT) # os.environ["DJANGO_SETTINGS_MODULE"] = "jajaja.settings" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gvaldap.settings.production") +from django.core.wsgi import get_wsgi_application # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. -from django.core.wsgi import get_wsgi_application application = get_wsgi_application() # Apply WSGI middleware here. diff --git a/gvaldap/ldapentities/__init__.py b/gvaldap/ldapentities/__init__.py index e69de29..42332ea 100644 --- a/gvaldap/ldapentities/__init__.py +++ b/gvaldap/ldapentities/__init__.py @@ -0,0 +1,7 @@ +""" +This app takes care of managing LDAP entities, at the moment these are: + +* LDAP groups (:py:class:`ldapentities.models.LdapGroup`). +* LDAP users (:py:class:`ldapentities.models.LdapUser`) + +""" diff --git a/gvaldap/ldapentities/admin.py b/gvaldap/ldapentities/admin.py index 8d46d8a..97c81c5 100644 --- a/gvaldap/ldapentities/admin.py +++ b/gvaldap/ldapentities/admin.py @@ -1,3 +1,10 @@ +""" +Admin classes for easy `django admin`_ based administration of LDAP entities. + +.. _django admin: https://docs.djangoproject.com/en/dev/ref/contrib/admin/ + +""" + from django.contrib import admin from .models import ( @@ -7,12 +14,22 @@ from .models import ( class LdapGroupAdmin(admin.ModelAdmin): + """ + Admin class for :py:class:`LDAP group ` + entities. + + """ exclude = ['dn', 'members'] list_display = ['name', 'gid'] search_fields = ['name'] class LdapUserAdmin(admin.ModelAdmin): + """ + Admin class for :py:class:`LDAP user ` + entities. + + """ exclude = ['dn', 'password'] list_display = ['username', 'uid'] search_fields = ['username'] diff --git a/gvaldap/ldapentities/models.py b/gvaldap/ldapentities/models.py index ef1d390..89ee373 100644 --- a/gvaldap/ldapentities/models.py +++ b/gvaldap/ldapentities/models.py @@ -1,3 +1,12 @@ +""" +This module defines models for LDAP entities. + +The models are based on :py:class:`ldapmodels.Model` from `django-ldapdb`_. + +.. _django-ldapdb: https://github.com/jlaine/django-ldapdb#readme + +""" + from django.conf import settings from django.utils.encoding import python_2_unicode_compatible from ldapdb.models.fields import ( @@ -13,44 +22,87 @@ from passlib.hash import ldap_salted_sha1 @python_2_unicode_compatible class LdapGroup(ldapmodels.Model): """ - Class for representing an LDAP group entity. + Class for representing an LDAP group entity with objectClass `posixGroup`. + + .. seealso:: :rfc:`2307#section-4` + + .. py:attribute:: base_dn + + a string containing the LDAP base distinguished name + + .. py:attribute:: members + + contains the list of `memberUid` attributes """ # LDAP meta-data base_dn = settings.GROUP_BASE_DN + #: list of object classes object_classes = ['posixGroup'] # posixGroup attributes + #: group id (`gidNumber`) gid = IntegerField(db_column='gidNumber', unique=True) + #: group name (`cn`) name = CharField(db_column='cn', max_length=200, primary_key=True) + #: group description (`description`) description = CharField(db_column='description') members = ListField(db_column='memberUid', blank=True) def __str__(self): + """ + Get a string representation of this LDAP group. + """ return self.name @python_2_unicode_compatible class LdapUser(ldapmodels.Model): """ - Class for representing an LDAP user entity. + Class for representing an LDAP user entity with objectClasses `account` and + `posixAccount`. + + .. seealso:: :rfc:`2307#section-4`, :rfc:`4524#section-3.1` + + .. py:attribute:: base_dn + + a string containing the LDAP base distinguished name """ base_dn = settings.USER_BASE_DN + #: list of object classes object_classes = ['account', 'posixAccount'] # posixAccount + #: user id (`uidNumber`) uid = IntegerField(db_column='uidNumber', unique=True) + #: group id (`gidNumber`) of the user's primary group group = IntegerField(db_column='gidNumber') + #: GECOS field (`gecos`) gecos = CharField(db_column='gecos') + #: home directory (`homeDirectory`) home_directory = CharField(db_column='homeDirectory') + #: login shell (`loginShell`) login_shell = CharField(db_column='loginShell', default='/bin/bash') + #: user name (`uid`) username = CharField(db_column='uid', primary_key=True) + #: password (`userPassword`) in an LDAP compatible format password = CharField(db_column='userPassword') + #: common name (`cn`) common_name = CharField(db_column='cn') def __str__(self): + """ + Get a string representation of this LDAP user. + """ return self.username def set_password(self, password): + """ + Sets the encrypted password of the user from the given clear text + password. + + :param str password: the clear text password + + """ self.password = ldap_salted_sha1.encrypt(password) diff --git a/gvaldap/ldapentities/tests.py b/gvaldap/ldapentities/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/gvaldap/ldapentities/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/gvaldap/osusers/__init__.py b/gvaldap/osusers/__init__.py index e69de29..b960717 100644 --- a/gvaldap/osusers/__init__.py +++ b/gvaldap/osusers/__init__.py @@ -0,0 +1,3 @@ +""" +This module contains :py:mod:`osusers.tasks`. +""" diff --git a/gvaldap/osusers/admin.py b/gvaldap/osusers/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/gvaldap/osusers/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/gvaldap/osusers/models.py b/gvaldap/osusers/models.py index e69de29..d1ff455 100644 --- a/gvaldap/osusers/models.py +++ b/gvaldap/osusers/models.py @@ -0,0 +1,3 @@ +""" +Empty models module required for Django to accept this as an app. +""" diff --git a/gvaldap/osusers/tasks.py b/gvaldap/osusers/tasks.py index b1be4c1..591d22a 100644 --- a/gvaldap/osusers/tasks.py +++ b/gvaldap/osusers/tasks.py @@ -1,9 +1,17 @@ +""" +This module defines `Celery`_ tasks to manage LDAP entities. + +.. _Celery: http://www.celeryproject.org/ + +""" + from __future__ import absolute_import from django.core.exceptions import ObjectDoesNotExist from celery import shared_task from celery.utils.log import get_task_logger from celery.exceptions import Reject + from ldapentities.models import ( LdapGroup, LdapUser, @@ -15,6 +23,19 @@ _logger = get_task_logger(__name__) @shared_task def create_ldap_group(groupname, gid, descr): + """ + This task creates an :py:class:`LDAP group ` + if it does not exist yet. + + If a group with the given name exists its group id and description + attributes are updated. + + :param str groupname: the group name + :param int gid: the group id + :param str descr: description text for the group + :return: the distinguished name of the group + :rtype: str + """ try: ldapgroup = LdapGroup.objects.get(name=groupname) _logger.info( @@ -30,6 +51,28 @@ def create_ldap_group(groupname, gid, descr): @shared_task def create_ldap_user(username, uid, gid, gecos, homedir, shell, password): + """ + This task creates an :py:class:`LDAP user ` + if it does not exist yet. + + The task is rejected if the primary group of the user is not defined. + + The user's fields are updated if the user already exists. + + :param str username: the user name + :param int uid: the user id + :param int gid: the user's primary group's id + :param str gecos: the text for the GECOS field + :param str homedir: the user's home directory + :param str shell: the user's login shell + :param str or None password: the clear text password, if :py:const:`None` + is passed the password is not touched + :raises celery.exceptions.Reject: if the specified primary group does not + exist + :return: the distinguished name of the user + :rtype: str + + """ try: ldapuser = LdapUser.objects.get(username=username) _logger.info( @@ -64,6 +107,19 @@ def create_ldap_user(username, uid, gid, gecos, homedir, shell, password): @shared_task(bind=True) def add_ldap_user_to_group(self, username, groupname): + """ + This task adds the specified user to the given group. + + This task does nothing if the user is already member of the group. + + :param str username: the user name + :param str groupname: the group name + :raises celery.exceptions.Retry: if the user does not exist yet, + :py:func:`create_ldap_user` should be called before + :return: True if the user has been added to the group otherwise False + :rtype: boolean + + """ try: ldapgroup = LdapGroup.objects.get(name=groupname) ldapuser = LdapUser.objects.get(username=username) @@ -80,19 +136,40 @@ def add_ldap_user_to_group(self, username, groupname): _logger.info('ldap user {0} is already in group {1}'.format( ldapuser.username, ldapgroup.dn) ) + return True + return False @shared_task def remove_ldap_user_from_group(username, groupname): + """ + This task removes the given user from the given group. + + :param str username: the user name + :param str groupname: the group name + :return: True if the user has been removed, False otherwise + :rtype: boolean + + """ ldapgroup = LdapGroup.objects.get(name=groupname) ldapuser = LdapUser.objects.get(username=username) - if ldapuser.username in ldapgroup.members: + performdelete = ldapuser.username in ldapgroup.members + if performdelete: ldapgroup.members.remove(ldapuser.username) ldapgroup.save() + return performdelete @shared_task def delete_ldap_user(username): + """ + This task deletes the given user. + + :param str username: the user name + :return: True if the user has been deleted, False otherwise + :rtype: boolean + + """ try: ldapuser = LdapUser.objects.get(username=username) except LdapUser.DoesNotExist: @@ -111,10 +188,20 @@ def delete_ldap_user(username): ldapgroup.members.remove(ldapuser.username) ldapgroup.save() ldapuser.delete() + return True + return False @shared_task def delete_ldap_group_if_empty(groupname): + """ + This task deletes the given group. + + :param str groupname: the group name + :return: True if the user has been deleted, False otherwise + :rtype: boolean + + """ try: ldapgroup = LdapGroup.objects.get(name=groupname) except LdapGroup.DoesNotExist: @@ -124,7 +211,9 @@ def delete_ldap_group_if_empty(groupname): else: if len(ldapgroup.members) == 0: ldapgroup.delete() + return True else: _logger.info('ldap group {0} still has {1} members'.format( ldapgroup.dn, len(ldapgroup.members)) ) + return False diff --git a/gvaldap/osusers/tests.py b/gvaldap/osusers/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/gvaldap/osusers/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/requirements/base.txt b/requirements/base.txt index a2c45e6..a00a80c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -7,7 +7,4 @@ logutils==0.3.3 South==0.8.4 celery==3.1.11 passlib==1.6.2 -billiard==3.3.0.17 -kombu==3.0.16 -pytz==2014.3 pyaml==14.05.7 diff --git a/requirements/local.txt b/requirements/local.txt index 5f597de..8e4f5a9 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -4,3 +4,4 @@ coverage==3.7.1 django-debug-toolbar==1.2.1 Sphinx==1.2.2 sqlparse==0.1.11 +releases==0.6.1