Merge branch 'unstable' into NO_BACKUP_UPGRADE

This commit is contained in:
Maniack Crudelis 2018-02-22 11:51:27 +01:00 committed by GitHub
commit b3c600ac1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 519 additions and 111 deletions

View file

@ -203,6 +203,30 @@ user:
extra:
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()
info:
action_help: Get user information
@ -1325,6 +1349,74 @@ dyndns:
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 #
#############################

View file

@ -64,9 +64,13 @@ ynh_remove_logrotate () {
# Create a dedicated systemd config
#
# This will use a template in ../conf/systemd.service
# and will replace the following keywords with
# global variables that should be defined before calling
# usage: ynh_add_systemd_config [Service name] [Template name]
# | arg: Service name (optionnal, $app by default)
# | arg: Name of template file (optionnal, this is 'systemd' by default, meaning ./conf/systemd.service will be used as template)
#
# This will use the template ../conf/<templatename>.service
# to generate a systemd config, by replacing the following keywords
# with global variables that should be defined before calling
# this helper :
#
# __APP__ by $app
@ -74,9 +78,11 @@ ynh_remove_logrotate () {
#
# usage: ynh_add_systemd_config
ynh_add_systemd_config () {
finalsystemdconf="/etc/systemd/system/$app.service"
local service_name="${1:-$app}"
finalsystemdconf="/etc/systemd/system/$service_name.service"
ynh_backup_if_checksum_is_different "$finalsystemdconf"
sudo cp ../conf/systemd.service "$finalsystemdconf"
sudo cp ../conf/${2:-systemd.service} "$finalsystemdconf"
# To avoid a break by set -u, use a void substitution ${var:-}. If the variable is not set, it's simply set with an empty variable.
# Substitute in a nginx config file only if the variable is not empty
@ -89,19 +95,25 @@ ynh_add_systemd_config () {
ynh_store_file_checksum "$finalsystemdconf"
sudo chown root: "$finalsystemdconf"
sudo systemctl enable $app
sudo systemctl enable $service_name
sudo systemctl daemon-reload
}
# Remove the dedicated systemd config
#
# usage: ynh_remove_systemd_config [Service name]
# | arg: Service name (optionnal, $app by default)
#
# usage: ynh_remove_systemd_config
ynh_remove_systemd_config () {
local finalsystemdconf="/etc/systemd/system/$app.service"
local service_name="${1:-$app}"
local finalsystemdconf="/etc/systemd/system/$service_name.service"
if [ -e "$finalsystemdconf" ]; then
sudo systemctl stop $app
sudo systemctl disable $app
sudo systemctl stop $service_name
sudo systemctl disable $service_name
ynh_secure_remove "$finalsystemdconf"
sudo systemctl daemon-reload
fi
}
@ -157,7 +169,17 @@ ynh_remove_nginx_config () {
#
# usage: ynh_add_fpm_config
ynh_add_fpm_config () {
finalphpconf="/etc/php5/fpm/pool.d/$app.conf"
# Configure PHP-FPM 7.0 by default
local fpm_config_dir="/etc/php/7.0/fpm"
local fpm_service="php7.0-fpm"
# Configure PHP-FPM 5 on Debian Jessie
if [ "$(ynh_get_debian_release)" == "jessie" ]; then
fpm_config_dir="/etc/php5/fpm"
fpm_service="php5-fpm"
fi
ynh_app_setting_set $app fpm_config_dir "$fpm_config_dir"
ynh_app_setting_set $app fpm_service "$fpm_service"
finalphpconf="$fpm_config_dir/pool.d/$app.conf"
ynh_backup_if_checksum_is_different "$finalphpconf"
sudo cp ../conf/php-fpm.conf "$finalphpconf"
ynh_replace_string "__NAMETOCHANGE__" "$app" "$finalphpconf"
@ -168,21 +190,27 @@ ynh_add_fpm_config () {
if [ -e "../conf/php-fpm.ini" ]
then
finalphpini="/etc/php5/fpm/conf.d/20-$app.ini"
finalphpini="$fpm_config_dir/conf.d/20-$app.ini"
ynh_backup_if_checksum_is_different "$finalphpini"
sudo cp ../conf/php-fpm.ini "$finalphpini"
sudo chown root: "$finalphpini"
ynh_store_file_checksum "$finalphpini"
fi
sudo systemctl reload php5-fpm
sudo systemctl reload $fpm_service
}
# Remove the dedicated php-fpm config
#
# usage: ynh_remove_fpm_config
ynh_remove_fpm_config () {
ynh_secure_remove "/etc/php5/fpm/pool.d/$app.conf"
ynh_secure_remove "/etc/php5/fpm/conf.d/20-$app.ini" 2>&1
sudo systemctl reload php5-fpm
local fpm_config_dir=$(ynh_app_setting_get $app fpm_config_dir)
local fpm_service=$(ynh_app_setting_get $app fpm_service)
# Assume php version 5 if not set
if [ -z "$fpm_config_dir" ]; then
fpm_config_dir="/etc/php5/fpm"
fpm_service="php5-fpm"
fi
ynh_secure_remove "$fpm_config_dir/pool.d/$app.conf"
ynh_secure_remove "$fpm_config_dir/conf.d/20-$app.ini" 2>&1
sudo systemctl reload $fpm_service
}

View file

@ -41,3 +41,10 @@ ynh_abort_if_errors () {
set -eu # Exit if a command fail, and if a variable is used unset.
trap ynh_exit_properly EXIT # Capturing exit signals on shell script
}
# Return the Debian release codename (i.e. jessie, stretch, etc.)
#
# usage: ynh_get_debian_release
ynh_get_debian_release () {
echo $(lsb_release --codename --short)
}

View file

@ -37,6 +37,7 @@ ynh_get_plain_key() {
ynh_restore_upgradebackup () {
echo "Upgrade failed." >&2
local app_bck=${app//_/-} # Replace all '_' by '-'
NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0}
if [ "$NO_BACKUP_UPGRADE" -eq 0 ]
@ -47,7 +48,7 @@ ynh_restore_upgradebackup () {
# Remove the application then restore it
sudo yunohost app remove $app
# Restore the backup
sudo yunohost backup restore --ignore-system $app_bck-pre-upgrade$backup_number --apps $app --force
sudo yunohost backup restore --ignore-system $app_bck-pre-upgrade$backup_number --apps $app --force --verbose
ynh_die "The app was restored to the way it was before the failed upgrade."
fi
else
@ -86,7 +87,7 @@ ynh_backup_before_upgrade () {
fi
# Create backup
sudo yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number
sudo yunohost backup create --ignore-system --apps $app --name $app_bck-pre-upgrade$backup_number --verbose
if [ "$?" -eq 0 ]
then
# If the backup succeeded, remove the previous backup

View file

@ -53,6 +53,9 @@ do_pre_regen() {
else
sudo cp services.yml /etc/yunohost/services.yml
fi
mkdir -p "$pending_dir"/etc/etckeeper/
cp etckeeper.conf "$pending_dir"/etc/etckeeper/
}
_update_services() {

View file

@ -1 +1,2 @@
server_tokens off;
gzip_types text/css text/javascript application/javascript;

View file

@ -37,7 +37,17 @@ server {
# > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048
#ssl_dhparam /etc/ssl/private/dh2048.pem;
add_header Strict-Transport-Security "max-age=31536000;";
# Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
# https://observatory.mozilla.org/
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header 'Referrer-Policy' 'same-origin';
add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval'";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Download-Options noopen;
add_header X-Permitted-Cross-Domain-Policies none;
add_header X-Frame-Options "SAMEORIGIN";
location / {
return 302 https://$http_host/yunohost/admin;

View file

@ -1,4 +1,7 @@
location /yunohost/admin {
# Avoid the nginx path/alias traversal weakness ( #1037 )
rewrite ^/yunohost/admin$ /yunohost/admin/ permanent;
location /yunohost/admin/ {
alias /usr/share/yunohost/admin/;
default_type text/html;
index index.html;

View file

@ -42,7 +42,16 @@ server {
# > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048
#ssl_dhparam /etc/ssl/private/dh2048.pem;
add_header Strict-Transport-Security "max-age=31536000;";
# Follows the Web Security Directives from the Mozilla Dev Lab and the Mozilla Obervatory + Partners
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
# https://observatory.mozilla.org/
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header Content-Security-Policy "upgrade-insecure-requests; object-src 'none'; script-src https: 'unsafe-eval'";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Download-Options noopen;
add_header X-Permitted-Cross-Domain-Policies none;
add_header X-Frame-Options "SAMEORIGIN";
access_by_lua_file /usr/share/ssowat/access.lua;

View file

@ -0,0 +1,43 @@
# The VCS to use.
#VCS="hg"
VCS="git"
#VCS="bzr"
#VCS="darcs"
# Options passed to git commit when run by etckeeper.
GIT_COMMIT_OPTIONS="--quiet"
# Options passed to hg commit when run by etckeeper.
HG_COMMIT_OPTIONS=""
# Options passed to bzr commit when run by etckeeper.
BZR_COMMIT_OPTIONS=""
# Options passed to darcs record when run by etckeeper.
DARCS_COMMIT_OPTIONS="-a"
# Uncomment to avoid etckeeper committing existing changes
# to /etc automatically once per day.
#AVOID_DAILY_AUTOCOMMITS=1
# Uncomment the following to avoid special file warning
# (the option is enabled automatically by cronjob regardless).
#AVOID_SPECIAL_FILE_WARNING=1
# Uncomment to avoid etckeeper committing existing changes to
# /etc before installation. It will cancel the installation,
# so you can commit the changes by hand.
#AVOID_COMMIT_BEFORE_INSTALL=1
# The high-level package manager that's being used.
# (apt, pacman-g2, yum, zypper etc)
HIGHLEVEL_PACKAGE_MANAGER=apt
# The low-level package manager that's being used.
# (dpkg, rpm, pacman, pacman-g2, etc)
LOWLEVEL_PACKAGE_MANAGER=dpkg
# To push each commit to a remote, put the name of the remote here.
# (eg, "origin" for git). Space-separated lists of multiple remotes
# also work (eg, "origin gitlab github" for git).
PUSH_REMOTE=""

2
debian/control vendored
View file

@ -18,7 +18,7 @@ Depends: ${python:Depends}, ${misc:Depends}
, ca-certificates, netcat-openbsd, iproute
, mariadb-server | mysql-server, php5-mysql | php5-mysqlnd
, slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd
, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail
, postfix-ldap, postfix-policyd-spf-perl, postfix-pcre, procmail, mailutils
, dovecot-ldap, dovecot-lmtpd, dovecot-managesieved
, dovecot-antispam, fail2ban
, nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl

2
locales/ar.json Normal file
View file

@ -0,0 +1,2 @@
{
}

View file

@ -14,6 +14,7 @@
"app_change_url_identical_domains": "The old and new domain/url_path are identical ('{domain:s}{path:s}'), nothing to do.",
"app_change_url_no_script": "This application '{app_name:s}' doesn't support url modification yet. Maybe you should upgrade the application.",
"app_change_url_success": "Successfully changed {app:s} url to {domain:s}{path:s}",
"app_checkurl_is_deprecated": "Packagers /!\\ 'app checkurl' is deprecated ! Please use 'app register-url' instead !",
"app_extraction_failed": "Unable to extract installation files",
"app_id_invalid": "Invalid app id",
"app_incompatible": "The app {app} is incompatible with your YunoHost version",

View file

@ -432,7 +432,7 @@ def app_change_url(auth, app, domain, path):
path -- New path at which the application will be move
"""
from yunohost.hook import hook_exec
from yunohost.hook import hook_exec, hook_callback
installed = _is_installed(app)
if not installed:
@ -485,6 +485,12 @@ def app_change_url(auth, app, domain, path):
shutil.copytree(os.path.join(APPS_SETTING_PATH, app, "scripts"),
os.path.join(APP_TMP_FOLDER, "scripts"))
if os.path.exists(os.path.join(APP_TMP_FOLDER, "conf")):
shutil.rmtree(os.path.join(APP_TMP_FOLDER, "conf"))
shutil.copytree(os.path.join(APPS_SETTING_PATH, app, "conf"),
os.path.join(APP_TMP_FOLDER, "conf"))
# Execute App change_url script
os.system('chown -R admin: %s' % INSTALL_TMP)
os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts")))
@ -521,6 +527,8 @@ def app_change_url(auth, app, domain, path):
logger.success(m18n.n("app_change_url_success",
app=app, domain=domain, path=path))
hook_callback('post_app_change_url', args=args_list, env=env_dict)
def app_upgrade(auth, app=[], url=None, file=None):
"""
@ -532,7 +540,8 @@ def app_upgrade(auth, app=[], url=None, file=None):
url -- Git url to fetch for upgrade
"""
from yunohost.hook import hook_add, hook_remove, hook_exec
from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
# Retrieve interface
is_api = msettings.get('interface') == 'api'
@ -635,6 +644,9 @@ def app_upgrade(auth, app=[], url=None, file=None):
upgraded_apps.append(app_instance_name)
logger.success(m18n.n('app_upgraded', app=app_instance_name))
hook_callback('post_app_upgrade', args=args_list, env=env_dict)
if not upgraded_apps:
raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade'))
@ -658,7 +670,7 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False):
no_remove_on_failure -- Debug option to avoid removing the app on a failed installation
"""
from yunohost.hook import hook_add, hook_remove, hook_exec
from yunohost.hook import hook_add, hook_remove, hook_exec, hook_callback
# Fetch or extract sources
try:
@ -800,6 +812,8 @@ def app_install(auth, app, label=None, args=None, no_remove_on_failure=False):
logger.success(m18n.n('installation_complete'))
hook_callback('post_app_install', args=args_list, env=env_dict)
def app_remove(auth, app):
"""
@ -809,7 +823,7 @@ def app_remove(auth, app):
app -- App(s) to delete
"""
from yunohost.hook import hook_exec, hook_remove
from yunohost.hook import hook_exec, hook_remove, hook_callback
if not _is_installed(app):
raise MoulinetteError(errno.EINVAL,
@ -838,6 +852,8 @@ def app_remove(auth, app):
if hook_exec('/tmp/yunohost_remove/scripts/remove', args=args_list, env=env_dict, user="root") == 0:
logger.success(m18n.n('app_removed', app=app))
hook_callback('post_app_remove', args=args_list, env=env_dict)
if os.path.exists(app_setting_path):
shutil.rmtree(app_setting_path)
shutil.rmtree('/tmp/yunohost_remove')
@ -1149,6 +1165,9 @@ def app_checkurl(auth, url, app=None):
app -- Write domain & path to app settings for further checks
"""
logger.warning(m18n.n("app_checkurl_is_deprecated"))
from yunohost.domain import domain_list
if "https://" == url[:8]:

View file

@ -1543,6 +1543,10 @@ class BackupMethod(object):
# Can create a hard link only if files are on the same fs
# (i.e. we can't if it's on a different fs)
if os.stat(src).st_dev == os.stat(dest_dir).st_dev:
# Don't hardlink /etc/cron.d files to avoid cron bug
# 'NUMBER OF HARD LINKS > 1' see #1043
cron_path = os.path.abspath('/etc/cron') + '.'
if not os.path.abspath(src).startswith(cron_path):
os.link(src, dest)
# Success, go to next file to organize
continue

View file

@ -44,6 +44,7 @@ from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
import yunohost.domain
from yunohost.utils.network import get_public_ip
from moulinette import m18n
from yunohost.app import app_ssowatconf
@ -809,7 +810,7 @@ def _backup_current_cert(domain):
def _check_domain_is_ready_for_ACME(domain):
public_ip = yunohost.domain.get_public_ip()
public_ip = get_public_ip()
# Check if IP from DNS matches public IP
if not _dns_ip_match_public_ip(public_ip, domain):
@ -856,14 +857,9 @@ def _regen_dnsmasq_if_needed():
"""
Update the dnsmasq conf if some IPs are not up to date...
"""
try:
ipv4 = yunohost.domain.get_public_ip()
except:
ipv4 = None
try:
ipv6 = yunohost.domain.get_public_ip(6)
except:
ipv6 = None
ipv4 = get_public_ip()
ipv6 = get_public_ip(6)
do_regen = False

View file

@ -30,8 +30,6 @@ import yaml
import errno
import requests
from urllib import urlopen
from moulinette import m18n, msettings
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
@ -39,6 +37,7 @@ from moulinette.utils.log import getActionLogger
import yunohost.certificate
from yunohost.service import service_regen_conf
from yunohost.utils.network import get_public_ip
logger = getActionLogger('yunohost.domain')
@ -260,42 +259,6 @@ def domain_url_available(auth, domain, path):
return available
def get_public_ip(protocol=4):
"""Retrieve the public IP address from ip.yunohost.org"""
if protocol == 4:
url = 'https://ip.yunohost.org'
elif protocol == 6:
url = 'https://ip6.yunohost.org'
else:
raise ValueError("invalid protocol version")
try:
return urlopen(url).read().strip()
except IOError:
logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1)
raise MoulinetteError(errno.ENETUNREACH,
m18n.n('no_internet_connection'))
def get_public_ips():
"""
Retrieve the public IPv4 and v6 from ip. and ip6.yunohost.org
Returns a 2-tuple (ipv4, ipv6). ipv4 or ipv6 can be None if they were not
found.
"""
try:
ipv4 = get_public_ip()
except:
ipv4 = None
try:
ipv6 = get_public_ip(6)
except:
ipv6 = None
return (ipv4, ipv6)
def _get_maindomain():
with open('/etc/yunohost/current_host', 'r') as f:
maindomain = f.readline().rstrip()
@ -356,15 +319,8 @@ def _build_dns_conf(domain, ttl=3600):
}
"""
try:
ipv4 = get_public_ip()
except:
ipv4 = None
try:
ipv6 = get_public_ip(6)
except:
ipv6 = None
basic = []

View file

@ -39,7 +39,8 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file, write_to_file, rm
from moulinette.utils.network import download_json
from yunohost.domain import get_public_ips, _get_maindomain, _build_dns_conf
from yunohost.domain import _get_maindomain, _build_dns_conf
from yunohost.utils.network import get_public_ip
logger = getActionLogger('yunohost.dyndns')
@ -193,7 +194,8 @@ def dyndns_update(dyn_host="dyndns.yunohost.org", domain=None, key=None,
old_ipv6 = read_file(OLD_IPV6_FILE).rstrip()
# Get current IPv4 and IPv6
(ipv4_, ipv6_) = get_public_ips()
ipv4_ = get_public_ip()
ipv6_ = get_public_ip(6)
if ipv4 is None:
ipv4 = ipv4_

View file

@ -41,7 +41,8 @@ from moulinette import m18n
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
from yunohost.domain import get_public_ip, _get_maindomain
from yunohost.utils.network import get_public_ip
from yunohost.domain import _get_maindomain
logger = getActionLogger('yunohost.monitor')
@ -210,10 +211,7 @@ def monitor_network(units=None, human_readable=False):
else:
logger.debug('interface name %s was not found', iname)
elif u == 'infos':
try:
p_ipv4 = get_public_ip()
except:
p_ipv4 = 'unknown'
p_ipv4 = get_public_ip() or 'unknown'
l_ip = 'unknown'
for name, addrs in devices.items():

102
src/yunohost/ssh.py Normal file
View file

@ -0,0 +1,102 @@
# encoding: utf-8
import os
from moulinette.utils.filesystem import read_file, write_to_file, chown, chmod, mkdir
from yunohost.user import _get_user_for_ssh
def ssh_authorized_keys_list(auth, username):
user = _get_user_for_ssh(auth, username, ["homeDirectory"])
if not user:
raise Exception("User with username '%s' doesn't exists" % username)
authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys")
if not os.path.exists(authorized_keys_file):
return []
keys = []
last_comment = ""
for line in read_file(authorized_keys_file).split("\n"):
# empty line
if not line.strip():
continue
if line.lstrip().startswith("#"):
last_comment = line.lstrip().lstrip("#").strip()
continue
# assuming a key per non empty line
key = line.strip()
keys.append({
"key": key,
"name": last_comment,
})
last_comment = ""
return {"keys": keys}
def ssh_authorized_keys_add(auth, username, key, comment):
user = _get_user_for_ssh(auth, username, ["homeDirectory", "uid"])
if not user:
raise Exception("User with username '%s' doesn't exists" % username)
authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys")
if not os.path.exists(authorized_keys_file):
# ensure ".ssh" exists
mkdir(os.path.join(user["homeDirectory"][0], ".ssh"),
force=True, parents=True, uid=user["uid"][0])
# create empty file to set good permissions
write_to_file(authorized_keys_file, "")
chown(authorized_keys_file, uid=user["uid"][0])
chmod(authorized_keys_file, 0600)
authorized_keys_content = read_file(authorized_keys_file)
authorized_keys_content += "\n"
authorized_keys_content += "\n"
if comment and comment.strip():
if not comment.lstrip().startswith("#"):
comment = "# " + comment
authorized_keys_content += comment.replace("\n", " ").strip()
authorized_keys_content += "\n"
authorized_keys_content += key.strip()
authorized_keys_content += "\n"
write_to_file(authorized_keys_file, authorized_keys_content)
def ssh_authorized_keys_remove(auth, username, key):
user = _get_user(auth, username, ["homeDirectory", "uid"])
if not user:
raise Exception("User with username '%s' doesn't exists" % username)
authorized_keys_file = os.path.join(user["homeDirectory"][0], ".ssh", "authorized_keys")
if not os.path.exists(authorized_keys_file):
raise Exception("this key doesn't exists ({} dosesn't exists)".format(authorized_keys_file))
authorized_keys_content = read_file(authorized_keys_file)
if key not in authorized_keys_content:
raise Exception("Key '{}' is not present in authorized_keys".format(key))
# don't delete the previous comment because we can't verify if it's legit
# this regex approach failed for some reasons and I don't know why :(
# authorized_keys_content = re.sub("{} *\n?".format(key),
# "",
# authorized_keys_content,
# flags=re.MULTILINE)
authorized_keys_content = authorized_keys_content.replace(key, "")
write_to_file(authorized_keys_file, authorized_keys_content)

View file

@ -33,8 +33,9 @@ import logging
import subprocess
import pwd
import socket
from collections import OrderedDict
from xmlrpclib import Fault
from importlib import import_module
from collections import OrderedDict
import apt
import apt.progress
@ -45,12 +46,13 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output
from moulinette.utils.filesystem import read_json, write_to_json
from yunohost.app import app_fetchlist, app_info, app_upgrade, app_ssowatconf, app_list, _install_appslist_fetch_cron
from yunohost.domain import domain_add, domain_list, get_public_ip, _get_maindomain, _set_maindomain
from yunohost.domain import domain_add, domain_list, _get_maindomain, _set_maindomain
from yunohost.dyndns import _dyndns_available, _dyndns_provides
from yunohost.firewall import firewall_upnp
from yunohost.service import service_status, service_regen_conf, service_log, service_start, service_enable
from yunohost.monitor import monitor_disk, monitor_system
from yunohost.utils.packages import ynh_packages_version
from yunohost.utils.network import get_public_ip
# FIXME this is a duplicate from apps.py
APPS_SETTING_PATH = '/etc/yunohost/apps/'
@ -568,7 +570,7 @@ def tools_diagnosis(auth, private=False):
diagnosis['system'] = OrderedDict()
try:
disks = monitor_disk(units=['filesystem'], human_readable=True)
except MoulinetteError as e:
except (MoulinetteError, Fault) as e:
logger.warning(m18n.n('diagnosis_monitor_disk_error', error=format(e)), exc_info=1)
else:
diagnosis['system']['disks'] = {}
@ -621,16 +623,11 @@ def tools_diagnosis(auth, private=False):
# Private data
if private:
diagnosis['private'] = OrderedDict()
# Public IP
diagnosis['private']['public_ip'] = {}
try:
diagnosis['private']['public_ip']['IPv4'] = get_public_ip(4)
except MoulinetteError as e:
pass
try:
diagnosis['private']['public_ip']['IPv6'] = get_public_ip(6)
except MoulinetteError as e:
pass
# Domains
diagnosis['private']['domains'] = domain_list(auth)['domains']

View file

@ -25,6 +25,7 @@
"""
import os
import re
import pwd
import json
import errno
import crypt
@ -35,10 +36,13 @@ import subprocess
from moulinette import m18n
from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger
from moulinette.utils.filesystem import read_file
from yunohost.service import service_status
logger = getActionLogger('yunohost.user')
SSHD_CONFIG_PATH = "/etc/ssh/sshd_config"
def user_list(auth, fields=None):
"""
@ -56,6 +60,8 @@ def user_list(auth, fields=None):
'cn': 'fullname',
'mail': 'mail',
'maildrop': 'mail-forward',
'loginShell': 'shell',
'homeDirectory': 'home_path',
'mailuserquota': 'mailbox-quota'
}
@ -71,7 +77,7 @@ def user_list(auth, fields=None):
raise MoulinetteError(errno.EINVAL,
m18n.n('field_invalid', attr))
else:
attrs = ['uid', 'cn', 'mail', 'mailuserquota']
attrs = ['uid', 'cn', 'mail', 'mailuserquota', 'loginShell']
result = auth.search('ou=users,dc=yunohost,dc=org',
'(&(objectclass=person)(!(uid=root))(!(uid=nobody)))',
@ -81,6 +87,12 @@ def user_list(auth, fields=None):
entry = {}
for attr, values in user.items():
if values:
if attr == "loginShell":
if values[0].strip() == "/bin/false":
entry["ssh_allowed"] = False
else:
entry["ssh_allowed"] = True
entry[user_attrs[attr]] = values[0]
uid = entry[user_attrs['uid']]
@ -435,6 +447,36 @@ def user_info(auth, username):
raise MoulinetteError(167, m18n.n('user_info_failed'))
def user_allow_ssh(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'})
def user_disallow_ssh(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'})
def _convertSize(num, suffix=''):
for unit in ['K', 'M', 'G', 'T', 'P', 'E', 'Z']:
if abs(num) < 1024.0:
@ -470,3 +512,56 @@ def _hash_user_password(password):
salt = '$6$' + 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]

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
""" License
Copyright (C) 2017 YUNOHOST.ORG
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses
"""
import logging
from urllib import urlopen
logger = logging.getLogger('yunohost.utils.network')
def get_public_ip(protocol=4):
"""Retrieve the public IP address from ip.yunohost.org"""
if protocol == 4:
url = 'https://ip.yunohost.org'
elif protocol == 6:
url = 'https://ip6.yunohost.org'
else:
raise ValueError("invalid protocol version")
try:
return urlopen(url).read().strip()
except IOError:
return None