finish initial documentation

This commit is contained in:
Jan Dittberner 2014-05-31 16:12:51 +02:00
commit 60fc992191
28 changed files with 425 additions and 78 deletions

1
.gitignore vendored
View file

@ -39,3 +39,4 @@ Thumbs.db
Desktop.ini Desktop.ini
.ropeproject .ropeproject
_build/

View file

@ -4,49 +4,8 @@ gvaldap
This is the GNUViech Admin LDAP administration tool project. 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
<http://www.gnuviech-server.de>`_.
You have several options in setting up your working environment. We recommend Read the :doc:`Installation instructions <install>` to get started locally.
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

4
docs/changelog.rst Normal file
View file

@ -0,0 +1,4 @@
Changelog
=========
* :feature:`-` intial support for creating LDAP users and groups

97
docs/code.rst Normal file
View file

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

View file

@ -12,12 +12,13 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
#import sys, os import sys
import os
# If extensions (or modules to document with autodoc) are in another directory, # 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 # 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. # 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 ----------------------------------------------------- # -- General configuration -----------------------------------------------------
@ -26,11 +27,15 @@
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # 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. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] 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. # The suffix of source filenames.
source_suffix = '.rst' source_suffix = '.rst'

View file

@ -1,4 +1,10 @@
Deploy 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

View file

@ -3,6 +3,8 @@
You can adapt this file completely to your liking, but it should at least You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive. contain the root `toctree` directive.
.. include:: ../README.rst
Welcome to gvaldap's documentation! Welcome to gvaldap's documentation!
==================================== ====================================
@ -13,8 +15,8 @@ Contents:
install install
deploy deploy
tests code
changelog
Indices and tables Indices and tables

View file

@ -1,4 +1,84 @@
.. index:: installation
=======
Install 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
<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:
.. 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/

View file

@ -0,0 +1,3 @@
"""
This is the gvaldap project module.
"""

View file

@ -1,3 +1,9 @@
"""
This module defines the Celery_ app for gvaldap.
.. _Celery: http://www.celeryproject.org/
"""
from __future__ import absolute_import from __future__ import absolute_import
import os import os
@ -10,12 +16,8 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE',
'gvaldap.settings.production') 'gvaldap.settings.production')
#: The Celery application
app = Celery('gvaldap') app = Celery('gvaldap')
app.config_from_object('django.conf:settings') app.config_from_object('django.conf:settings')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
@app.task(bind=True)
def debug_task(self):
print('Request: {0!r}'.format(self.request))

View file

@ -1 +1,3 @@
"""
This module contains settings for various environments.
"""

View file

@ -1,5 +1,9 @@
"""Common settings and globals.""" # -*- coding: utf-8 -*-
# pymode:lint_ignore=E501 # pymode:lint_ignore=E501
"""
Common settings and globals.
"""
from os.path import abspath, basename, dirname, join, normpath from os.path import abspath, basename, dirname, join, normpath
@ -12,7 +16,14 @@ from django.core.exceptions import ImproperlyConfigured
def get_env_setting(setting): 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: try:
return environ[setting] return environ[setting]
except KeyError: except KeyError:

View file

@ -1,5 +1,8 @@
"""Development settings and globals."""
# pymode:lint_ignore=W0401,E501 # pymode:lint_ignore=W0401,E501
"""
Development settings and globals based on :py:mod:`gvaldap.settings.base`.
"""
from __future__ import absolute_import from __future__ import absolute_import

View file

@ -1,5 +1,8 @@
"""Production settings and globals."""
# pymode:lint_ignore=W0401,E501 # pymode:lint_ignore=W0401,E501
"""
Production settings and globals based on :py:mod:`gvaldap.settings.base`.
"""
from __future__ import absolute_import from __future__ import absolute_import

View file

@ -1,4 +1,8 @@
# pymode:lint_ignore=W0401 # pymode:lint_ignore=W0401
"""
Test settings based on :py:mod:`gvaldap.settings.base`.
"""
from __future__ import absolute_import from __future__ import absolute_import
from .base import * from .base import *

View file

@ -1,3 +1,8 @@
"""
This module defines the main URLConf for gvaldap.
"""
from django.conf.urls import patterns, include, url from django.conf.urls import patterns, include, url
from django.conf import settings from django.conf import settings

View file

@ -26,10 +26,10 @@ path.append(SITE_ROOT)
# os.environ["DJANGO_SETTINGS_MODULE"] = "jajaja.settings" # os.environ["DJANGO_SETTINGS_MODULE"] = "jajaja.settings"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gvaldap.settings.production") 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 # This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION # file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here. # setting points here.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application() application = get_wsgi_application()
# Apply WSGI middleware here. # Apply WSGI middleware here.

View file

@ -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`)
"""

View file

@ -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 django.contrib import admin
from .models import ( from .models import (
@ -7,12 +14,22 @@ from .models import (
class LdapGroupAdmin(admin.ModelAdmin): class LdapGroupAdmin(admin.ModelAdmin):
"""
Admin class for :py:class:`LDAP group <ldapentities.models.LdapGroup>`
entities.
"""
exclude = ['dn', 'members'] exclude = ['dn', 'members']
list_display = ['name', 'gid'] list_display = ['name', 'gid']
search_fields = ['name'] search_fields = ['name']
class LdapUserAdmin(admin.ModelAdmin): class LdapUserAdmin(admin.ModelAdmin):
"""
Admin class for :py:class:`LDAP user <ldapentities.models.LdapUser>`
entities.
"""
exclude = ['dn', 'password'] exclude = ['dn', 'password']
list_display = ['username', 'uid'] list_display = ['username', 'uid']
search_fields = ['username'] search_fields = ['username']

View file

@ -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.conf import settings
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from ldapdb.models.fields import ( from ldapdb.models.fields import (
@ -13,44 +22,87 @@ from passlib.hash import ldap_salted_sha1
@python_2_unicode_compatible @python_2_unicode_compatible
class LdapGroup(ldapmodels.Model): 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 # LDAP meta-data
base_dn = settings.GROUP_BASE_DN base_dn = settings.GROUP_BASE_DN
#: list of object classes
object_classes = ['posixGroup'] object_classes = ['posixGroup']
# posixGroup attributes # posixGroup attributes
#: group id (`gidNumber`)
gid = IntegerField(db_column='gidNumber', unique=True) gid = IntegerField(db_column='gidNumber', unique=True)
#: group name (`cn`)
name = CharField(db_column='cn', max_length=200, primary_key=True) name = CharField(db_column='cn', max_length=200, primary_key=True)
#: group description (`description`)
description = CharField(db_column='description') description = CharField(db_column='description')
members = ListField(db_column='memberUid', blank=True) members = ListField(db_column='memberUid', blank=True)
def __str__(self): def __str__(self):
"""
Get a string representation of this LDAP group.
"""
return self.name return self.name
@python_2_unicode_compatible @python_2_unicode_compatible
class LdapUser(ldapmodels.Model): 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 base_dn = settings.USER_BASE_DN
#: list of object classes
object_classes = ['account', 'posixAccount'] object_classes = ['account', 'posixAccount']
# posixAccount # posixAccount
#: user id (`uidNumber`)
uid = IntegerField(db_column='uidNumber', unique=True) uid = IntegerField(db_column='uidNumber', unique=True)
#: group id (`gidNumber`) of the user's primary group
group = IntegerField(db_column='gidNumber') group = IntegerField(db_column='gidNumber')
#: GECOS field (`gecos`)
gecos = CharField(db_column='gecos') gecos = CharField(db_column='gecos')
#: home directory (`homeDirectory`)
home_directory = CharField(db_column='homeDirectory') home_directory = CharField(db_column='homeDirectory')
#: login shell (`loginShell`)
login_shell = CharField(db_column='loginShell', default='/bin/bash') login_shell = CharField(db_column='loginShell', default='/bin/bash')
#: user name (`uid`)
username = CharField(db_column='uid', primary_key=True) username = CharField(db_column='uid', primary_key=True)
#: password (`userPassword`) in an LDAP compatible format
password = CharField(db_column='userPassword') password = CharField(db_column='userPassword')
#: common name (`cn`)
common_name = CharField(db_column='cn') common_name = CharField(db_column='cn')
def __str__(self): def __str__(self):
"""
Get a string representation of this LDAP user.
"""
return self.username return self.username
def set_password(self, password): 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) self.password = ldap_salted_sha1.encrypt(password)

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -0,0 +1,3 @@
"""
This module contains :py:mod:`osusers.tasks`.
"""

View file

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View file

@ -0,0 +1,3 @@
"""
Empty models module required for Django to accept this as an app.
"""

View file

@ -1,9 +1,17 @@
"""
This module defines `Celery`_ tasks to manage LDAP entities.
.. _Celery: http://www.celeryproject.org/
"""
from __future__ import absolute_import from __future__ import absolute_import
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task from celery import shared_task
from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
from celery.exceptions import Reject from celery.exceptions import Reject
from ldapentities.models import ( from ldapentities.models import (
LdapGroup, LdapGroup,
LdapUser, LdapUser,
@ -15,6 +23,19 @@ _logger = get_task_logger(__name__)
@shared_task @shared_task
def create_ldap_group(groupname, gid, descr): def create_ldap_group(groupname, gid, descr):
"""
This task creates an :py:class:`LDAP group <ldapentities.models.LdapGroup>`
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: try:
ldapgroup = LdapGroup.objects.get(name=groupname) ldapgroup = LdapGroup.objects.get(name=groupname)
_logger.info( _logger.info(
@ -30,6 +51,28 @@ def create_ldap_group(groupname, gid, descr):
@shared_task @shared_task
def create_ldap_user(username, uid, gid, gecos, homedir, shell, password): def create_ldap_user(username, uid, gid, gecos, homedir, shell, password):
"""
This task creates an :py:class:`LDAP user <ldapentities.models.LdapUser>`
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: try:
ldapuser = LdapUser.objects.get(username=username) ldapuser = LdapUser.objects.get(username=username)
_logger.info( _logger.info(
@ -64,6 +107,19 @@ def create_ldap_user(username, uid, gid, gecos, homedir, shell, password):
@shared_task(bind=True) @shared_task(bind=True)
def add_ldap_user_to_group(self, username, groupname): 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: try:
ldapgroup = LdapGroup.objects.get(name=groupname) ldapgroup = LdapGroup.objects.get(name=groupname)
ldapuser = LdapUser.objects.get(username=username) 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( _logger.info('ldap user {0} is already in group {1}'.format(
ldapuser.username, ldapgroup.dn) ldapuser.username, ldapgroup.dn)
) )
return True
return False
@shared_task @shared_task
def remove_ldap_user_from_group(username, groupname): 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) ldapgroup = LdapGroup.objects.get(name=groupname)
ldapuser = LdapUser.objects.get(username=username) 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.members.remove(ldapuser.username)
ldapgroup.save() ldapgroup.save()
return performdelete
@shared_task @shared_task
def delete_ldap_user(username): 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: try:
ldapuser = LdapUser.objects.get(username=username) ldapuser = LdapUser.objects.get(username=username)
except LdapUser.DoesNotExist: except LdapUser.DoesNotExist:
@ -111,10 +188,20 @@ def delete_ldap_user(username):
ldapgroup.members.remove(ldapuser.username) ldapgroup.members.remove(ldapuser.username)
ldapgroup.save() ldapgroup.save()
ldapuser.delete() ldapuser.delete()
return True
return False
@shared_task @shared_task
def delete_ldap_group_if_empty(groupname): 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: try:
ldapgroup = LdapGroup.objects.get(name=groupname) ldapgroup = LdapGroup.objects.get(name=groupname)
except LdapGroup.DoesNotExist: except LdapGroup.DoesNotExist:
@ -124,7 +211,9 @@ def delete_ldap_group_if_empty(groupname):
else: else:
if len(ldapgroup.members) == 0: if len(ldapgroup.members) == 0:
ldapgroup.delete() ldapgroup.delete()
return True
else: else:
_logger.info('ldap group {0} still has {1} members'.format( _logger.info('ldap group {0} still has {1} members'.format(
ldapgroup.dn, len(ldapgroup.members)) ldapgroup.dn, len(ldapgroup.members))
) )
return False

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -7,7 +7,4 @@ logutils==0.3.3
South==0.8.4 South==0.8.4
celery==3.1.11 celery==3.1.11
passlib==1.6.2 passlib==1.6.2
billiard==3.3.0.17
kombu==3.0.16
pytz==2014.3
pyaml==14.05.7 pyaml==14.05.7

View file

@ -4,3 +4,4 @@ coverage==3.7.1
django-debug-toolbar==1.2.1 django-debug-toolbar==1.2.1
Sphinx==1.2.2 Sphinx==1.2.2
sqlparse==0.1.11 sqlparse==0.1.11
releases==0.6.1