mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Move ssh commands into a subcategory of 'user' + fix a few bugs (#445)
* Move all ssh commands to a subcategory in user... * [fix] Actionmap didn't match functions input ? * [fix] Invalidate nscd cache to propagate new loginShell * Consistency of list-keys even if there's no key..
This commit is contained in:
parent
90e093a482
commit
062ca32eac
3 changed files with 197 additions and 175 deletions
|
@ -203,30 +203,6 @@ user:
|
||||||
extra:
|
extra:
|
||||||
pattern: *pattern_mailbox_quota
|
pattern: *pattern_mailbox_quota
|
||||||
|
|
||||||
### ssh_user_enable_ssh()
|
|
||||||
allow-ssh:
|
|
||||||
action_help: Allow the user to uses ssh
|
|
||||||
api: POST /ssh/user/enable-ssh
|
|
||||||
configuration:
|
|
||||||
authenticate: all
|
|
||||||
arguments:
|
|
||||||
username:
|
|
||||||
help: Username of the user
|
|
||||||
extra:
|
|
||||||
pattern: *pattern_username
|
|
||||||
|
|
||||||
### ssh_user_disable_ssh()
|
|
||||||
disallow-ssh:
|
|
||||||
action_help: Disallow the user to uses ssh
|
|
||||||
api: POST /ssh/user/disable-ssh
|
|
||||||
configuration:
|
|
||||||
authenticate: all
|
|
||||||
arguments:
|
|
||||||
username:
|
|
||||||
help: Username of the user
|
|
||||||
extra:
|
|
||||||
pattern: *pattern_username
|
|
||||||
|
|
||||||
### user_info()
|
### user_info()
|
||||||
info:
|
info:
|
||||||
action_help: Get user information
|
action_help: Get user information
|
||||||
|
@ -238,6 +214,78 @@ user:
|
||||||
username:
|
username:
|
||||||
help: Username or email to get information
|
help: Username or email to get information
|
||||||
|
|
||||||
|
subcategories:
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
subcategory_help: Manage ssh access
|
||||||
|
actions:
|
||||||
|
### user_ssh_enable()
|
||||||
|
allow:
|
||||||
|
action_help: Allow the user to uses ssh
|
||||||
|
api: POST /users/ssh/enable
|
||||||
|
configuration:
|
||||||
|
authenticate: all
|
||||||
|
arguments:
|
||||||
|
username:
|
||||||
|
help: Username of the user
|
||||||
|
extra:
|
||||||
|
pattern: *pattern_username
|
||||||
|
|
||||||
|
### user_ssh_disable()
|
||||||
|
disallow:
|
||||||
|
action_help: Disallow the user to uses ssh
|
||||||
|
api: POST /users/ssh/disable
|
||||||
|
configuration:
|
||||||
|
authenticate: all
|
||||||
|
arguments:
|
||||||
|
username:
|
||||||
|
help: Username of the user
|
||||||
|
extra:
|
||||||
|
pattern: *pattern_username
|
||||||
|
|
||||||
|
### user_ssh_keys_list()
|
||||||
|
list-keys:
|
||||||
|
action_help: Show user's authorized ssh keys
|
||||||
|
api: GET /users/ssh/keys
|
||||||
|
configuration:
|
||||||
|
authenticate: all
|
||||||
|
arguments:
|
||||||
|
username:
|
||||||
|
help: Username of the user
|
||||||
|
extra:
|
||||||
|
pattern: *pattern_username
|
||||||
|
|
||||||
|
### user_ssh_keys_add()
|
||||||
|
add-key:
|
||||||
|
action_help: Add a new authorized ssh key for this user
|
||||||
|
api: POST /users/ssh/key
|
||||||
|
configuration:
|
||||||
|
authenticate: all
|
||||||
|
arguments:
|
||||||
|
username:
|
||||||
|
help: Username of the user
|
||||||
|
extra:
|
||||||
|
pattern: *pattern_username
|
||||||
|
key:
|
||||||
|
help: The key to be added
|
||||||
|
-c:
|
||||||
|
full: --comment
|
||||||
|
help: Optionnal comment about the key
|
||||||
|
|
||||||
|
### user_ssh_keys_remove()
|
||||||
|
remove-key:
|
||||||
|
action_help: Remove an authorized ssh key for this user
|
||||||
|
api: DELETE /users/ssh/key
|
||||||
|
configuration:
|
||||||
|
authenticate: all
|
||||||
|
arguments:
|
||||||
|
username:
|
||||||
|
help: Username of the user
|
||||||
|
extra:
|
||||||
|
pattern: *pattern_username
|
||||||
|
key:
|
||||||
|
help: The key to be removed
|
||||||
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# Domain #
|
# Domain #
|
||||||
|
@ -1349,74 +1397,6 @@ dyndns:
|
||||||
api: DELETE /dyndns/cron
|
api: DELETE /dyndns/cron
|
||||||
|
|
||||||
|
|
||||||
#############################
|
|
||||||
# SSH #
|
|
||||||
#############################
|
|
||||||
ssh:
|
|
||||||
category_help: Manage ssh keys and access
|
|
||||||
actions: {}
|
|
||||||
subcategories:
|
|
||||||
authorized-keys:
|
|
||||||
subcategory_help: Manage user's authorized ssh keys
|
|
||||||
|
|
||||||
actions:
|
|
||||||
### ssh_authorized_keys_list()
|
|
||||||
list:
|
|
||||||
action_help: Show user's authorized ssh keys
|
|
||||||
api: GET /ssh/authorized-keys
|
|
||||||
configuration:
|
|
||||||
authenticate: all
|
|
||||||
arguments:
|
|
||||||
username:
|
|
||||||
help: Username of the user
|
|
||||||
extra:
|
|
||||||
pattern: *pattern_username
|
|
||||||
|
|
||||||
### ssh_authorized_keys_add()
|
|
||||||
add:
|
|
||||||
action_help: Add a new authorized ssh key for this user
|
|
||||||
api: POST /ssh/authorized-keys
|
|
||||||
configuration:
|
|
||||||
authenticate: all
|
|
||||||
arguments:
|
|
||||||
username:
|
|
||||||
help: Username of the user
|
|
||||||
extra:
|
|
||||||
pattern: *pattern_username
|
|
||||||
-u:
|
|
||||||
full: --public
|
|
||||||
help: Public key
|
|
||||||
extra:
|
|
||||||
required: True
|
|
||||||
-i:
|
|
||||||
full: --private
|
|
||||||
help: Private key
|
|
||||||
extra:
|
|
||||||
required: True
|
|
||||||
-n:
|
|
||||||
full: --name
|
|
||||||
help: Key name
|
|
||||||
extra:
|
|
||||||
required: True
|
|
||||||
|
|
||||||
### ssh_authorized_keys_remove()
|
|
||||||
remove:
|
|
||||||
action_help: Remove an authorized ssh key for this user
|
|
||||||
api: DELETE /ssh/authorized-keys
|
|
||||||
configuration:
|
|
||||||
authenticate: all
|
|
||||||
arguments:
|
|
||||||
username:
|
|
||||||
help: Username of the user
|
|
||||||
extra:
|
|
||||||
pattern: *pattern_username
|
|
||||||
-k:
|
|
||||||
full: --key
|
|
||||||
help: Key as a string
|
|
||||||
extra:
|
|
||||||
required: True
|
|
||||||
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# Tools #
|
# Tools #
|
||||||
#############################
|
#############################
|
||||||
|
|
|
@ -1,13 +1,57 @@
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
|
import re
|
||||||
import os
|
import os
|
||||||
|
import errno
|
||||||
|
import pwd
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from moulinette import m18n
|
||||||
|
from moulinette.core import MoulinetteError
|
||||||
from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir
|
from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir
|
||||||
|
|
||||||
from yunohost.user import _get_user_for_ssh
|
SSHD_CONFIG_PATH = "/etc/ssh/sshd_config"
|
||||||
|
|
||||||
|
|
||||||
def ssh_authorized_keys_list(auth, username):
|
def user_ssh_allow(auth, username):
|
||||||
|
"""
|
||||||
|
Allow YunoHost user connect as ssh.
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
username -- User username
|
||||||
|
"""
|
||||||
|
# TODO it would be good to support different kind of shells
|
||||||
|
|
||||||
|
if not _get_user_for_ssh(auth, username):
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username))
|
||||||
|
|
||||||
|
auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/bash'})
|
||||||
|
|
||||||
|
# Somehow this is needed otherwise the PAM thing doesn't forget about the
|
||||||
|
# old loginShell value ?
|
||||||
|
subprocess.call(['nscd', '-i', 'passwd'])
|
||||||
|
|
||||||
|
|
||||||
|
def user_ssh_disallow(auth, username):
|
||||||
|
"""
|
||||||
|
Disallow YunoHost user connect as ssh.
|
||||||
|
|
||||||
|
Keyword argument:
|
||||||
|
username -- User username
|
||||||
|
"""
|
||||||
|
# TODO it would be good to support different kind of shells
|
||||||
|
|
||||||
|
if not _get_user_for_ssh(auth, username):
|
||||||
|
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username))
|
||||||
|
|
||||||
|
auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/false'})
|
||||||
|
|
||||||
|
# Somehow this is needed otherwise the PAM thing doesn't forget about the
|
||||||
|
# old loginShell value ?
|
||||||
|
subprocess.call(['nscd', '-i', 'passwd'])
|
||||||
|
|
||||||
|
|
||||||
|
def user_ssh_list_keys(auth, username):
|
||||||
user = _get_user_for_ssh(auth, username, ["homeDirectory"])
|
user = _get_user_for_ssh(auth, username, ["homeDirectory"])
|
||||||
if not user:
|
if not user:
|
||||||
raise Exception("User with username '%s' doesn't exists" % username)
|
raise Exception("User with username '%s' doesn't exists" % username)
|
||||||
|
@ -15,7 +59,7 @@ def ssh_authorized_keys_list(auth, username):
|
||||||
authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys")
|
authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys")
|
||||||
|
|
||||||
if not os.path.exists(authorized_keys_file):
|
if not os.path.exists(authorized_keys_file):
|
||||||
return []
|
return {"keys": []}
|
||||||
|
|
||||||
keys = []
|
keys = []
|
||||||
last_comment = ""
|
last_comment = ""
|
||||||
|
@ -40,7 +84,7 @@ def ssh_authorized_keys_list(auth, username):
|
||||||
return {"keys": keys}
|
return {"keys": keys}
|
||||||
|
|
||||||
|
|
||||||
def ssh_authorized_keys_add(auth, username, key, comment):
|
def user_ssh_add_key(auth, username, key, comment):
|
||||||
user = _get_user_for_ssh(auth, username, ["homeDirectory", "uid"])
|
user = _get_user_for_ssh(auth, username, ["homeDirectory", "uid"])
|
||||||
if not user:
|
if not user:
|
||||||
raise Exception("User with username '%s' doesn't exists" % username)
|
raise Exception("User with username '%s' doesn't exists" % username)
|
||||||
|
@ -74,8 +118,8 @@ def ssh_authorized_keys_add(auth, username, key, comment):
|
||||||
write_to_file(authorized_keys_file, authorized_keys_content)
|
write_to_file(authorized_keys_file, authorized_keys_content)
|
||||||
|
|
||||||
|
|
||||||
def ssh_authorized_keys_remove(auth, username, key):
|
def user_ssh_remove_key(auth, username, key):
|
||||||
user = _get_user(auth, username, ["homeDirectory", "uid"])
|
user = _get_user_for_ssh(auth, username, ["homeDirectory", "uid"])
|
||||||
if not user:
|
if not user:
|
||||||
raise Exception("User with username '%s' doesn't exists" % username)
|
raise Exception("User with username '%s' doesn't exists" % username)
|
||||||
|
|
||||||
|
@ -100,3 +144,60 @@ def ssh_authorized_keys_remove(auth, username, key):
|
||||||
authorized_keys_content = authorized_keys_content.replace(key, "")
|
authorized_keys_content = authorized_keys_content.replace(key, "")
|
||||||
|
|
||||||
write_to_file(authorized_keys_file, authorized_keys_content)
|
write_to_file(authorized_keys_file, authorized_keys_content)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Helpers
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user_for_ssh(auth, username, attrs=None):
|
||||||
|
def ssh_root_login_status(auth):
|
||||||
|
# XXX temporary placed here for when the ssh_root commands are integrated
|
||||||
|
# extracted from https://github.com/YunoHost/yunohost/pull/345
|
||||||
|
# XXX should we support all the options?
|
||||||
|
# this is the content of "man sshd_config"
|
||||||
|
# PermitRootLogin
|
||||||
|
# Specifies whether root can log in using ssh(1). The argument must be
|
||||||
|
# “yes”, “without-password”, “forced-commands-only”, or “no”. The
|
||||||
|
# default is “yes”.
|
||||||
|
sshd_config_content = read_file(SSHD_CONFIG_PATH)
|
||||||
|
|
||||||
|
if re.search("^ *PermitRootLogin +(no|forced-commands-only) *$",
|
||||||
|
sshd_config_content, re.MULTILINE):
|
||||||
|
return {"PermitRootLogin": False}
|
||||||
|
|
||||||
|
return {"PermitRootLogin": True}
|
||||||
|
|
||||||
|
if username == "root":
|
||||||
|
root_unix = pwd.getpwnam("root")
|
||||||
|
return {
|
||||||
|
'username': 'root',
|
||||||
|
'fullname': '',
|
||||||
|
'mail': '',
|
||||||
|
'ssh_allowed': ssh_root_login_status(auth)["PermitRootLogin"],
|
||||||
|
'shell': root_unix.pw_shell,
|
||||||
|
'home_path': root_unix.pw_dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
if username == "admin":
|
||||||
|
admin_unix = pwd.getpwnam("admin")
|
||||||
|
return {
|
||||||
|
'username': 'admin',
|
||||||
|
'fullname': '',
|
||||||
|
'mail': '',
|
||||||
|
'ssh_allowed': admin_unix.pw_shell.strip() != "/bin/false",
|
||||||
|
'shell': admin_unix.pw_shell,
|
||||||
|
'home_path': admin_unix.pw_dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html
|
||||||
|
user = auth.search('ou=users,dc=yunohost,dc=org',
|
||||||
|
'(&(objectclass=person)(uid=%s))' % username,
|
||||||
|
attrs)
|
||||||
|
|
||||||
|
assert len(user) in (0, 1)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return user[0]
|
||||||
|
|
|
@ -41,9 +41,6 @@ from yunohost.service import service_status
|
||||||
|
|
||||||
logger = getActionLogger('yunohost.user')
|
logger = getActionLogger('yunohost.user')
|
||||||
|
|
||||||
SSHD_CONFIG_PATH = "/etc/ssh/sshd_config"
|
|
||||||
|
|
||||||
|
|
||||||
def user_list(auth, fields=None):
|
def user_list(auth, fields=None):
|
||||||
"""
|
"""
|
||||||
List users
|
List users
|
||||||
|
@ -446,36 +443,30 @@ def user_info(auth, username):
|
||||||
else:
|
else:
|
||||||
raise MoulinetteError(167, m18n.n('user_info_failed'))
|
raise MoulinetteError(167, m18n.n('user_info_failed'))
|
||||||
|
|
||||||
|
#
|
||||||
|
# SSH subcategory
|
||||||
|
#
|
||||||
|
#
|
||||||
|
import yunohost.ssh
|
||||||
|
|
||||||
def user_allow_ssh(auth, username):
|
def user_ssh_allow(auth, username):
|
||||||
"""
|
return yunohost.ssh.user_ssh_allow(auth, username)
|
||||||
Allow YunoHost user connect as ssh.
|
|
||||||
|
|
||||||
Keyword argument:
|
def user_ssh_disallow(auth, username):
|
||||||
username -- User username
|
return yunohost.ssh.user_ssh_disallow(auth, username)
|
||||||
"""
|
|
||||||
# TODO it would be good to support different kind of shells
|
|
||||||
|
|
||||||
if not _get_user_for_ssh(auth, username):
|
def user_ssh_list_keys(auth, username):
|
||||||
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username))
|
return yunohost.ssh.user_ssh_list_keys(auth, username)
|
||||||
|
|
||||||
auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/bash'})
|
def user_ssh_add_key(auth, username, key, comment):
|
||||||
|
return yunohost.ssh.user_ssh_add_key(auth, username, key, comment)
|
||||||
|
|
||||||
|
def user_ssh_remove_key(auth, username, key):
|
||||||
|
return yunohost.ssh.user_ssh_remove_key(auth, username, key)
|
||||||
|
|
||||||
def user_disallow_ssh(auth, username):
|
#
|
||||||
"""
|
# End SSH subcategory
|
||||||
Disallow YunoHost user connect as ssh.
|
#
|
||||||
|
|
||||||
Keyword argument:
|
|
||||||
username -- User username
|
|
||||||
"""
|
|
||||||
# TODO it would be good to support different kind of shells
|
|
||||||
|
|
||||||
if not _get_user_for_ssh(auth, username) :
|
|
||||||
raise MoulinetteError(errno.EINVAL, m18n.n('user_unknown', user=username))
|
|
||||||
|
|
||||||
auth.update('uid=%s,ou=users' % username, {'loginShell': '/bin/false'})
|
|
||||||
|
|
||||||
|
|
||||||
def _convertSize(num, suffix=''):
|
def _convertSize(num, suffix=''):
|
||||||
for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']:
|
for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']:
|
||||||
|
@ -514,54 +505,4 @@ def _hash_user_password(password):
|
||||||
return '{CRYPT}' + crypt.crypt(str(password), salt)
|
return '{CRYPT}' + crypt.crypt(str(password), salt)
|
||||||
|
|
||||||
|
|
||||||
def _get_user_for_ssh(auth, username, attrs=None):
|
|
||||||
def ssh_root_login_status(auth):
|
|
||||||
# XXX temporary placed here for when the ssh_root commands are integrated
|
|
||||||
# extracted from https://github.com/YunoHost/yunohost/pull/345
|
|
||||||
# XXX should we support all the options?
|
|
||||||
# this is the content of "man sshd_config"
|
|
||||||
# PermitRootLogin
|
|
||||||
# Specifies whether root can log in using ssh(1). The argument must be
|
|
||||||
# “yes”, “without-password”, “forced-commands-only”, or “no”. The
|
|
||||||
# default is “yes”.
|
|
||||||
sshd_config_content = read_file(SSHD_CONFIG_PATH)
|
|
||||||
|
|
||||||
if re.search("^ *PermitRootLogin +(no|forced-commands-only) *$",
|
|
||||||
sshd_config_content, re.MULTILINE):
|
|
||||||
return {"PermitRootLogin": False}
|
|
||||||
|
|
||||||
return {"PermitRootLogin": True}
|
|
||||||
|
|
||||||
if username == "root":
|
|
||||||
root_unix = pwd.getpwnam("root")
|
|
||||||
return {
|
|
||||||
'username': 'root',
|
|
||||||
'fullname': '',
|
|
||||||
'mail': '',
|
|
||||||
'ssh_allowed': ssh_root_login_status(auth)["PermitRootLogin"],
|
|
||||||
'shell': root_unix.pw_shell,
|
|
||||||
'home_path': root_unix.pw_dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
if username == "admin":
|
|
||||||
admin_unix = pwd.getpwnam("admin")
|
|
||||||
return {
|
|
||||||
'username': 'admin',
|
|
||||||
'fullname': '',
|
|
||||||
'mail': '',
|
|
||||||
'ssh_allowed': admin_unix.pw_shell.strip() != "/bin/false",
|
|
||||||
'shell': admin_unix.pw_shell,
|
|
||||||
'home_path': admin_unix.pw_dir,
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO escape input using https://www.python-ldap.org/doc/html/ldap-filter.html
|
|
||||||
user = auth.search('ou=users,dc=yunohost,dc=org',
|
|
||||||
'(&(objectclass=person)(uid=%s))' % username,
|
|
||||||
attrs)
|
|
||||||
|
|
||||||
assert len(user) in (0, 1)
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return user[0]
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue