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: 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
@ -1325,6 +1349,74 @@ 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 #
############################# #############################

View file

@ -64,9 +64,13 @@ ynh_remove_logrotate () {
# Create a dedicated systemd config # Create a dedicated systemd config
# #
# This will use a template in ../conf/systemd.service # usage: ynh_add_systemd_config [Service name] [Template name]
# and will replace the following keywords with # | arg: Service name (optionnal, $app by default)
# global variables that should be defined before calling # | 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 : # this helper :
# #
# __APP__ by $app # __APP__ by $app
@ -74,9 +78,11 @@ ynh_remove_logrotate () {
# #
# usage: ynh_add_systemd_config # usage: ynh_add_systemd_config
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" 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. # 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 # 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" ynh_store_file_checksum "$finalsystemdconf"
sudo chown root: "$finalsystemdconf" sudo chown root: "$finalsystemdconf"
sudo systemctl enable $app sudo systemctl enable $service_name
sudo systemctl daemon-reload sudo systemctl daemon-reload
} }
# Remove the dedicated systemd config # Remove the dedicated systemd config
# #
# usage: ynh_remove_systemd_config [Service name]
# | arg: Service name (optionnal, $app by default)
#
# usage: ynh_remove_systemd_config # usage: ynh_remove_systemd_config
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 if [ -e "$finalsystemdconf" ]; then
sudo systemctl stop $app sudo systemctl stop $service_name
sudo systemctl disable $app sudo systemctl disable $service_name
ynh_secure_remove "$finalsystemdconf" ynh_secure_remove "$finalsystemdconf"
sudo systemctl daemon-reload
fi fi
} }
@ -157,7 +169,17 @@ ynh_remove_nginx_config () {
# #
# usage: ynh_add_fpm_config # usage: ynh_add_fpm_config
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" ynh_backup_if_checksum_is_different "$finalphpconf"
sudo cp ../conf/php-fpm.conf "$finalphpconf" sudo cp ../conf/php-fpm.conf "$finalphpconf"
ynh_replace_string "__NAMETOCHANGE__" "$app" "$finalphpconf" ynh_replace_string "__NAMETOCHANGE__" "$app" "$finalphpconf"
@ -168,21 +190,27 @@ ynh_add_fpm_config () {
if [ -e "../conf/php-fpm.ini" ] if [ -e "../conf/php-fpm.ini" ]
then 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" ynh_backup_if_checksum_is_different "$finalphpini"
sudo cp ../conf/php-fpm.ini "$finalphpini" sudo cp ../conf/php-fpm.ini "$finalphpini"
sudo chown root: "$finalphpini" sudo chown root: "$finalphpini"
ynh_store_file_checksum "$finalphpini" ynh_store_file_checksum "$finalphpini"
fi fi
sudo systemctl reload $fpm_service
sudo systemctl reload php5-fpm
} }
# Remove the dedicated php-fpm config # Remove the dedicated php-fpm config
# #
# usage: ynh_remove_fpm_config # usage: ynh_remove_fpm_config
ynh_remove_fpm_config () { ynh_remove_fpm_config () {
ynh_secure_remove "/etc/php5/fpm/pool.d/$app.conf" local fpm_config_dir=$(ynh_app_setting_get $app fpm_config_dir)
ynh_secure_remove "/etc/php5/fpm/conf.d/20-$app.ini" 2>&1 local fpm_service=$(ynh_app_setting_get $app fpm_service)
sudo systemctl reload php5-fpm # 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. 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 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,22 +37,23 @@ ynh_get_plain_key() {
ynh_restore_upgradebackup () { ynh_restore_upgradebackup () {
echo "Upgrade failed." >&2 echo "Upgrade failed." >&2
local app_bck=${app//_/-} # Replace all '_' by '-' local app_bck=${app//_/-} # Replace all '_' by '-'
NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0}
if [ "$NO_BACKUP_UPGRADE" -eq 0 ] NO_BACKUP_UPGRADE=${NO_BACKUP_UPGRADE:-0}
then
if [ "$NO_BACKUP_UPGRADE" -eq 0 ]
then
# Check if an existing backup can be found before removing and restoring the application. # Check if an existing backup can be found before removing and restoring the application.
if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$backup_number if sudo yunohost backup list | grep -q $app_bck-pre-upgrade$backup_number
then then
# Remove the application then restore it # Remove the application then restore it
sudo yunohost app remove $app sudo yunohost app remove $app
# Restore the backup # 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." ynh_die "The app was restored to the way it was before the failed upgrade."
fi fi
else else
echo "\$NO_BACKUP_UPGRADE is set, that means there's no backup to restore. You have to fix this upgrade by yourself !" >&2 echo "\$NO_BACKUP_UPGRADE is set, that means there's no backup to restore. You have to fix this upgrade by yourself !" >&2
fi fi
} }
# Make a backup in case of failed upgrade # Make a backup in case of failed upgrade
@ -86,7 +87,7 @@ ynh_backup_before_upgrade () {
fi fi
# Create backup # 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 ] if [ "$?" -eq 0 ]
then then
# If the backup succeeded, remove the previous backup # If the backup succeeded, remove the previous backup

View file

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

View file

@ -1 +1,2 @@
server_tokens off; 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 # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048
#ssl_dhparam /etc/ssl/private/dh2048.pem; #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 / { location / {
return 302 https://$http_host/yunohost/admin; 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/; alias /usr/share/yunohost/admin/;
default_type text/html; default_type text/html;
index index.html; index index.html;

View file

@ -42,7 +42,16 @@ server {
# > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048 # > openssl dhparam -out /etc/ssl/private/dh2048.pem -outform PEM -2 2048
#ssl_dhparam /etc/ssl/private/dh2048.pem; #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; 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 , ca-certificates, netcat-openbsd, iproute
, mariadb-server | mysql-server, php5-mysql | php5-mysqlnd , mariadb-server | mysql-server, php5-mysql | php5-mysqlnd
, slapd, ldap-utils, sudo-ldap, libnss-ldapd, nscd , 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-ldap, dovecot-lmtpd, dovecot-managesieved
, dovecot-antispam, fail2ban , dovecot-antispam, fail2ban
, nginx-extras (>=1.6.2), php5-fpm, php5-ldap, php5-intl , 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_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_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_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_extraction_failed": "Unable to extract installation files",
"app_id_invalid": "Invalid app id", "app_id_invalid": "Invalid app id",
"app_incompatible": "The app {app} is incompatible with your YunoHost version", "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 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) installed = _is_installed(app)
if not installed: 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"), shutil.copytree(os.path.join(APPS_SETTING_PATH, app, "scripts"),
os.path.join(APP_TMP_FOLDER, "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 # Execute App change_url script
os.system('chown -R admin: %s' % INSTALL_TMP) os.system('chown -R admin: %s' % INSTALL_TMP)
os.system('chmod +x %s' % os.path.join(os.path.join(APP_TMP_FOLDER, "scripts"))) 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", logger.success(m18n.n("app_change_url_success",
app=app, domain=domain, path=path)) 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): 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 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 # Retrieve interface
is_api = msettings.get('interface') == 'api' 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) upgraded_apps.append(app_instance_name)
logger.success(m18n.n('app_upgraded', app=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: if not upgraded_apps:
raise MoulinetteError(errno.ENODATA, m18n.n('app_no_upgrade')) 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 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 # Fetch or extract sources
try: 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')) logger.success(m18n.n('installation_complete'))
hook_callback('post_app_install', args=args_list, env=env_dict)
def app_remove(auth, app): def app_remove(auth, app):
""" """
@ -809,7 +823,7 @@ def app_remove(auth, app):
app -- App(s) to delete 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): if not _is_installed(app):
raise MoulinetteError(errno.EINVAL, 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: 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)) 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): if os.path.exists(app_setting_path):
shutil.rmtree(app_setting_path) shutil.rmtree(app_setting_path)
shutil.rmtree('/tmp/yunohost_remove') 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 app -- Write domain & path to app settings for further checks
""" """
logger.warning(m18n.n("app_checkurl_is_deprecated"))
from yunohost.domain import domain_list from yunohost.domain import domain_list
if "https://" == url[:8]: if "https://" == url[:8]:

View file

@ -1543,9 +1543,13 @@ class BackupMethod(object):
# Can create a hard link only if files are on the same fs # 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) # (i.e. we can't if it's on a different fs)
if os.stat(src).st_dev == os.stat(dest_dir).st_dev: if os.stat(src).st_dev == os.stat(dest_dir).st_dev:
os.link(src, dest) # Don't hardlink /etc/cron.d files to avoid cron bug
# Success, go to next file to organize # 'NUMBER OF HARD LINKS > 1' see #1043
continue 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
# If mountbind or hardlink couldnt be created, # If mountbind or hardlink couldnt be created,
# prepare a list of files that need to be copied # prepare a list of files that need to be copied

View file

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

View file

@ -30,8 +30,6 @@ import yaml
import errno import errno
import requests import requests
from urllib import urlopen
from moulinette import m18n, msettings from moulinette import m18n, msettings
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger from moulinette.utils.log import getActionLogger
@ -39,6 +37,7 @@ from moulinette.utils.log import getActionLogger
import yunohost.certificate import yunohost.certificate
from yunohost.service import service_regen_conf from yunohost.service import service_regen_conf
from yunohost.utils.network import get_public_ip
logger = getActionLogger('yunohost.domain') logger = getActionLogger('yunohost.domain')
@ -260,42 +259,6 @@ def domain_url_available(auth, domain, path):
return available 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(): def _get_maindomain():
with open('/etc/yunohost/current_host', 'r') as f: with open('/etc/yunohost/current_host', 'r') as f:
maindomain = f.readline().rstrip() maindomain = f.readline().rstrip()
@ -356,15 +319,8 @@ def _build_dns_conf(domain, ttl=3600):
} }
""" """
try: ipv4 = get_public_ip()
ipv4 = get_public_ip() ipv6 = get_public_ip(6)
except:
ipv4 = None
try:
ipv6 = get_public_ip(6)
except:
ipv6 = None
basic = [] 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.filesystem import read_file, write_to_file, rm
from moulinette.utils.network import download_json 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') 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() old_ipv6 = read_file(OLD_IPV6_FILE).rstrip()
# Get current IPv4 and IPv6 # Get current IPv4 and IPv6
(ipv4_, ipv6_) = get_public_ips() ipv4_ = get_public_ip()
ipv6_ = get_public_ip(6)
if ipv4 is None: if ipv4 is None:
ipv4 = ipv4_ ipv4 = ipv4_

View file

@ -41,7 +41,8 @@ from moulinette import m18n
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.utils.log import getActionLogger 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') logger = getActionLogger('yunohost.monitor')
@ -210,10 +211,7 @@ def monitor_network(units=None, human_readable=False):
else: else:
logger.debug('interface name %s was not found', iname) logger.debug('interface name %s was not found', iname)
elif u == 'infos': elif u == 'infos':
try: p_ipv4 = get_public_ip() or 'unknown'
p_ipv4 = get_public_ip()
except:
p_ipv4 = 'unknown'
l_ip = 'unknown' l_ip = 'unknown'
for name, addrs in devices.items(): 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 subprocess
import pwd import pwd
import socket import socket
from collections import OrderedDict from xmlrpclib import Fault
from importlib import import_module from importlib import import_module
from collections import OrderedDict
import apt import apt
import apt.progress import apt.progress
@ -45,12 +46,13 @@ from moulinette.utils.log import getActionLogger
from moulinette.utils.process import check_output from moulinette.utils.process import check_output
from moulinette.utils.filesystem import read_json, write_to_json 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.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.dyndns import _dyndns_available, _dyndns_provides
from yunohost.firewall import firewall_upnp from yunohost.firewall import firewall_upnp
from yunohost.service import service_status, service_regen_conf, service_log, service_start, service_enable 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.monitor import monitor_disk, monitor_system
from yunohost.utils.packages import ynh_packages_version from yunohost.utils.packages import ynh_packages_version
from yunohost.utils.network import get_public_ip
# FIXME this is a duplicate from apps.py # FIXME this is a duplicate from apps.py
APPS_SETTING_PATH = '/etc/yunohost/apps/' APPS_SETTING_PATH = '/etc/yunohost/apps/'
@ -568,7 +570,7 @@ def tools_diagnosis(auth, private=False):
diagnosis['system'] = OrderedDict() diagnosis['system'] = OrderedDict()
try: try:
disks = monitor_disk(units=['filesystem'], human_readable=True) 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) logger.warning(m18n.n('diagnosis_monitor_disk_error', error=format(e)), exc_info=1)
else: else:
diagnosis['system']['disks'] = {} diagnosis['system']['disks'] = {}
@ -621,16 +623,11 @@ def tools_diagnosis(auth, private=False):
# Private data # Private data
if private: if private:
diagnosis['private'] = OrderedDict() diagnosis['private'] = OrderedDict()
# Public IP # Public IP
diagnosis['private']['public_ip'] = {} diagnosis['private']['public_ip'] = {}
try: diagnosis['private']['public_ip']['IPv4'] = get_public_ip(4)
diagnosis['private']['public_ip']['IPv4'] = get_public_ip(4) diagnosis['private']['public_ip']['IPv6'] = get_public_ip(6)
except MoulinetteError as e:
pass
try:
diagnosis['private']['public_ip']['IPv6'] = get_public_ip(6)
except MoulinetteError as e:
pass
# Domains # Domains
diagnosis['private']['domains'] = domain_list(auth)['domains'] diagnosis['private']['domains'] = domain_list(auth)['domains']

View file

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

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