🍽 Fork YunoHost — snapshot mangé par la machine à tsoins
Upstream: https://github.com/YunoHost/yunohost @ 3a5f8bac8301c450897b96cbd43a4c7d3ba750fb But (José) : transformer tout le code en bions + ploxions du xerboxion. La carte de digestion vit au labo : /yunohost-digest.json
This commit is contained in:
388
tests/test_sso_and_portalapi.py
Normal file
388
tests/test_sso_and_portalapi.py
Normal file
@@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2024 YunoHost Contributors
|
||||
#
|
||||
# This file is part of YunoHost (see https://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 base64
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from yunohost.app import (
|
||||
app_change_url,
|
||||
app_install,
|
||||
app_remove,
|
||||
app_setting,
|
||||
app_ssowatconf,
|
||||
)
|
||||
from yunohost.authenticators.ldap_ynhuser import (
|
||||
SESSION_FOLDER,
|
||||
Authenticator,
|
||||
short_hash,
|
||||
)
|
||||
from yunohost.domain import _get_maindomain, domain_add, domain_list, domain_remove
|
||||
from yunohost.permission import user_permission_list, user_permission_update
|
||||
from yunohost.user import user_create, user_delete, user_list, user_update
|
||||
|
||||
from .conftest import get_test_apps_dir, message, raiseYunohostError
|
||||
|
||||
# Get main domain
|
||||
maindomain = open("/etc/yunohost/current_host").read().strip()
|
||||
subdomain = f"sub.{maindomain}"
|
||||
secondarydomain = "secondary.test"
|
||||
dummy_password = "test123Ynh"
|
||||
|
||||
|
||||
def setup_function(function):
|
||||
Authenticator.invalidate_all_sessions_for_user("alice")
|
||||
assert number_of_active_session_for_user("alice") == 0
|
||||
Authenticator.invalidate_all_sessions_for_user("bob")
|
||||
assert number_of_active_session_for_user("bob") == 0
|
||||
|
||||
user_permission_update(
|
||||
"hellopy.main", add=["visitors", "all_users"], remove=["alice", "bob"]
|
||||
)
|
||||
|
||||
app_setting("hellopy", "auth_header", delete=True)
|
||||
app_setting("hellopy", "protect_against_basic_auth_spoofing", delete=True)
|
||||
app_ssowatconf()
|
||||
|
||||
|
||||
def teardown_function(function):
|
||||
pass
|
||||
|
||||
|
||||
def setup_module(module):
|
||||
assert os.system("systemctl is-active yunohost-portal-api >/dev/null") == 0
|
||||
|
||||
if "alice" not in user_list()["users"]:
|
||||
user_create(
|
||||
"alice", maindomain, dummy_password, fullname="Alice White", admin=True
|
||||
)
|
||||
if "bob" not in user_list()["users"]:
|
||||
user_create("bob", maindomain, dummy_password, fullname="Bob Marley")
|
||||
|
||||
app_install(
|
||||
os.path.join(get_test_apps_dir(), "hellopy_ynh"),
|
||||
args=f"domain={maindomain}&init_main_permission=visitors",
|
||||
force=True,
|
||||
)
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
if "alice" in user_list()["users"]:
|
||||
user_delete("alice", force=True)
|
||||
if "bob" in user_list()["users"]:
|
||||
user_delete("bob", force=True)
|
||||
|
||||
app_remove("hellopy")
|
||||
|
||||
if subdomain in domain_list()["domains"]:
|
||||
domain_remove(subdomain)
|
||||
if secondarydomain in domain_list()["domains"]:
|
||||
domain_remove(secondarydomain)
|
||||
|
||||
|
||||
def login(session, logged_as, logged_on=None):
|
||||
if not logged_on:
|
||||
logged_on = maindomain
|
||||
|
||||
login_endpoint = f"https://{logged_on}/yunohost/portalapi/login"
|
||||
r = session.post(
|
||||
login_endpoint,
|
||||
data={"credentials": f"{logged_as}:{dummy_password}"},
|
||||
headers={
|
||||
"X-Requested-With": "",
|
||||
},
|
||||
verify=False,
|
||||
)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def logout(session):
|
||||
logout_endpoint = f"https://{maindomain}/yunohost/portalapi/logout"
|
||||
r = session.get(
|
||||
logout_endpoint,
|
||||
headers={
|
||||
"X-Requested-With": "",
|
||||
},
|
||||
verify=False,
|
||||
)
|
||||
return r
|
||||
|
||||
|
||||
def number_of_active_session_for_user(user):
|
||||
return len(list(SESSION_FOLDER.glob(f"{short_hash(user)}*")))
|
||||
|
||||
|
||||
def request(webpath, logged_as=None, session=None, inject_auth=None, logged_on=None):
|
||||
webpath = webpath.rstrip("/")
|
||||
|
||||
headers = {}
|
||||
if inject_auth:
|
||||
b64loginpassword = base64.b64encode(
|
||||
(inject_auth[0] + ":" + inject_auth[1]).encode()
|
||||
).decode()
|
||||
headers["Authorization"] = f"Basic {b64loginpassword}"
|
||||
|
||||
# Anonymous access
|
||||
if session:
|
||||
r = session.get(webpath, verify=False, allow_redirects=False, headers=headers)
|
||||
elif not logged_as:
|
||||
r = requests.get(webpath, verify=False, allow_redirects=False, headers=headers)
|
||||
# Login as a user using dummy password
|
||||
else:
|
||||
with requests.Session() as session:
|
||||
r = login(session, logged_as, logged_on)
|
||||
# We should have some cookies related to authentication now
|
||||
assert session.cookies
|
||||
r = session.get(
|
||||
webpath, verify=False, allow_redirects=False, headers=headers
|
||||
)
|
||||
|
||||
return r
|
||||
|
||||
|
||||
def test_api_public_as_anonymous():
|
||||
# FIXME : should list apps only if the domain option is enabled
|
||||
|
||||
r = request(f"https://{maindomain}/yunohost/portalapi/public")
|
||||
assert r.status_code == 200 and "apps" in r.json()
|
||||
|
||||
|
||||
def test_api_me_as_anonymous():
|
||||
r = request(f"https://{maindomain}/yunohost/portalapi/me")
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_api_login_and_logout():
|
||||
with requests.Session() as session:
|
||||
r = login(session, "alice")
|
||||
|
||||
assert "yunohost.portal" in session.cookies
|
||||
assert r.status_code == 200
|
||||
|
||||
assert number_of_active_session_for_user("alice") == 1
|
||||
|
||||
r = logout(session)
|
||||
|
||||
assert number_of_active_session_for_user("alice") == 0
|
||||
|
||||
|
||||
def test_api_login_nonexistinguser():
|
||||
with requests.Session() as session:
|
||||
r = login(session, "nonexistent")
|
||||
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_api_public_and_me_logged_in():
|
||||
r = request(f"https://{maindomain}/yunohost/portalapi/public", logged_as="alice")
|
||||
assert r.status_code == 200 and "apps" in r.json()
|
||||
r = request(f"https://{maindomain}/yunohost/portalapi/me", logged_as="alice")
|
||||
assert r.status_code == 200 and r.json()["username"] == "alice"
|
||||
|
||||
assert number_of_active_session_for_user("alice") == 2
|
||||
|
||||
|
||||
def test_api_session_expired():
|
||||
with requests.Session() as session:
|
||||
r = login(session, "alice")
|
||||
|
||||
assert "yunohost.portal" in session.cookies
|
||||
assert r.status_code == 200
|
||||
|
||||
r = request(f"https://{maindomain}/yunohost/portalapi/me", session=session)
|
||||
assert r.status_code == 200 and r.json()["username"] == "alice"
|
||||
|
||||
for file in SESSION_FOLDER.glob(f"{short_hash('alice')}*"):
|
||||
os.utime(str(file), (0, 0))
|
||||
|
||||
r = request(f"https://{maindomain}/yunohost/portalapi/me", session=session)
|
||||
assert number_of_active_session_for_user("alice") == 0
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_public_routes_not_blocked_by_ssowat():
|
||||
r = request(f"https://{maindomain}/yunohost/api/whatever")
|
||||
# Getting code 405, Method not allowed, which means the API does answer,
|
||||
# meaning it's not blocked by ssowat
|
||||
# Or : on the CI, the yunohost-api is likely to be down (to save resources)
|
||||
assert r.status_code in [405, 502]
|
||||
|
||||
os.system("mkdir -p /var/www/.well-known/acme-challenge-public")
|
||||
Path("/var/www/.well-known/acme-challenge-public/toto").touch()
|
||||
r = request(f"http://{maindomain}/.well-known/acme-challenge/toto")
|
||||
assert r.status_code == 200
|
||||
|
||||
r = request(f"http://{maindomain}/.well-known/acme-challenge/nonexistent")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_permission_propagation_on_ssowat():
|
||||
res = user_permission_list(full=True)["permissions"]
|
||||
assert "visitors" in res["hellopy.main"]["allowed"]
|
||||
assert "all_users" in res["hellopy.main"]["allowed"]
|
||||
|
||||
r = request(f"https://{maindomain}/")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
r = request(f"https://{maindomain}/", logged_as="alice")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
r = request(f"https://{maindomain}/", logged_as="bob")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
user_permission_update(
|
||||
"hellopy.main", remove=["visitors", "all_users"], add="alice"
|
||||
)
|
||||
|
||||
# Visitors now get redirected to portal
|
||||
r = request(f"https://{maindomain}/")
|
||||
assert r.status_code == 302
|
||||
assert r.headers["Location"].startswith(f"https://{maindomain}/yunohost/sso?r=")
|
||||
|
||||
# Alice can still access the app fine
|
||||
r = request(f"https://{maindomain}/", logged_as="alice")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
|
||||
def test_login_right_depending_on_app_access_and_mail():
|
||||
r = request(f"https://{maindomain}/", logged_as="bob")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
user_permission_update(
|
||||
"hellopy.main", remove=["visitors", "all_users"], add="alice"
|
||||
)
|
||||
|
||||
# Bob can still login even though he has no access to any apps, because its mail address is on the maindomain
|
||||
with requests.Session() as session:
|
||||
r = login(session, "bob")
|
||||
assert session.cookies
|
||||
|
||||
if secondarydomain not in domain_list()["domains"]:
|
||||
domain_add(secondarydomain)
|
||||
|
||||
user_update("bob", mail=f"bob@{secondarydomain}")
|
||||
|
||||
# Now bob shouldn't be able to login anymore (on the main domain)
|
||||
with requests.Session() as session:
|
||||
r = login(session, "bob")
|
||||
assert not session.cookies
|
||||
|
||||
user_permission_update("hellopy.main", add="bob")
|
||||
|
||||
# Bob should be allowed to login again (even though its mail is on secondarydomain)
|
||||
r = request(f"https://{maindomain}/", logged_as="bob")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
|
||||
def test_sso_basic_auth_header():
|
||||
r = request(f"https://{maindomain}/show-auth")
|
||||
assert (
|
||||
r.status_code == 200 and r.content.decode().strip() == "User: None\nPwd: None"
|
||||
)
|
||||
|
||||
r = request(f"https://{maindomain}/show-auth", logged_as="alice")
|
||||
assert (
|
||||
r.status_code == 200
|
||||
and r.content.decode().strip() == f"User: alice\nPwd: {dummy_password}"
|
||||
)
|
||||
|
||||
app_setting("hellopy", "auth_header", value="basic-without-password")
|
||||
app_ssowatconf()
|
||||
|
||||
r = request(f"https://{maindomain}/show-auth", logged_as="alice")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == f"User: alice\nPwd: -"
|
||||
|
||||
|
||||
def test_sso_basic_auth_header_spoofing():
|
||||
r = request(f"https://{maindomain}/show-auth")
|
||||
assert (
|
||||
r.status_code == 200 and r.content.decode().strip() == "User: None\nPwd: None"
|
||||
)
|
||||
|
||||
r = request(f"https://{maindomain}/show-auth", inject_auth=("foo", "bar"))
|
||||
assert (
|
||||
r.status_code == 200 and r.content.decode().strip() == "User: None\nPwd: None"
|
||||
)
|
||||
|
||||
app_setting("hellopy", "protect_against_basic_auth_spoofing", value="false")
|
||||
app_ssowatconf()
|
||||
|
||||
r = request(f"https://{maindomain}/show-auth", inject_auth=("foo", "bar"))
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "User: foo\nPwd: bar"
|
||||
|
||||
|
||||
def test_sso_on_subdomain():
|
||||
if subdomain not in domain_list()["domains"]:
|
||||
domain_add(subdomain)
|
||||
|
||||
app_change_url("hellopy", domain=subdomain, path="/")
|
||||
|
||||
r = request(f"https://{subdomain}/")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
r = request(f"https://{subdomain}/", logged_as="alice")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
r = request(f"https://{subdomain}/show-auth", logged_as="alice")
|
||||
assert r.status_code == 200 and r.content.decode().strip().startswith("User: alice")
|
||||
|
||||
|
||||
def test_sso_on_secondary_domain():
|
||||
if secondarydomain not in domain_list()["domains"]:
|
||||
domain_add(secondarydomain)
|
||||
|
||||
app_change_url("hellopy", domain=secondarydomain, path="/")
|
||||
|
||||
r = request(f"https://{secondarydomain}/")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
r = request(f"https://{secondarydomain}/", logged_as="alice")
|
||||
assert r.status_code == 200 and r.content.decode().strip() == "Hello world!"
|
||||
|
||||
r = request(f"https://{secondarydomain}/show-auth", logged_as="alice")
|
||||
# Getting 'User: None despite being logged on the main domain
|
||||
assert r.status_code == 200 and r.content.decode().strip().startswith("User: None")
|
||||
|
||||
r = request(
|
||||
f"https://{secondarydomain}/show-auth",
|
||||
logged_as="alice",
|
||||
logged_on=secondarydomain,
|
||||
)
|
||||
assert r.status_code == 200 and r.content.decode().strip().startswith("User: alice")
|
||||
|
||||
|
||||
# accès à l'api portal
|
||||
# -> test des routes
|
||||
# apps publique (seulement si activé ?)
|
||||
# /me
|
||||
# /update
|
||||
|
||||
|
||||
# accès aux trucs précédent meme avec une app installée sur la racine ?
|
||||
# ou une app par défaut ?
|
||||
|
||||
# accès à un deuxième "domain principal"
|
||||
|
||||
# accès à un app sur un sous-domaine
|
||||
# pas loggué -> redirect vers sso sur domaine principal
|
||||
# se logger sur API sur domain principal, puis utilisation du cookie sur le sous-domaine
|
||||
Reference in New Issue
Block a user