Merge branch 'release/0.1' into production

This commit is contained in:
Jan Dittberner 2014-05-31 16:21:26 +02:00
commit 70bc8f4ff3
25 changed files with 669 additions and 58 deletions

1
.gitignore vendored
View file

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

View file

@ -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
<http://www.gnuviech-server.de>`_.
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 <install>` to get started locally.

5
docs/changelog.rst Normal file
View file

@ -0,0 +1,5 @@
Changelog
=========
* :release:`0.1 <2014-05-31>`
* :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
# 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'

View file

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

View file

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

View file

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

23
gvaldap/gvaldap/celery.py Normal file
View file

@ -0,0 +1,23 @@
"""
This module defines the Celery_ app for gvaldap.
.. _Celery: http://www.celeryproject.org/
"""
from __future__ import absolute_import
import os
from celery import Celery
from django.conf import settings
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)

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
"""
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:
@ -216,6 +227,8 @@ DJANGO_APPS = (
# Apps specific for this project go here.
LOCAL_APPS = (
'ldapentities',
'osusers',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
@ -262,6 +275,12 @@ WSGI_APPLICATION = '%s.wsgi.application' % SITE_NAME
########## END WSGI CONFIGURATION
########## LDAP SETTINGS
GROUP_BASE_DN = get_env_setting('GVALDAP_BASEDN_GROUP')
USER_BASE_DN = get_env_setting('GVALDAP_BASEDN_USER')
########## END LDAP SETTINGS
########## SOUTH CONFIGURATION
# See: http://south.readthedocs.org/en/latest/installation.html#configuring-your-django-installation
INSTALLED_APPS += (
@ -271,3 +290,13 @@ INSTALLED_APPS += (
# Don't need to use South when setting up a test database.
SOUTH_TESTS_MIGRATE = False
########## END SOUTH CONFIGURATION
########## CELERY CONFIGURATION
BROKER_URL = get_env_setting('GVALDAP_BROKER_URL')
CELERY_RESULT_BACKEND = 'amqp'
CELERY_RESULT_PERSISTENT = True
CELERY_TASK_RESULT_EXPIRES = None
CELERY_ACCEPT_CONTENT = ['yaml']
CELERY_RESULT_SERIALIZER = 'yaml'
########## END CELERY CONFIGURATION

View file

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

View file

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

View file

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

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 import settings

View file

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

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

@ -0,0 +1,39 @@
"""
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 (
LdapGroup,
LdapUser,
)
class LdapGroupAdmin(admin.ModelAdmin):
"""
Admin class for :py:class:`LDAP group <ldapentities.models.LdapGroup>`
entities.
"""
exclude = ['dn', 'members']
list_display = ['name', 'gid']
search_fields = ['name']
class LdapUserAdmin(admin.ModelAdmin):
"""
Admin class for :py:class:`LDAP user <ldapentities.models.LdapUser>`
entities.
"""
exclude = ['dn', 'password']
list_display = ['username', 'uid']
search_fields = ['username']
admin.site.register(LdapGroup, LdapGroupAdmin)
admin.site.register(LdapUser, LdapUserAdmin)

View file

@ -0,0 +1,108 @@
"""
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 (
CharField,
IntegerField,
ListField,
)
import ldapdb.models as ldapmodels
from passlib.hash import ldap_salted_sha1
@python_2_unicode_compatible
class LdapGroup(ldapmodels.Model):
"""
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 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)

View file

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

View file

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

219
gvaldap/osusers/tasks.py Normal file
View file

@ -0,0 +1,219 @@
"""
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,
)
_logger = get_task_logger(__name__)
@shared_task
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:
ldapgroup = LdapGroup.objects.get(name=groupname)
_logger.info(
'ldap group with dn {0} already exists'.format(ldapgroup.dn)
)
ldapgroup.gid = gid
except LdapGroup.DoesNotExist:
ldapgroup = LdapGroup(gid=gid, name=groupname)
ldapgroup.description = descr
ldapgroup.save()
return ldapgroup.dn
@shared_task
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:
ldapuser = LdapUser.objects.get(username=username)
_logger.info(
'ldap user with dn {0} already exists'.format(ldapuser.dn)
)
except LdapUser.DoesNotExist:
ldapuser = LdapUser(username=username)
try:
ldapgroup = LdapGroup.objects.get(gid=gid)
except ObjectDoesNotExist as exc:
_logger.info('ldap group with gid {0} does not exist')
raise Reject(exc, requeue=False)
ldapuser.uid = uid
ldapuser.group = gid
ldapuser.gecos = gecos
ldapuser.home_directory = homedir
ldapuser.login_shell = shell
ldapuser.username = username
ldapuser.common_name = username
if password is not None:
ldapuser.set_password(password)
if ldapuser.username in ldapgroup.members:
_logger.info('user {0} is already member of {1}'.format(
ldapuser.username, ldapgroup.dn)
)
else:
ldapgroup.members.append(ldapuser.username)
ldapgroup.save()
ldapuser.save()
return ldapuser.dn
@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)
except LdapGroup.DoesNotExist:
_logger.error('ldap group {0} does not exist'.format(groupname))
except LdapUser.DoesNotExist as exc:
_logger.error('ldap user {0} does not exist'.format(username))
self.retry(exc=exc, time_limit=5)
else:
if not ldapuser.username in ldapgroup.members:
ldapgroup.members.append(ldapuser.username)
ldapgroup.save()
else:
_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)
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:
_logger.info('there is no ldap user with uid {0}'.format(
username)
)
else:
try:
ldapgroup = LdapGroup.objects.get(gid=ldapuser.group)
except LdapGroup.DoesNotExist:
_logger.info('group {0} for user {1} does not exist'.format(
ldapuser.group, ldapuser.username)
)
else:
if ldapuser.username in ldapgroup.members:
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:
_logger.info('ldap group with name {0} does not exist'.format(
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

View file

@ -5,3 +5,6 @@ django-braces==1.4.0
django-model-utils==2.0.3
logutils==0.3.3
South==0.8.4
celery==3.1.11
passlib==1.6.2
pyaml==14.05.7

View file

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