2017-08-12 12:11:01 +02:00
=================================================
2017-08-12 12:04:05 +02:00
Common LDAP operation (for YunoHost but not only)
=================================================
Moulinette is deeply integrated with LDAP which is used for a series of things
like:
* storing users
* storing domains (for users emails)
* SSO
This page document how to uses it on a programming side in YunoHost.
Getting access to LDAP in a command
2017-08-12 12:11:01 +02:00
===================================
2017-08-12 12:04:05 +02:00
To get access to LDAP you need to authenticate against it, for that you need to
2017-08-12 13:36:38 +02:00
declare your command with requiring authentication in the :ref: `actionsmap` this way:
2017-08-12 12:04:05 +02:00
::
configuration:
authenticate: all
Here is a complete example:
::
somecommand:
category_help: ..
actions:
### somecommand_stuff()
2017-08-12 12:11:05 +02:00
stuff:
2017-08-12 12:04:05 +02:00
action_help: ...
api: GET /...
configuration:
authenticate: all
This will prompt the user for a password in CLI.
If you only need to **read** LDAP (and not modify it, for example by listing
domains), then you prevent the need for a password by using the
2017-08-12 12:12:41 +02:00
:file: `ldap-anonymous` authenticator this way:
2017-08-12 12:04:05 +02:00
::
configuration:
authenticate: all
authenticator: ldap-anonymous
2017-08-12 12:12:41 +02:00
Once you have declared your command like that, your python function will
received the :file: `auth` object as first argument, it will be used to talk to
LDAP, so you need to declare your function this way:
::
def somecommand_stuff(auth, ...):
...
2017-08-12 12:39:22 +02:00
2017-08-12 13:57:06 +02:00
auth in the moulinette code
---------------------------
The :file: `auth` object is an instance of :file: `moulinette.authenticators.ldap.Authenticator` class.
Here its docstring:
.. autoclass :: moulinette.authenticators.ldap.Authenticator
2017-08-13 01:43:29 +02:00
LDAP Schema
===========
2017-08-13 01:49:40 +02:00
This is a generated example of the ldap schema provided by YunoHost
2017-08-13 18:18:42 +02:00
(to generate this graph uses :file: `make ldap_graph` , you'll need graphviz):
2017-08-13 01:43:29 +02:00
.. image :: ldap_graph.png
2017-08-12 12:39:22 +02:00
Reading from LDAP
=================
Reading data from LDAP is done using the :file: `auth` object received as first
argument of the python function. To see how to get this object read the
previous section.
The API looks like this:
::
auth.search(ldap_path, ldap_query)
This will return a list of dictionary with strings as keys and list as values.
You can also specify a list of attributes you want to access from LDAP using a list of string (on only one string apparently):
::
auth.search(ldap_path, ldap_query, ['first_attribute', 'another_attribute'])
For example, if we request the user :file: `alice` with its :file: `homeDirectory` , this would look like this:
::
auth.search('ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(uid=alice))', ['homeDirectory', 'another_attribute'])
And as a result we will get:
::
[{'homeDirectory': ['/home/alice']}]
Notice that even for a single result we get a **list** of result and that every
value in the dictionary is also a **list** of values. This is not really convenient and it would be better to have a real ORM, but for now we are stuck with that.
Apparently if we don't specify the list of attributes it seems that we get all attributes (need to be confirmed).
2017-08-12 12:45:52 +02:00
2017-08-12 13:57:21 +02:00
Here is the method docstring:
.. automethod :: moulinette.authenticators.ldap.Authenticator.search
2017-08-12 12:53:40 +02:00
Users LDAP schema
-----------------
2017-08-12 13:35:42 +02:00
According to :file: `ldapvi` this is the user schema (on YunoHost 2.7):
2017-08-12 12:53:40 +02:00
::
2017-08-12 13:35:42 +02:00
# path: uid=the_unix_username,ou=users,dc=yunohost,dc=org
2017-08-12 12:53:40 +02:00
uid: the_unix_username
objectClass: mailAccount
objectClass: inetOrgPerson
objectClass: posixAccount
loginShell: /bin/false
uidNumber: 80833
maildrop: the_unix_username # why?
cn: first_name last_name
displayName: first_name last_name
mailuserquota: some_value
gidNumber: 80833
sn: last_name
homeDirectory: /home/the_unix_username
mail: the_unix_username@domain.com
# if the user is the admin he will also have the following mails
mail: root@domain.com
mail: admin@domain.com
mail: webmaster@domain.com
mail: postmaster@domain.com
givenName: first_name
2017-08-12 13:35:42 +02:00
The admin user is a special case that looks like this:
2017-08-12 13:25:25 +02:00
::
2017-08-12 13:35:42 +02:00
# path: cn=admin,dc=yunohost,dc=org
2017-08-12 13:25:25 +02:00
gidNumber: 1007
cn: admin
homeDirectory: /home/admin
objectClass: organizationalRole
objectClass: posixAccount
objectClass: simpleSecurityObject
loginShell: /bin/bash
description: LDAP Administrator
uidNumber: 1007
uid: admin
2017-08-12 13:35:50 +02:00
Other user related schemas:
::
# path: cn=admins,ou=groups,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: top
memberUid: admin
gidNumber: 4001
cn: admins
# path: cn=sftpusers,ou=groups,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: top
gidNumber: 4002
cn: sftpusers
memberUid: admin
memberUid: alice
# and all other users
# path: cn=admin,ou=sudo,dc=yunohost,dc=org
# this entry seems to specify which unix user is a sudoer
cn: admin
sudoCommand: ALL
sudoUser: admin
objectClass: sudoRole
objectClass: top
sudoOption: !authenticate
sudoHost: ALL
2017-08-12 12:45:52 +02:00
Reading users from LDAP
-----------------------
2017-08-12 12:54:53 +02:00
The user schema is located at this path: :file: `ou=users,dc=yunohost,dc=org`
2017-08-12 12:45:52 +02:00
According to already existing code, the queries we uses are:
* :file: `'(&(objectclass=person)(!(uid=root))(!(uid=nobody)))'` to get all users (not that I've never encountered users with :file: `root` or :file: `nobody` uid in the ldap database, those might be there for historical reason)
* :file: `'(&(objectclass=person)(uid=%s))' % username` to access one user data
This give us the 2 following python calls:
::
# all users
auth.search('ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(!(uid=root))(!(uid=nobody)))')
# one user
auth.search('ou=users,dc=yunohost,dc=org', '(&(objectclass=person)(uid=some_username))')
Apparently we could also access one user using the following path (and not query): :file: `uid=user_username,ou=users,dc=yunohost,dc=org` but I haven't test it.
2017-08-12 12:58:54 +02:00
If you want specific attributes look at the general documentation on how to read from LDAP a bit above of this section.
2017-08-12 13:16:00 +02:00
2017-08-13 01:24:35 +02:00
Users LDAP schema
-----------------
According to :file: `ldapvi` this is the domain schema (on YunoHost 2.7):
::
10 virtualdomain=domain.com,ou=domains,dc=yunohost,dc=org
objectClass: mailDomain
objectClass: top
virtualdomain: domain.com
2017-08-13 01:40:55 +02:00
2017-08-13 23:05:37 +02:00
Adding data in LDAP
===================
Adding stuff in LDAP seems pretty simple, according to existing code it looks like this:
::
auth.add('key=%s,ou=some_location', {'attribute1': 'value', ...})
They weird stuff is the path you need to create. This looks like that for domain and users:
::
# domain
auth.add('virtualdomain=%s,ou=domains' % domain, attr_dict)
# user
auth.add('uid=%s,ou=users' % username, attr_dict)
You need to respect the expected attributes. Refer to the schemas for that.
:file: `auth.add` seems to return something false when it failed (None probably)
so you need to check it's return code.
Here is the docstring:
.. automethod :: moulinette.authenticators.ldap.Authenticator.add
Adding user in LDAP
-------------------
Here is how it's done for a new user:
::
auth.add('uid=%s,ou=users' % username, {
'objectClass': ['mailAccount', 'inetOrgPerson', 'posixAccount'],
'givenName': firstname,
'sn': lastname,
'displayName': '%s %s' % (firstname, lastname),
'cn': fullname,
'uid': username,
'mail': mail,
'maildrop': username,
'mailuserquota': mailbox_quota,
'userPassword': user_pwd,
'gidNumber': uid,
'uidNumber': uid,
'homeDirectory': '/home/' + username,
'loginShell': '/bin/false'
})
Adding a domain in LDAP
-----------------------
Here is how it's done for a new domain:
::
auth.add('virtualdomain=%s,ou=domains' % domain, {
'objectClass': ['mailDomain', 'top']
'virtualdomain': domain,
})
2017-08-12 13:16:00 +02:00
Updating LDAP data
==================
2017-08-12 13:25:44 +02:00
Update a user from LDAP looks like a simplified version of searching. The syntax is the following one:
2017-08-12 13:16:00 +02:00
::
auth.update(exact_path_to_object, {'attribute_to_modify': 'new_value', 'another_attribute_to_modify': 'another_value', ...})
For example this will update a user :file: `loginShell` :
::
auth.update('uid=some_username,ou=users', {'loginShell': '/bin/bash'})
I don't know how this call behave if it fails and what it returns.
2017-08-12 13:57:21 +02:00
Here is the method docstring:
.. automethod :: moulinette.authenticators.ldap.Authenticator.update
2017-08-12 13:16:00 +02:00
Updating a user in LDAP
-------------------------
This is done this way:
::
auth.update('uid=some_username,ou=users', {'attribute': 'new_value', ...})
Refer to the user schema to know which attributes you can modify.
2017-08-12 13:57:33 +02:00
Validate uniqueness
===================
There is a method to validate the uniquess of some entry that is used during
user creation. I haven't used it and I'm not sure on how it work.
Here is how it's used (I don't understand why a path is not provided):
::
# Validate uniqueness of username and mail in LDAP
auth.validate_uniqueness({
'uid': username,
'mail': mail
})
And here is its docstring:
.. automethod :: moulinette.authenticators.ldap.Authenticator.update
2017-08-13 23:09:30 +02:00
Remove entries from LDAP
========================
Remove entries from LDAP is very simple, quite close to adding stuff except you don't need to specify the attributes dict, you just need to entrie path:
::
auth.remove(path)
Here how it looks like for domain and user:
::
# domain
auth.remove('virtualdomain=%s,ou=domains' % domain)
# user
auth.remove('uid=%s,ou=users' % username)
:file: `auth.remove` returns something that evaluate to False when it fails
(:file: `None` ?) so you need to check it returns code.
.. automethod :: moulinette.authenticators.ldap.Authenticator.remove
2019-05-25 23:50:59 +02:00
Reading LDIF file
=================
Reading parsing a ldif to be able to insert in the LDAP database is really easy. Here is how to get the content of a LDIF file
::
from moulinette.utils.filesystem import read_ldif
my_reslut = read_ldif("your_file.ldif")
Note that the main difference of what the auth object return with the search method is that this function return a 2-tuples with the "dn" and the LDAP entry.