diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py
index fdc38f25..c63d6ba3 100644
--- a/orchestra/contrib/systemusers/backends.py
+++ b/orchestra/contrib/systemusers/backends.py
@@ -70,7 +70,7 @@ class UNIXUserController(ServiceController):
self.append(textwrap.dedent("""\
# Set extra permissions: %(user)s home is inside %(mainuser)s home
if true; then
-# if mount | grep "^$(df %(home)s|grep '^/'|cut -d' ' -f1)\s" | grep acl > /dev/null; then
+ # if mount | grep "^$(df %(home)s|grep '^/'|cut -d' ' -f1)\s" | grep acl > /dev/null; then
# Account group as the owner
chown %(mainuser)s:%(mainuser)s '%(home)s'
chmod g+s '%(home)s'
@@ -460,3 +460,246 @@ class VsFTPdTraffic(ServiceMonitor):
'username': user.username,
}
return replace(context, "'", '"')
+
+
+
+# -----------------------------------------------------------------------------------------------------------------------------------------
+
+
+class UNIXUserControllerNewServers(ServiceController):
+ """
+ Basic UNIX system user/group support based on useradd, usermod, userdel and groupdel.
+ Autodetects and uses ACL if available, for better permission management.
+ """
+ verbose_name = _("UNIX user new servers")
+ model = 'systemusers.SystemUser'
+ actions = ('save', 'delete', 'set_permission', 'validate_paths_exist', 'create_link')
+ doc_settings = (settings, (
+ 'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS',
+ 'SYSTEMUSERS_MOVE_ON_DELETE_PATH',
+ 'SYSTEMUSERS_FORBIDDEN_PATHS'
+ ))
+
+ def save(self, user):
+ context = self.get_context(user)
+ if not context['user']:
+ return
+ if not user.active:
+ self.append(textwrap.dedent("""
+ #Just disable that user, if it exists
+ if id %(user)s ; then
+ usermod %(user)s --password '%(password)s'
+ fi
+ """) % context)
+ return
+ if user.is_main:
+ # TODO userd add will fail if %(user)s group already exists
+ self.append(textwrap.dedent("""
+ # Update/create user state for %(user)s
+ if id %(user)s ; then
+ usermod %(user)s --home '%(home)s/%(user)s' \\
+ --password '%(password)s' \\
+ --shell '%(shell)s' \\
+ --groups '%(groups)s'
+ else
+ useradd_code=0
+ useradd %(user)s --home '%(home)s/%(user)s' \\
+ --password '%(password)s' \\
+ --shell '%(shell)s' \\
+ --groups '%(groups)s' || useradd_code=$?
+ if [[ $useradd_code -eq 8 ]]; then
+ # User is logged in, kill and retry
+ pkill -u %(user)s; sleep 2
+ pkill -9 -u %(user)s; sleep 1
+ useradd %(user)s --home '%(home)s/%(user)s' \\
+ --password '%(password)s' \\
+ --shell '%(shell)s' \\
+ --groups '%(groups)s'
+ elif [[ $useradd_code -ne 0 ]]; then
+ exit $useradd_code
+ fi
+ fi
+ mkdir -p '%(base_home)s/%(user)s'
+ chown root:%(user)s %(base_home)s
+ chmod 710 '%(base_home)s'
+ setfacl -m 'u:%(user)s:rx' %(base_home)s
+
+ chown %(user)s:%(user)s '%(base_home)s/%(user)s'
+ chmod 700 '%(base_home)s/%(user)s'
+ """) % context
+ )
+ self.append(textwrap.dedent("""\
+ ls -A /etc/skel/ | while read line; do
+ if [[ ! -e "%(home)s/${line}" ]]; then
+ cp -a "/etc/skel/${line}" "%(base_home)s/%(user)s/${line}" && \\
+ chown -R %(user)s:%(user)s "%(base_home)s/%(user)s/${line}"
+ fi
+ done
+ """) % context
+ )
+
+ for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS:
+ context['member'] = member
+ self.append('usermod -a -G %(user)s %(member)s || exit_code=$?' % context)
+ if not user.is_main:
+ self.append('usermod -a -G %(user)s %(mainuser)s || exit_code=$?' % context)
+
+ def delete(self, user):
+ context = self.get_context(user)
+ if not context['user']:
+ return
+ self.append(textwrap.dedent("""
+ # Delete %(user)s user
+ nohup bash -c 'sleep 2 && killall -u %(user)s -s KILL' &> /dev/null &
+ killall -u %(user)s || true
+ userdel %(user)s || exit_code=$?
+ groupdel %(group)s || exit_code=$?\
+ """) % context
+ )
+ if context['deleted_home']:
+ self.append(textwrap.dedent("""\
+ # Move home into SYSTEMUSERS_MOVE_ON_DELETE_PATH, nesting if exists.
+ deleted_home="%(deleted_home)s"
+ while [[ -e "$deleted_home" ]]; do
+ deleted_home="${deleted_home}/$(basename ${deleted_home})"
+ done
+ mv '%(base_home)s' "$deleted_home" || exit_code=$?
+ """) % context
+ )
+ else:
+ self.append("rm -fr -- '%(base_home)s'" % context)
+
+ def grant_permissions(self, user, context):
+ context['perms'] = user.set_perm_perms
+ # Capital X adds execution permissions for directories, not files
+ context['perms_X'] = context['perms'] + 'X'
+ self.append(textwrap.dedent("""\
+ # Grant execution permissions to every parent directory
+ for access_path in %(access_paths)s; do
+ # Preserve existing ACLs
+ acl=$(getfacl -a "$access_path" | grep '^user:%(user)s:') && {
+ perms=$(echo "$acl" | cut -d':' -f3)
+ perms=$(echo "$perms" | cut -c 1,2)x
+ setfacl -m u:%(user)s:$perms "$access_path"
+ } || setfacl -m u:%(user)s:--x "$access_path"
+ done
+ # Grant perms to existing files, excluding execution
+ find '%(perm_to)s' -type f %(exclude_acl)s \\
+ -exec setfacl -m u:%(user)s:%(perms)s {} \\;
+ # Grant perms to extisting directories and set defaults for future content
+ find '%(perm_to)s' -type d %(exclude_acl)s \\
+ -exec setfacl -m u:%(user)s:%(perms_X)s -m d:u:%(user)s:%(perms_X)s {} \\;
+ # Account group as the owner of new files
+ chmod g+s '%(perm_to)s'""") % context
+ )
+ if not user.is_main:
+ self.append(textwrap.dedent("""\
+ # Grant access to main user
+ find '%(perm_to)s' -type d %(exclude_acl)s \\
+ -exec setfacl -m d:u:%(mainuser)s:rwx {} \\;\
+ """) % context
+ )
+
+ def revoke_permissions(self, user, context):
+ revoke_perms = {
+ 'rw': '',
+ 'r': 'w',
+ 'w': 'r',
+ }
+ context.update({
+ 'perms': revoke_perms[user.set_perm_perms],
+ 'option': '-x' if user.set_perm_perms == 'rw' else '-m'
+ })
+ self.append(textwrap.dedent("""\
+ # Revoke permissions
+ find '%(perm_to)s' %(exclude_acl)s \\
+ -exec setfacl %(option)s u:%(user)s:%(perms)s {} \\;\
+ """) % context
+ )
+
+ def set_permission(self, user):
+ context = self.get_context(user)
+ context.update({
+ 'perm_action': user.set_perm_action,
+ 'perm_to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension),
+ })
+ exclude_acl = []
+ for exclude in settings.SYSTEMUSERS_FORBIDDEN_PATHS:
+ context['exclude_acl'] = os.path.join(user.set_perm_base_home, exclude)
+ exclude_acl.append('-not -path "%(exclude_acl)s"' % context)
+ context['exclude_acl'] = ' \\\n -a '.join(exclude_acl) if exclude_acl else ''
+ # Access paths
+ head = user.set_perm_base_home
+ relative = ''
+ access_paths = ["'%s'" % head]
+ for tail in user.set_perm_home_extension.split(os.sep)[:-1]:
+ relative = os.path.join(relative, tail)
+ for exclude in settings.SYSTEMUSERS_FORBIDDEN_PATHS:
+ if fnmatch.fnmatch(relative, exclude):
+ break
+ else:
+ # No match
+ head = os.path.join(head, tail)
+ access_paths.append("'%s'" % head)
+ context['access_paths'] = ' '.join(access_paths)
+
+ if user.set_perm_action == 'grant':
+ self.grant_permissions(user, context)
+ elif user.set_perm_action == 'revoke':
+ self.revoke_permissions(user, context)
+ else:
+ raise NotImplementedError()
+
+ def create_link(self, user):
+ context = self.get_context(user)
+ context.update({
+ 'link_target': user.create_link_target,
+ 'link_name': user.create_link_name,
+ })
+ self.append(textwrap.dedent("""\
+ # Create link
+ su - %(user)s --shell /bin/bash << 'EOF' || exit_code=1
+ if [[ ! -e '%(link_name)s' ]]; then
+ ln -s '%(link_target)s' '%(link_name)s'
+ else
+ echo "%(link_name)s already exists, doing nothing." >&2
+ exit 1
+ fi
+ EOF""") % context
+ )
+
+ def validate_paths_exist(self, user):
+ for path in user.paths_to_validate:
+ context = {
+ 'path': path,
+ }
+ self.append(textwrap.dedent("""
+ if [[ ! -e '%(path)s' ]]; then
+ echo "%(path)s path does not exists." >&2
+ fi""") % context
+ )
+
+ def get_groups(self, user):
+ if user.is_main:
+ groups = list(user.account.systemusers.exclude(username=user.username).values_list('username', flat=True))
+ groups.append("main-systemusers")
+ return groups
+ groups = list(user.groups.values_list('username', flat=True))
+ groups.append("webapp-systemusers")
+ return groups
+
+ def get_context(self, user):
+ context = {
+ 'object_id': user.pk,
+ 'user': user.username,
+ 'group': user.username,
+ 'groups': ','.join(self.get_groups(user)),
+ 'password': user.password if user.active else '*%s' % user.password,
+ 'shell': user.shell,
+ 'mainuser': user.username if user.is_main else user.account.username,
+ 'home': user.get_home(),
+ 'base_home': user.get_base_home(),
+ 'mainuser_home': user.main.get_home(),
+ }
+ context['deleted_home'] = settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH % context
+ return replace(context, "'", '"')