Drop the 'admin' user, have 'admins' be a group of Yunohost users instead

This commit is contained in:
Alexandre Aubin 2022-01-11 14:53:04 +01:00
parent beadea5305
commit 6cae524910
13 changed files with 125 additions and 166 deletions

View file

@ -130,7 +130,6 @@ olcSuffix: dc=yunohost,dc=org
# admin entry below
# These access lines apply to database #1 only
olcAccess: {0}to attrs=userPassword,shadowLastChange
by dn.base="cn=admin,dc=yunohost,dc=org" write
by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write
by anonymous auth
by self write
@ -140,7 +139,6 @@ olcAccess: {0}to attrs=userPassword,shadowLastChange
# owning it if they are authenticated.
# Others should be able to see it.
olcAccess: {1}to attrs=cn,gecos,givenName,mail,maildrop,displayName,sn
by dn.base="cn=admin,dc=yunohost,dc=org" write
by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write
by self write
by * read
@ -160,9 +158,7 @@ olcAccess: {2}to dn.base=""
# The admin dn has full write access, everyone else
# can read everything.
olcAccess: {3}to *
by dn.base="cn=admin,dc=yunohost,dc=org" write
by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" write
by group/groupOfNames/member.exact="cn=admin,ou=groups,dc=yunohost,dc=org" write
by * read
#
olcAddContentAcl: FALSE

View file

@ -5,15 +5,6 @@ objectClass: organization
o: yunohost.org
dc: yunohost
dn: cn=admin,ou=sudo,dc=yunohost,dc=org
cn: admin
objectClass: sudoRole
objectClass: top
sudoCommand: ALL
sudoUser: admin
sudoOption: !authenticate
sudoHost: ALL
dn: ou=users,dc=yunohost,dc=org
objectClass: organizationalUnit
objectClass: top
@ -39,28 +30,30 @@ objectClass: organizationalUnit
objectClass: top
ou: groups
dn: cn=admins,ou=sudo,dc=yunohost,dc=org
cn: admins
objectClass: sudoRole
objectClass: top
sudoCommand: ALL
sudoUser: %admins
sudoHost: ALL
dn: ou=sudo,dc=yunohost,dc=org
objectClass: organizationalUnit
objectClass: top
ou: sudo
dn: cn=admin,dc=yunohost,dc=org
objectClass: organizationalRole
objectClass: posixAccount
objectClass: simpleSecurityObject
cn: admin
uid: admin
uidNumber: 1007
gidNumber: 1007
homeDirectory: /home/admin
loginShell: /bin/bash
userPassword: yunohost
dn: cn=admins,ou=groups,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: top
memberUid: admin
objectClass: groupOfNamesYnh
objectClass: mailGroup
gidNumber: 4001
mail: root
mail: admin
mail: webmaster
mail: postmaster
mail: abuse
cn: admins
dn: cn=all_users,ou=groups,dc=yunohost,dc=org

View file

@ -89,4 +89,7 @@ olcObjectClasses: ( 1.3.6.1.4.1.40328.1.1.2.3
NAME 'mailGroup' SUP top AUXILIARY
DESC 'Mail Group'
MUST ( mail )
MAY (
mailalias $ maildrop
)
)

View file

@ -42,7 +42,7 @@ do_init_regen() {
# Backup folders
mkdir -p /home/yunohost.backup/archives
chmod 750 /home/yunohost.backup/archives
chown root:root /home/yunohost.backup/archives # This is later changed to admin:root once admin user exists
chown root:root /home/yunohost.backup/archives # This is later changed to root:admins once the admins group exists
# Empty ssowat json persistent conf
echo "{}" >'/etc/ssowat/conf.json.persistent'
@ -173,12 +173,11 @@ do_post_regen() {
# Enfore permissions #
######################
chmod 750 /home/admin
chmod 750 /home/yunohost.backup
chmod 750 /home/yunohost.backup/archives
chmod 770 /home/yunohost.backup
chmod 770 /home/yunohost.backup/archives
chmod 700 /var/cache/yunohost
chown admin:root /home/yunohost.backup
chown admin:root /home/yunohost.backup/archives
chown root:admins /home/yunohost.backup
chown root:admins /home/yunohost.backup/archives
chown root:root /var/cache/yunohost
# NB: x permission for 'others' is important for ssl-cert (and maybe mdns), otherwise slapd will fail to start because can't access the certs

View file

@ -58,14 +58,6 @@ EOF
nscd -i passwd || true
systemctl restart slapd
# We don't use mkhomedir_helper because 'admin' may not be recognized
# when this script is ran in a chroot (e.g. ISO install)
# We also refer to admin as uid 1007 for the same reason
if [ ! -d /home/admin ]; then
cp -r /etc/skel /home/admin
chown -R 1007:1007 /home/admin
fi
}
_regenerate_slapd_conf() {
@ -172,22 +164,6 @@ objectClass: top"
echo "Reloading slapd"
systemctl force-reload slapd
# on slow hardware/vm this regen conf would exit before the admin user that
# is stored in ldap is available because ldap seems to slow to restart
# so we'll wait either until we are able to log as admin or until a timeout
# is reached
# we need to do this because the next hooks executed after this one during
# postinstall requires to run as admin thus breaking postinstall on slow
# hardware which mean yunohost can't be correctly installed on those hardware
# and this sucks
# wait a maximum time of 5 minutes
# yes, force-reload behave like a restart
number_of_wait=0
while ! su admin -c '' && ((number_of_wait < 60)); do
sleep 5
((number_of_wait += 1))
done
}
do_$1_regen ${@:2}

View file

@ -6,7 +6,6 @@
"admin_password": "Administration password",
"admin_password_change_failed": "Unable to change password",
"admin_password_changed": "The administration password was changed",
"admin_password_too_long": "Please choose a password shorter than 127 characters",
"already_up_to_date": "Nothing to do. Everything is already up-to-date.",
"app_action_broke_system": "This action seems to have broken these important services: {services}",
"app_action_cannot_be_ran_because_required_services_down": "These required services should be running to run this action: {services}. Try restarting them to continue (and possibly investigate why they are down).",
@ -534,6 +533,7 @@
"password_too_simple_2": "The password needs to be at least 8 characters long and contain a digit, upper and lower characters",
"password_too_simple_3": "The password needs to be at least 8 characters long and contain a digit, upper, lower and special characters",
"password_too_simple_4": "The password needs to be at least 12 characters long and contain a digit, upper, lower and special characters",
"password_too_long": "Please choose a password shorter than 127 characters",
"pattern_backup_archive_name": "Must be a valid filename with max 30 characters, alphanumeric and -_. characters only",
"pattern_domain": "Must be a valid domain name (e.g. my-domain.org)",
"pattern_email": "Must be a valid e-mail address, without '+' symbol (e.g. someone@example.com)",
@ -685,5 +685,5 @@
"yunohost_configured": "YunoHost is now configured",
"yunohost_installing": "Installing YunoHost...",
"yunohost_not_installed": "YunoHost is not correctly installed. Please run 'yunohost tools postinstall'",
"yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - adding a first user through the 'Users' section of the webadmin (or 'yunohost user create <username>' in command-line);\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc."
"yunohost_postinstall_end_tip": "The post-install completed! To finalize your setup, please consider:\n - diagnose potential issues through the 'Diagnosis' section of the webadmin (or 'yunohost diagnosis run' in command-line);\n - reading the 'Finalizing your setup' and 'Getting to know YunoHost' parts in the admin documentation: https://yunohost.org/admindoc."
}

View file

@ -1438,10 +1438,10 @@ tools:
category_help: Specific tools
actions:
### tools_adminpw()
adminpw:
action_help: Change password of admin and root users
api: PUT /adminpw
### tools_rootpw()
rootpw:
action_help: Change root password
api: PUT /rootpw
arguments:
-n:
full: --new-password
@ -1476,6 +1476,25 @@ tools:
ask: ask_main_domain
pattern: *pattern_domain
required: True
-u:
full: --username
help: Username for the first (admin) user
extra:
ask: ask_username
pattern: *pattern_username
required: True
-f:
full: --firstname
extra:
ask: ask_firstname
required: True
pattern: *pattern_firstname
-l:
full: --lastname
extra:
ask: ask_lastname
required: True
pattern: *pattern_lastname
-p:
full: --password
help: YunoHost admin password
@ -1487,14 +1506,10 @@ tools:
--ignore-dyndns:
help: Do not subscribe domain to a DynDNS service
action: store_true
--force-password:
help: Use this if you really want to set a weak password
action: store_true
--force-diskspace:
help: Use this if you really want to install YunoHost on a setup with less than 10 GB on the root filesystem
action: store_true
### tools_update()
update:
action_help: YunoHost update

View file

@ -9,35 +9,45 @@ import time
from moulinette import m18n
from moulinette.authentication import BaseAuthenticator
from yunohost.utils.error import YunohostError
from yunohost.utils.ldap import _get_ldap_interface
logger = logging.getLogger("yunohost.authenticators.ldap_admin")
LDAP_URI = "ldap://localhost:389"
ADMIN_GROUP = "cn=admins,ou=groups,dc=yunohost,dc=org"
AUTH_DN = "uid={uid},ou=users,dc=yunohost,dc=org"
class Authenticator(BaseAuthenticator):
name = "ldap_admin"
def __init__(self, *args, **kwargs):
self.uri = "ldap://localhost:389"
self.basedn = "dc=yunohost,dc=org"
self.admindn = "cn=admin,dc=yunohost,dc=org"
pass
def _authenticate_credentials(self, credentials=None):
# TODO : change authentication format
# to support another dn to support multi-admins
admins = _get_ldap_interface().search(ADMIN_GROUP, attrs=["memberUid"])[0]["memberUid"]
uid, password = credentials.split(":", 1)
if uid not in admins:
raise YunohostError("invalid_credentials")
dn = AUTH_DN.format(uid=uid)
def _reconnect():
con = ldap.ldapobject.ReconnectLDAPObject(
self.uri, retry_max=10, retry_delay=0.5
LDAP_URI, retry_max=10, retry_delay=0.5
)
con.simple_bind_s(self.admindn, credentials)
con.simple_bind_s(dn, password)
return con
try:
con = _reconnect()
except ldap.INVALID_CREDENTIALS:
raise YunohostError("invalid_password")
raise YunohostError("invalid_credentials")
except ldap.SERVER_DOWN:
# ldap is down, attempt to restart it before really failing
logger.warning(m18n.n("ldap_server_is_down_restart_it"))
@ -57,11 +67,8 @@ class Authenticator(BaseAuthenticator):
logger.warning("Error during ldap authentication process: %s", e)
raise
else:
if who != self.admindn:
raise YunohostError(
f"Not logged with the appropriate identity ? Found {who}, expected {self.admindn} !?",
raw_msg=True,
)
if who != dn:
raise YunohostError(f"Not logged with the appropriate identity ? Found {who}, expected {dn} !?", raw_msg=True)
finally:
# Free the connection, we don't really need it to keep it open as the point is only to check authentication...
if con:

View file

@ -342,7 +342,7 @@ class BackupManager:
# FIXME replace isdir by exists ? manage better the case where the path
# exists
if not os.path.isdir(self.work_dir):
filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin")
filesystem.mkdir(self.work_dir, 0o750, parents=True)
elif self.is_tmp_work_dir:
logger.debug(
@ -358,7 +358,7 @@ class BackupManager:
# we're in /home/yunohost.backup/tmp so that should be okay...
# c.f. method clean() which also does this)
filesystem.rm(self.work_dir, recursive=True, force=True)
filesystem.mkdir(self.work_dir, 0o750, parents=True, uid="admin")
filesystem.mkdir(self.work_dir, 0o750, parents=True)
#
# Backup target management #
@ -1886,7 +1886,7 @@ class CopyBackupMethod(BackupMethod):
dest_parent = os.path.dirname(dest)
if not os.path.exists(dest_parent):
filesystem.mkdir(dest_parent, 0o700, True, uid="admin")
filesystem.mkdir(dest_parent, 0o700, True)
if os.path.isdir(source):
shutil.copytree(source, dest)
@ -1948,7 +1948,7 @@ class TarBackupMethod(BackupMethod):
"""
if not os.path.exists(self.repo):
filesystem.mkdir(self.repo, 0o750, parents=True, uid="admin")
filesystem.mkdir(self.repo, 0o750, parents=True)
# Check free space in output
self._check_is_enough_free_space()
@ -2632,9 +2632,9 @@ def _create_archive_dir():
if os.path.lexists(ARCHIVES_PATH):
raise YunohostError("backup_output_symlink_dir_broken", path=ARCHIVES_PATH)
# Create the archive folder, with 'admin' as owner, such that
# Create the archive folder, with 'admins' as groupowner, such that
# people can scp archives out of the server
mkdir(ARCHIVES_PATH, mode=0o750, parents=True, uid="admin", gid="root")
mkdir(ARCHIVES_PATH, mode=0o770, parents=True, gid="admins")
def _call_for_each_path(self, callback, csv_path=None):

View file

@ -158,15 +158,6 @@ def _get_user_for_ssh(username, attrs=None):
"home_path": root_unix.pw_dir,
}
if username == "admin":
admin_unix = pwd.getpwnam("admin")
return {
"username": "admin",
"fullname": "",
"mail": "",
"home_path": admin_unix.pw_dir,
}
# TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html
from yunohost.utils.ldap import _get_ldap_interface

View file

@ -19,10 +19,6 @@
"""
""" yunohost_tools.py
Specific tools
"""
import re
import os
import subprocess
@ -67,63 +63,40 @@ def tools_versions():
return ynh_packages_version()
def tools_adminpw(new_password, check_strength=True):
"""
Change admin password
def tools_rootpw(new_password):
Keyword argument:
new_password
"""
from yunohost.user import _hash_user_password
from yunohost.utils.password import assert_password_is_strong_enough
import spwd
if check_strength:
assert_password_is_strong_enough("admin", new_password)
assert_password_is_strong_enough("admin", new_password)
new_hash = _hash_user_password(new_password)
# UNIX seems to not like password longer than 127 chars ...
# e.g. SSH login gets broken (or even 'su admin' when entering the password)
if len(new_password) >= 127:
raise YunohostValidationError("admin_password_too_long")
new_hash = _hash_user_password(new_password)
from yunohost.utils.ldap import _get_ldap_interface
ldap = _get_ldap_interface()
raise YunohostValidationError("password_too_long")
# Write as root password
try:
ldap.update(
"cn=admin",
{"userPassword": [new_hash]},
)
except Exception as e:
logger.error("unable to change admin password : %s" % e)
raise YunohostError("admin_password_change_failed")
else:
# Write as root password
try:
hash_root = spwd.getspnam("root").sp_pwd
hash_root = spwd.getspnam("root").sp_pwd
with open("/etc/shadow", "r") as before_file:
before = before_file.read()
with open("/etc/shadow", "r") as before_file:
before = before_file.read()
with open("/etc/shadow", "w") as after_file:
after_file.write(
before.replace(
"root:" + hash_root, "root:" + new_hash.replace("{CRYPT}", "")
)
with open("/etc/shadow", "w") as after_file:
after_file.write(
before.replace(
"root:" + hash_root, "root:" + new_hash.replace("{CRYPT}", "")
)
# An IOError may be thrown if for some reason we can't read/write /etc/passwd
# A KeyError could also be thrown if 'root' is not in /etc/passwd in the first place (for example because no password defined ?)
# (c.f. the line about getspnam)
except (IOError, KeyError):
logger.warning(m18n.n("root_password_desynchronized"))
return
logger.info(m18n.n("root_password_replaced_by_admin_password"))
logger.success(m18n.n("admin_password_changed"))
)
# An IOError may be thrown if for some reason we can't read/write /etc/passwd
# A KeyError could also be thrown if 'root' is not in /etc/passwd in the first place (for example because no password defined ?)
# (c.f. the line about getspnam)
except (IOError, KeyError):
logger.warning(m18n.n("root_password_desynchronized"))
return
def tools_maindomain(new_main_domain=None):
@ -189,25 +162,18 @@ def _detect_virt():
def tools_postinstall(
operation_logger,
domain,
username,
firstname,
lastname,
password,
ignore_dyndns=False,
force_password=False,
force_diskspace=False,
):
"""
YunoHost post-install
Keyword argument:
domain -- YunoHost main domain
ignore_dyndns -- Do not subscribe domain to a DynDNS service (only
needed for nohost.me, noho.st domains)
password -- YunoHost admin password
"""
from yunohost.dyndns import _dyndns_available
from yunohost.utils.dns import is_yunohost_dyndns_domain
from yunohost.utils.password import assert_password_is_strong_enough
from yunohost.domain import domain_main_domain
from yunohost.user import user_create
import psutil
# Do some checks at first
@ -230,10 +196,6 @@ def tools_postinstall(
if not force_diskspace and main_space < 10 * GB:
raise YunohostValidationError("postinstall_low_rootfsspace")
# Check password
if not force_password:
assert_password_is_strong_enough("admin", password)
# If this is a nohost.me/noho.st, actually check for availability
if not ignore_dyndns and is_yunohost_dyndns_domain(domain):
# Check if the domain is available...
@ -268,8 +230,10 @@ def tools_postinstall(
domain_add(domain, dyndns)
domain_main_domain(domain)
user_create(username, firstname, lastname, domain, password, admin=True)
# Update LDAP admin and create home dir
tools_adminpw(password, check_strength=not force_password)
tools_rootpw(password)
# Enable UPnP silently and reload firewall
firewall_upnp("enable", no_refresh=True)

View file

@ -55,7 +55,7 @@ FIELDS_FOR_IMPORT = {
"groups": r"^|([a-z0-9_]+(,?[a-z0-9_]+)*)$",
}
FIRST_ALIASES = ["root@", "admin@", "webmaster@", "postmaster@", "abuse@"]
ADMIN_ALIASES = ["root@", "admin@", "webmaster@", "postmaster@", "abuse@"]
def user_list(fields=None):
@ -138,6 +138,7 @@ def user_create(
domain,
password,
mailbox_quota="0",
admin=False,
from_import=False,
):
@ -146,8 +147,13 @@ def user_create(
from yunohost.utils.password import assert_password_is_strong_enough
from yunohost.utils.ldap import _get_ldap_interface
# UNIX seems to not like password longer than 127 chars ...
# e.g. SSH login gets broken (or even 'su admin' when entering the password)
if len(password) >= 127:
raise YunohostValidationError("password_too_long")
# Ensure sufficiently complex password
assert_password_is_strong_enough("user", password)
assert_password_is_strong_enough("admin" if admin else "user", password)
# Validate domain used for email address/xmpp account
if domain is None:
@ -189,9 +195,10 @@ def user_create(
raise YunohostValidationError("system_username_exists")
main_domain = _get_maindomain()
aliases = [alias + main_domain for alias in FIRST_ALIASES]
# FIXME: should forbit root@any.domain, not just main domain?
admin_aliases = [alias + main_domain for alias in ADMIN_ALIASES]
if mail in aliases:
if mail in admin_aliases:
raise YunohostValidationError("mail_unavailable")
if not from_import:
@ -232,10 +239,6 @@ def user_create(
"loginShell": ["/bin/bash"],
}
# If it is the first user, add some aliases
if not ldap.search(base="ou=users,dc=yunohost,dc=org", filter="uid=*"):
attr_dict["mail"] = [attr_dict["mail"]] + aliases
try:
ldap.add("uid=%s,ou=users" % username, attr_dict)
except Exception as e:
@ -263,6 +266,8 @@ def user_create(
# Create group for user and add to group 'all_users'
user_group_create(groupname=username, gid=uid, primary_group=True, sync_perm=False)
user_group_update(groupname="all_users", add=username, force=True, sync_perm=True)
if admin:
user_group_update(groupname="admins", add=username, sync_perm=True)
# Trigger post_user_create hooks
env_dict = {
@ -416,6 +421,12 @@ def user_update(
change_password = Moulinette.prompt(
m18n.n("ask_password"), is_password=True, confirm=True
)
# UNIX seems to not like password longer than 127 chars ...
# e.g. SSH login gets broken (or even 'su admin' when entering the password)
if len(change_password) >= 127:
raise YunohostValidationError("password_too_long")
# Ensure sufficiently complex password
assert_password_is_strong_enough("user", change_password)
@ -424,7 +435,6 @@ def user_update(
if mail:
main_domain = _get_maindomain()
aliases = [alias + main_domain for alias in FIRST_ALIASES]
# If the requested mail address is already as main address or as an alias by this user
if mail in user["mail"]:
@ -439,6 +449,9 @@ def user_update(
raise YunohostError(
"mail_domain_unknown", domain=mail[mail.find("@") + 1 :]
)
# FIXME: should also forbid root@any.domain and not just the main domain
aliases = [alias + main_domain for alias in ADMIN_ALIASES]
if mail in aliases:
raise YunohostValidationError("mail_unavailable")

View file

@ -1144,6 +1144,8 @@ class UserQuestion(Question):
)
if self.default is None:
# FIXME: this code is obsolete with the new admins group
# Should be replaced by something like "any first user we find in the admin group"
root_mail = "root@%s" % _get_maindomain()
for user in self.choices:
if root_mail in user_info(user).get("mail-aliases", []):