groups: add mail-aliases management (#1539)

This commit is contained in:
Alexandre Aubin 2022-12-01 21:34:36 +01:00 committed by GitHub
parent f49c121b8c
commit 0f9d938853
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 157 additions and 49 deletions

View file

@ -47,14 +47,7 @@ dn: cn=admins,ou=groups,dc=yunohost,dc=org
objectClass: posixGroup
objectClass: top
objectClass: groupOfNamesYnh
objectClass: mailGroup
gidNumber: 4001
mail: root
mail: admin
mail: admins
mail: webmaster
mail: postmaster
mail: abuse
cn: admins
dn: cn=all_users,ou=groups,dc=yunohost,dc=org

View file

@ -446,6 +446,8 @@
"group_unknown": "The group '{group}' is unknown",
"group_update_failed": "Could not update the group '{group}': {error}",
"group_updated": "Group '{group}' updated",
"group_update_aliases": "Updating aliases for group '{group}'",
"group_no_change": "Nothing to change for group '{group}'",
"group_user_already_in_group": "User {user} is already in group {group}",
"group_user_not_in_group": "User {user} is not in group {group}",
"hook_exec_failed": "Could not run script: {path}",
@ -519,7 +521,7 @@
"mail_alias_remove_failed": "Could not remove e-mail alias '{mail}'",
"mail_domain_unknown": "Invalid e-mail address for domain '{domain}'. Please, use a domain administrated by this server.",
"mail_forward_remove_failed": "Could not remove e-mail forwarding '{mail}'",
"mail_unavailable": "This e-mail address is reserved and shall be automatically allocated to the very first user",
"mail_unavailable": "This e-mail address is reserved for the admins group",
"mailbox_disabled": "E-mail turned off for user {user}",
"mailbox_used_space_dovecot_down": "The Dovecot mailbox service needs to be up if you want to fetch used mailbox space",
"main_domain_change_failed": "Unable to change the main domain",

View file

@ -321,6 +321,35 @@ user:
extra:
pattern: *pattern_username
add-mailalias:
action_help: Add mail aliases to group
api: PUT /users/groups/<groupname>/aliases/<aliases>
arguments:
groupname:
help: Name of the group to add user(s) to
extra:
pattern: *pattern_groupname
aliases:
help: Mail aliases to add
nargs: "+"
metavar: MAIL
extra:
pattern: *pattern_email
remove-mailalias:
action_help: Remove mail aliases to group
api: DELETE /users/groups/<groupname>/aliases/<aliases>
arguments:
groupname:
help: Name of the group to add user(s) to
extra:
pattern: *pattern_groupname
aliases:
help: Mail aliases to remove
nargs: "+"
metavar: MAIL
permission:
subcategory_help: Manage permissions
actions:

View file

@ -445,6 +445,8 @@ def domain_main_domain(operation_logger, new_main_domain=None):
if not new_main_domain:
return {"current_main_domain": _get_maindomain()}
old_main_domain = _get_maindomain()
# Check domain exists
_assert_domain_exists(new_main_domain)
@ -468,6 +470,9 @@ def domain_main_domain(operation_logger, new_main_domain=None):
if os.path.exists("/etc/yunohost/installed"):
regen_conf()
from yunohost.user import _update_admins_group_aliases
_update_admins_group_aliases(old_main_domain=old_main_domain, new_main_domain=new_main_domain)
logger.success(m18n.n("main_domain_changed"))

View file

@ -22,10 +22,12 @@ class MyMigration(Migration):
@Migration.ldap_migration
def run(self, *args):
from yunohost.user import user_list, user_info, user_group_update, user_update
from yunohost.user import user_list, user_info, user_group_update, user_update, user_group_add_mailalias, ADMIN_ALIASES
from yunohost.utils.ldap import _get_ldap_interface
from yunohost.permission import permission_sync_to_user
from yunohost.domain import _get_maindomain
main_domain = _get_maindomain()
ldap = _get_ldap_interface()
all_users = user_list()["users"].keys()
@ -87,12 +89,13 @@ class MyMigration(Migration):
"cn=admins,ou=groups",
{
"cn": ["admins"],
"objectClass": ["top", "posixGroup", "groupOfNamesYnh", "mailGroup"],
"objectClass": ["top", "posixGroup", "groupOfNamesYnh"],
"gidNumber": ["4001"],
"mail": ["root", "admin", "admins", "webmaster", "postmaster", "abuse"],
},
)
user_group_add_mailalias("admins", [f"{alias}@{main_domain}" for alias in ADMIN_ALIASES])
permission_sync_to_user()
if new_admin_user:

View file

@ -49,7 +49,7 @@ FIELDS_FOR_IMPORT = {
"groups": r"^|([a-z0-9_]+(,?[a-z0-9_]+)*)$",
}
ADMIN_ALIASES = ["root@", "admin@", "admins", "webmaster@", "postmaster@", "abuse@"]
ADMIN_ALIASES = ["root", "admin", "admins", "webmaster", "postmaster", "abuse"]
def user_list(fields=None):
@ -209,11 +209,7 @@ def user_create(
if username in all_existing_usernames:
raise YunohostValidationError("system_username_exists")
main_domain = _get_maindomain()
# FIXME: should forbit root@any.domain, not just main domain?
admin_aliases = [alias + main_domain for alias in ADMIN_ALIASES]
if mail in admin_aliases:
if mail.split("@")[0] in ADMIN_ALIASES:
raise YunohostValidationError("mail_unavailable")
if not from_import:
@ -377,7 +373,7 @@ def user_update(
" ".join(fullname.split()[1:]) or " "
) # Stupid hack because LDAP requires the sn/lastname attr, but it accepts a single whitespace...
from yunohost.domain import domain_list, _get_maindomain
from yunohost.domain import domain_list
from yunohost.app import app_ssowatconf
from yunohost.utils.password import (
assert_password_is_strong_enough,
@ -443,8 +439,6 @@ def user_update(
env_dict["YNH_USER_PASSWORD"] = change_password
if mail:
main_domain = _get_maindomain()
# If the requested mail address is already as main address or as an alias by this user
if mail in user["mail"]:
user["mail"].remove(mail)
@ -459,9 +453,7 @@ def user_update(
"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:
if mail.split("@")[0] in ADMIN_ALIASES:
raise YunohostValidationError("mail_unavailable")
new_attr_dict["mail"] = [mail] + user["mail"][1:]
@ -470,6 +462,9 @@ def user_update(
if not isinstance(add_mailalias, list):
add_mailalias = [add_mailalias]
for mail in add_mailalias:
if mail.split("@")[0] in ADMIN_ALIASES:
raise YunohostValidationError("mail_unavailable")
# (c.f. similar stuff as before)
if mail in user["mail"]:
user["mail"].remove(mail)
@ -1109,22 +1104,15 @@ def user_group_update(
groupname,
add=None,
remove=None,
add_mailalias=None,
remove_mailalias=None,
force=False,
sync_perm=True,
from_import=False,
):
"""
Update user informations
Keyword argument:
groupname -- Groupname to update
add -- User(s) to add in group
remove -- User(s) to remove in group
"""
from yunohost.permission import permission_sync_to_user
from yunohost.utils.ldap import _get_ldap_interface
from yunohost.utils.ldap import _get_ldap_interface, _ldap_path_extract
existing_users = list(user_list()["users"].keys())
@ -1141,53 +1129,119 @@ def user_group_update(
"group_cannot_edit_primary_group", group=groupname
)
ldap = _get_ldap_interface()
# Fetch info for this group
result = ldap.search(
"ou=groups",
"cn=" + groupname,
["cn", "member", "permission", "mail", "objectClass"],
)
if not result:
raise YunohostValidationError("group_unknown", group=groupname)
group = result[0]
# We extract the uid for each member of the group to keep a simple flat list of members
current_group = user_group_info(groupname)["members"]
new_group = copy.copy(current_group)
current_group_mail = group.get("mail", [])
new_group_mail = copy.copy(current_group_mail)
current_group_members = [_ldap_path_extract(p, "uid") for p in group.get("member", [])]
new_group_members = copy.copy(current_group_members)
new_attr_dict = {}
if add:
users_to_add = [add] if not isinstance(add, list) else add
for user in users_to_add:
if user not in existing_users:
raise YunohostValidationError("user_unknown", user=user)
if user in current_group:
if user in current_group_members:
logger.warning(
m18n.n("group_user_already_in_group", user=user, group=groupname)
)
else:
operation_logger.related_to.append(("user", user))
new_group += users_to_add
new_group_members += users_to_add
if remove:
users_to_remove = [remove] if not isinstance(remove, list) else remove
for user in users_to_remove:
if user not in current_group:
if user not in current_group_members:
logger.warning(
m18n.n("group_user_not_in_group", user=user, group=groupname)
)
else:
operation_logger.related_to.append(("user", user))
# Remove users_to_remove from new_group
# Kinda like a new_group -= users_to_remove
new_group = [u for u in new_group if u not in users_to_remove]
# Remove users_to_remove from new_group_members
# Kinda like a new_group_members -= users_to_remove
new_group_members = [u for u in new_group_members if u not in users_to_remove]
new_group_dns = [
"uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group
]
# If something changed, we add this to the stuff to commit later in the code
if set(new_group_members) != set(current_group_members):
new_group_members_dns = [
"uid=" + user + ",ou=users,dc=yunohost,dc=org" for user in new_group_members
]
new_attr_dict["member"] = set(new_group_members_dns)
new_attr_dict["memberUid"] = set(new_group_members)
if set(new_group) != set(current_group):
# Check the whole alias situation
if add_mailalias:
from yunohost.domain import domain_list
domains = domain_list()["domains"]
if not isinstance(add_mailalias, list):
add_mailalias = [add_mailalias]
for mail in add_mailalias:
if mail.split("@")[0] in ADMIN_ALIASES and groupname != "admins":
raise YunohostValidationError("mail_unavailable")
if mail in current_group_mail:
continue
try:
ldap.validate_uniqueness({"mail": mail})
except Exception as e:
raise YunohostError("group_update_failed", group=groupname, error=e)
if mail[mail.find("@") + 1 :] not in domains:
raise YunohostError(
"mail_domain_unknown", domain=mail[mail.find("@") + 1 :]
)
new_group_mail.append(mail)
if remove_mailalias:
from yunohost.domain import _get_maindomain
if not isinstance(remove_mailalias, list):
remove_mailalias = [remove_mailalias]
for mail in remove_mailalias:
if "@" in mail and mail.split("@")[0] in ADMIN_ALIASES and groupname == "admins" and mail.split("@")[1] == _get_maindomain():
raise YunohostValidationError(f"The alias {mail} can not be removed from the 'admins' group", raw_msg=True)
if mail in new_group_mail:
new_group_mail.remove(mail)
else:
raise YunohostValidationError("mail_alias_remove_failed", mail=mail)
if set(new_group_mail) != set(current_group_mail):
logger.info(m18n.n("group_update_aliases", group=groupname))
new_attr_dict["mail"] = set(new_group_mail)
if new_attr_dict["mail"] and "mailAccount" not in group["objectClass"]:
new_attr_dict["objectClass"] = group["objectClass"] + ["mailAccount"]
elif not new_attr_dict["mail"] and "mailAccount" in group["objectClass"]:
new_attr_dict["objectClass"] = [c for c in group["objectClass"] if c != "mailAccount"]
if new_attr_dict:
if not from_import:
operation_logger.start()
ldap = _get_ldap_interface()
try:
ldap.update(
f"cn={groupname},ou=groups",
{"member": set(new_group_dns), "memberUid": set(new_group)},
new_attr_dict
)
except Exception as e:
raise YunohostError("group_update_failed", group=groupname, error=e)
@ -1197,7 +1251,10 @@ def user_group_update(
if not from_import:
if groupname != "all_users":
logger.success(m18n.n("group_updated", group=groupname))
if not new_attr_dict:
logger.info(m18n.n("group_no_change", group=groupname))
else:
logger.success(m18n.n("group_updated", group=groupname))
else:
logger.debug(m18n.n("group_updated", group=groupname))
@ -1221,7 +1278,7 @@ def user_group_info(groupname):
result = ldap.search(
"ou=groups",
"cn=" + groupname,
["cn", "member", "permission"],
["cn", "member", "permission", "mail"],
)
if not result:
@ -1236,6 +1293,7 @@ def user_group_info(groupname):
"permissions": [
_ldap_path_extract(p, "cn") for p in infos.get("permission", [])
],
"mail-aliases": [m for m in infos.get("mail", [])]
}
@ -1265,6 +1323,13 @@ def user_group_remove(groupname, usernames, force=False, sync_perm=True):
)
def user_group_add_mailalias(groupname, aliases):
return user_group_update(groupname, add_mailalias=aliases, sync_perm=False)
def user_group_remove_mailalias(groupname, aliases):
return user_group_update(groupname, remove_mailalias=aliases, sync_perm=False)
#
# Permission subcategory
#
@ -1364,3 +1429,14 @@ def _hash_user_password(password):
salt = "$6$" + salt + "$"
return "{CRYPT}" + crypt.crypt(str(password), salt)
def _update_admins_group_aliases(old_main_domain, new_main_domain):
current_admin_aliases = user_group_info("admins")["mail-aliases"]
aliases_to_remove = [a for a in current_admin_aliases \
if "@" in a and a.split("@")[1] == old_main_domain and a.split("@")[0] in ADMIN_ALIASES]
aliases_to_add = [f"{a}@{new_main_domain}" for a in ADMIN_ALIASES]
user_group_update("admins", add_mailalias=aliases_to_add, remove_mailalias=aliases_to_remove)