Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[helper] App users ldap #977

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions data/helpers.d/user
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ ynh_system_user_exists() {
getent passwd "$username" &>/dev/null
}

# Check if the app user exists on LDAP
#
# Note that the username is : $YNH_APP_INSTANCE_NAME
#
# usage: ynh_ldap_user_exists
# | exit: Return 1 if the user doesn't exist, 0 otherwise
#
# Requires YunoHost version 3.8.0 or higher.
ynh_ldap_user_exists() {
# Declare an array to define the options of this helper.
local username=$YNH_APP_INSTANCE_NAME
test -n "$(yunohost tools shell -c "from yunohost.user import user_list; print(user_list(fields=['uid'], app_user=True)['users'].get('$username', ''))")"
}

# Check if a group exists on the system
#
# usage: ynh_system_group_exists --group=group
Expand Down Expand Up @@ -135,6 +149,64 @@ ynh_system_user_create () {
fi
}


# Create a LDAP user for app
#
# Note that the username is : $YNH_APP_INSTANCE_NAME
# This will create a user and a group in LDAP in the LDAP.
# The user will be in: ou=users,ou=apps,dc=yunohost,dc=org
# The group will be in: ou=groups,ou=apps,dc=yunohost,dc=org
#
# examples:
# # Create a nextcloud user with no home directory, /usr/sbin/nologin login shell (hence no login capability) and nextcloud@main_domain.tld as email
# ynh_ldap_user_create --password="The-Best_password" --home_dir /var/www/nextcloud
# # Create a discourse user using /var/www/discourse as home directory and the default login shell
# ynh_ldap_user_create --password="RAND0MP4sSw0RO" --home_dir=/var/www/discourse --mail forum@app_domain.tld --use_shell
#
# usage: ynh_ldap_user_create --username=user_name --password=password --home_dir=home_dir [--mail=app_email] [--use_shell]
# | arg: -p, --password - Password of the user. Used for the authentication in LDAP, sending and receiving email.
# | arg: -h, --home_dir= - Path of the home dir for the user. Usually the final path of the app.
# | arg: -m, --mail - Email of this user. By default it's username@yunohost_main_domain
# | arg: -s, --use_shell - Create a user using the default login shell if present. If this argument is omitted, the user will be created with /usr/sbin/nologin shell
#
# Note that with this helper the app will have a mailbox accessible with SMTP and IMAP.
# the identifier are:
# user: $YNH_APP_INSTANCE_NAME
# password: password defined when the helper is called.
#
# Requires YunoHost version 3.8.0 or higher.
ynh_ldap_user_create () {
# Declare an array to define the options of this helper.
local legacy_args=phms
local -A args_array=([p]=password= [h]=home_dir= [m]=mail [s]=use_shell )
local password
local home_dir
local mail
local use_shell
# Manage arguments with getopts
ynh_handle_getopts_args "$@"
local username=$YNH_APP_INSTANCE_NAME
mail="${mail:-}"
use_shell="${use_shell:-0}"

if ! ynh_ldap_user_exists # Check if the user exists in LDAP
then # If the user doesn't exist
if [ -n $mail ]
then
mail="$username"@$(cat /etc/yunohost/current_host)
fi
if [ $use_shell -eq 1 ]
then # If we want a shell for the user
local shell="/bin/bash"
else
local shell="/usr/sbin/nologin"
fi

# Note that we can't use the CLI because we need to specify that it's a app_user and not a normal user.
yunohost tools shell -c "from yunohost.user import user_create; user_create('$username', firstname='$username', lastname='App', mail='$mail', password='$password', home_directory='$home_dir', shell='$shell', app_user=True)"
fi
}

# Delete a system user
#
# usage: ynh_system_user_delete --username=user_name
Expand Down Expand Up @@ -163,3 +235,25 @@ ynh_system_user_delete () {
delgroup $username
fi
}

# Delete the LDAP user
#
# Note that the username is : $YNH_APP_INSTANCE_NAME
#
# Note that the LDAP app user is automatically deleted by the core when the app is removed
#
# usage: ynh_LDAP_user_delete
#
# Requires YunoHost version 3.8.0 or higher.
ynh_ldap_user_delete () {
# Declare an array to define the options of this helper.
local username=$YNH_APP_INSTANCE_NAME

# Check if the user exists on the system
if ynh_ldap_user_exists
then
yunohost tools shell -c "from yunohost.user import user_delete; user_delete('$username', app_user=True)"
else
ynh_print_warn --message="The user $username was not found"
fi
}
11 changes: 11 additions & 0 deletions data/other/ldap_scheme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ children:
- posixGroup
- groupOfNamesYnh

ou=users,ou=apps:
ou: users
objectClass:
- organizationalUnit
- top
ou=groups,ou=apps:
ou: users
objectClass:
- organizationalUnit
- top

depends_children:
cn=mail.main,ou=permission:
cn: mail.main
Expand Down
7 changes: 3 additions & 4 deletions data/templates/dovecot/dovecot-ldap.conf
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
hosts = 127.0.0.1
auth_bind = yes
ldap_version = 3
base = ou=users,dc=yunohost,dc=org
base = dc=yunohost,dc=org
user_attrs = uidNumber=500,gidNumber=8,mailuserquota=quota_rule=*:bytes=%$
user_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org))
pass_filter = (&(objectClass=inetOrgPerson)(uid=%n)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org))
user_filter = (&(objectClass=inetOrgPerson)(uid=%n)(|(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)(ou:dn:=apps)))
pass_filter = (&(objectClass=inetOrgPerson)(uid=%n)(|(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)(ou:dn:=apps)))
default_pass_scheme = SSHA

2 changes: 1 addition & 1 deletion data/templates/postfix/plain/ldap-accounts.cf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server_host = localhost
server_port = 389
search_base = dc=yunohost,dc=org
query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org))
query_filter = (&(objectClass=mailAccount)(mail=%s)(|(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)(ou:dn:=apps)))
result_attribute = uid
2 changes: 1 addition & 1 deletion data/templates/postfix/plain/ldap-aliases.cf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
server_host = localhost
server_port = 389
search_base = dc=yunohost,dc=org
query_filter = (&(objectClass=mailAccount)(mail=%s)(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org))
query_filter = (&(objectClass=mailAccount)(mail=%s)(|(permission=cn=mail.main,ou=permission,dc=yunohost,dc=org)(ou:dn:=apps)))
result_attribute = maildrop
3 changes: 3 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@
"migration_description_0012_postgresql_password_to_md5_authentication": "Force PostgreSQL authentication to use MD5 for local connections",
"migration_description_0013_futureproof_apps_catalog_system": "Migrate to the new future-proof apps catalog system",
"migration_description_0014_remove_app_status_json": "Remove legacy status.json app files",
"migration_description_0015_update_ldap_for_apps_users": "Update LDAP structure for app user in LDAP",
"migration_0003_start": "Starting migration to Stretch. The logs will be available in {logfile}.",
"migration_0003_patching_sources_list": "Patching the sources.lists…",
"migration_0003_main_upgrade": "Starting main upgrade…",
Expand Down Expand Up @@ -465,6 +466,8 @@
"migration_0011_update_LDAP_database": "Updating LDAP database…",
"migration_0011_update_LDAP_schema": "Updating LDAP schema…",
"migration_0011_failed_to_remove_stale_object": "Could not remove stale object {dn}: {error}",
"migration_0015_update_LDAP_database": "Updating LDAP database for app's user",
"migration_0015_done": "Migration completed. App can have his own user.",
"migrations_already_ran": "Those migrations are already done: {ids}",
"migrations_cant_reach_migration_file": "Could not access migrations files at the path '%s'",
"migrations_dependencies_not_satisfied": "Run these migrations: '{dependencies_id}', before migration {id}.",
Expand Down
10 changes: 10 additions & 0 deletions src/yunohost/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ def app_install(operation_logger, app, label=None, args=None, no_remove_on_failu

from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
from yunohost.log import OperationLogger
from yunohost.user import user_list, user_delete
from yunohost.permission import user_permission_list, permission_create, permission_url, permission_delete, permission_sync_to_user, user_permission_update

# Fetch or extract sources
Expand Down Expand Up @@ -860,6 +861,10 @@ def confirm_install(confirm):
if permission_name.startswith(app_instance_name+"."):
permission_delete(permission_name, force=True, sync_perm=False)

# Remove app user in LDAP
if app_instance_name in user_list(fields=['uid'], app_user=True)['users']:
user_delete(app_instance_name, app_user=True)

if remove_retcode != 0:
msg = m18n.n('app_not_properly_removed',
app=app_instance_name)
Expand Down Expand Up @@ -992,6 +997,7 @@ def app_remove(operation_logger, app):

"""
from yunohost.hook import hook_exec, hook_remove, hook_callback
from yunohost.user import user_list, user_delete
from yunohost.permission import user_permission_list, permission_delete, permission_sync_to_user
if not _is_installed(app):
raise YunohostError('app_not_installed', app=app, all_apps=_get_all_installed_apps_id())
Expand Down Expand Up @@ -1060,6 +1066,10 @@ def app_remove(operation_logger, app):
if permission_name.startswith(app+"."):
permission_delete(permission_name, force=True, sync_perm=False)

# Remove app user in LDAP
if app in user_list(fields=['uid'], app_user=True)['users']:
user_delete(app, app_user=True)

permission_sync_to_user()
_assert_system_is_sane_for_app(manifest, "post")

Expand Down
7 changes: 6 additions & 1 deletion src/yunohost/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,7 @@ def _restore_system(self):
if system_targets == []:
return

from yunohost.tools import _get_migration_by_name
from yunohost.user import user_group_list
from yunohost.permission import permission_create, permission_delete, user_permission_update, user_permission_list, permission_sync_to_user

Expand Down Expand Up @@ -1230,7 +1231,6 @@ def _restore_system(self):
#
# Legacy code
if not "all_users" in user_group_list()["groups"].keys():
from yunohost.tools import _get_migration_by_name
setup_group_permission = _get_migration_by_name("setup_group_permission")
# Update LDAP schema restart slapd
logger.info(m18n.n("migration_0011_update_LDAP_schema"))
Expand All @@ -1247,6 +1247,11 @@ def _restore_system(self):
if _is_installed(app_name):
permission_create(permission_name, url=permission_infos["url"], allowed=permission_infos["allowed"], sync_perm=False)

# Run this migration in all case. This will clean all app user while the backup is made
# and this will update the LDAP tree for app user in LDAP
cleanup_ldap_app_users = _get_migration_by_name("update_ldap_for_apps_users")
cleanup_ldap_app_users.run()

permission_sync_to_user()


Expand Down
67 changes: 67 additions & 0 deletions src/yunohost/data_migrations/0015_update_ldap_for_apps_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import time
import os

from moulinette import m18n
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_yaml

from yunohost.tools import Migration

logger = getActionLogger('yunohost.migration')

class MyMigration(Migration):
"""
Update the LDAP DB to be able to store apps' user in LDAP
"""

required = True

def remove_if_exists(self, target):

from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()

try:
objects = ldap.search(target + ",dc=yunohost,dc=org")
# ldap search will raise an exception if no corresponding object is found >.> ...
except Exception as e:
logger.debug("%s does not exist, no need to delete it" % target)
return

objects.reverse()
for o in objects:
for dn in o["dn"]:
dn = dn.replace(",dc=yunohost,dc=org", "")
logger.debug("Deleting old object %s ..." % dn)
try:
ldap.remove(dn)
except Exception as e:
raise YunohostError("migration_0011_failed_to_remove_stale_object", dn=dn, error=e)


def run(self):

logger.info(m18n.n("migration_0015_update_LDAP_database"))

from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()

ldap_map = read_yaml('/usr/share/yunohost/yunohost-config/moulinette/ldap_scheme.yml')

try:
self.remove_if_exists('ou=apps')

attr_dict = ldap_map['parents']['ou=apps']
ldap.add('ou=apps', attr_dict)

attr_dict = ldap_map['children']['ou=users,ou=apps']
ldap.add('ou=users,ou=apps', attr_dict)

attr_dict = ldap_map['children']['ou=groups,ou=apps']
ldap.add('ou=groups,ou=apps', attr_dict)

except Exception as e:
raise YunohostError("migration_0011_LDAP_update_failed", error=e)

logger.info(m18n.n("migration_0015_done"))
Loading