mirror of
https://github.com/YunoHost/yunohost.git
synced 2024-09-03 20:06:10 +02:00
Iterate on new portal API design: nginx config, cookie format, be able to open a non-root ldap session,
This commit is contained in:
parent
c01042b51d
commit
1efb50c7ab
7 changed files with 141 additions and 22 deletions
|
@ -2,6 +2,6 @@
|
|||
rewrite ^/yunohost/sso$ /yunohost/sso/ permanent;
|
||||
|
||||
location /yunohost/sso/ {
|
||||
# This is an empty location, only meant to avoid other locations
|
||||
# from matching /yunohost/sso, such that it's correctly handled by ssowat
|
||||
alias /usr/share/ssowat/portal/;
|
||||
index index.html;
|
||||
}
|
||||
|
|
|
@ -23,3 +23,23 @@ location = /yunohost/api/error/502 {
|
|||
add_header Content-Type text/plain;
|
||||
internal;
|
||||
}
|
||||
|
||||
location /yunohost/portalapi/ {
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_pass http://127.0.0.1:6788/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $http_host;
|
||||
|
||||
# Custom 502 error page
|
||||
error_page 502 /yunohost/portalapi/error/502;
|
||||
}
|
||||
|
||||
|
||||
# Yunohost admin output complete 502 error page, so use only plain text.
|
||||
location = /yunohost/portalapi/error/502 {
|
||||
return 502 '502 - Bad Gateway';
|
||||
add_header Content-Type text/plain;
|
||||
internal;
|
||||
}
|
||||
|
|
3
debian/control
vendored
3
debian/control
vendored
|
@ -14,7 +14,8 @@ Depends: ${python3:Depends}, ${misc:Depends}
|
|||
, python3-psutil, python3-requests, python3-dnspython, python3-openssl
|
||||
, python3-miniupnpc, python3-dbus, python3-jinja2
|
||||
, python3-toml, python3-packaging, python3-publicsuffix2
|
||||
, python3-ldap, python3-zeroconf, python3-lexicon,
|
||||
, python3-ldap, python3-zeroconf, python3-lexicon
|
||||
, python3-jwt
|
||||
, python-is-python3
|
||||
, nginx, nginx-extras (>=1.18)
|
||||
, apt, apt-transport-https, apt-utils, dirmngr
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
_global:
|
||||
namespace: yunohost
|
||||
cookie_name: yunohost.portal
|
||||
authentication:
|
||||
api: ldap_ynhuser
|
||||
cli: null
|
||||
lock: false
|
||||
cache: false
|
||||
|
||||
portal:
|
||||
category_help: Portal routes
|
||||
|
|
|
@ -1,12 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import jwt
|
||||
import logging
|
||||
import ldap
|
||||
import ldap.sasl
|
||||
import datetime
|
||||
|
||||
from moulinette import m18n
|
||||
from moulinette.authentication import BaseAuthenticator
|
||||
from yunohost.utils.error import YunohostError
|
||||
from moulinette.utils.text import random_ascii
|
||||
from yunohost.utils.error import YunohostError, YunohostAuthenticationError
|
||||
|
||||
session_secret = random_ascii()
|
||||
|
||||
logger = logging.getLogger("yunohostportal.authenticators.ldap_ynhuser")
|
||||
|
||||
|
@ -57,3 +62,79 @@ class Authenticator(BaseAuthenticator):
|
|||
# Free the connection, we don't really need it to keep it open as the point is only to check authentication...
|
||||
if con:
|
||||
con.unbind_s()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# FIXME FIXME FIXME : the password is to be encrypted to not expose it in the JWT cookie which is only signed and base64 encoded but not encrypted
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return {"user": username, "password": password}
|
||||
|
||||
def set_session_cookie(self, infos):
|
||||
|
||||
from bottle import response
|
||||
|
||||
assert isinstance(infos, dict)
|
||||
|
||||
# This allows to generate a new session id or keep the existing one
|
||||
current_infos = self.get_session_cookie(raise_if_no_session_exists=False)
|
||||
new_infos = {
|
||||
"id": current_infos["id"],
|
||||
# See https://pyjwt.readthedocs.io/en/latest/usage.html#registered-claim-names
|
||||
# for explanations regarding nbf, exp
|
||||
"nbf": int(datetime.datetime.now().timestamp()),
|
||||
"exp": int(datetime.datetime.now().timestamp()) + (7 * 24 * 3600) # One week validity
|
||||
}
|
||||
new_infos.update(infos)
|
||||
|
||||
response.set_cookie(
|
||||
"yunohost.portal",
|
||||
jwt.encode(new_infos, session_secret, algorithm="HS256").decode(),
|
||||
secure=True,
|
||||
httponly=True,
|
||||
path="/",
|
||||
# samesite="strict", # Bottle 0.12 doesn't support samesite, to be added in next versions
|
||||
# FIXME : add Expire clause
|
||||
)
|
||||
|
||||
def get_session_cookie(self, raise_if_no_session_exists=True):
|
||||
|
||||
from bottle import request
|
||||
|
||||
try:
|
||||
token = request.get_cookie("yunohost.portal", default="").encode()
|
||||
infos = jwt.decode(token, session_secret, algorithms="HS256", options={"require": ["id", "user", "exp", "nbf"]})
|
||||
except Exception:
|
||||
if not raise_if_no_session_exists:
|
||||
return {"id": random_ascii()}
|
||||
raise YunohostAuthenticationError("unable_authenticate")
|
||||
|
||||
if not infos and raise_if_no_session_exists:
|
||||
raise YunohostAuthenticationError("unable_authenticate")
|
||||
|
||||
if "id" not in infos:
|
||||
infos["id"] = random_ascii()
|
||||
|
||||
# FIXME: Here, maybe we want to re-authenticate the session via the authenticator
|
||||
# For example to check that the username authenticated is still in the admin group...
|
||||
|
||||
return infos
|
||||
|
||||
@staticmethod
|
||||
def delete_session_cookie(self):
|
||||
|
||||
from bottle import response
|
||||
|
||||
response.set_cookie("yunohost.portal", "", max_age=-1)
|
||||
response.delete_cookie("yunohost.portal")
|
||||
|
|
|
@ -22,12 +22,14 @@
|
|||
# from moulinette import Moulinette, m18n
|
||||
from moulinette.utils.log import getActionLogger
|
||||
|
||||
from yunohost.authenticators.ldap_ynhuser import Authenticator as Auth
|
||||
from yunohost.utils.ldap import LDAPInterface
|
||||
from yunohost.utils.error import YunohostValidationError
|
||||
|
||||
logger = getActionLogger("yunohostportal.user")
|
||||
|
||||
|
||||
def me():
|
||||
def portal_me():
|
||||
"""
|
||||
Get user informations
|
||||
|
||||
|
@ -36,11 +38,13 @@ def me():
|
|||
|
||||
"""
|
||||
|
||||
username = None # FIXME : this info should come from the authentication layer
|
||||
import pdb; pdb.set_trace()
|
||||
|
||||
from yunohost.utils.ldap import _get_ldap_interface
|
||||
auth = Auth().get_session_cookie()
|
||||
username = auth["user"]
|
||||
password = auth["password"]
|
||||
|
||||
ldap = _get_ldap_interface()
|
||||
ldap = LDAPInterface(username, password)
|
||||
|
||||
user_attrs = ["cn", "mail", "uid", "maildrop", "givenName", "sn", "mailuserquota"]
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ def _get_ldap_interface():
|
|||
global _ldap_interface
|
||||
|
||||
if _ldap_interface is None:
|
||||
_ldap_interface = LDAPInterface()
|
||||
_ldap_interface = LDAPInterface(user="root")
|
||||
|
||||
return _ldap_interface
|
||||
|
||||
|
@ -71,22 +71,34 @@ def _destroy_ldap_interface():
|
|||
|
||||
atexit.register(_destroy_ldap_interface)
|
||||
|
||||
URI = "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi"
|
||||
BASEDN = "dc=yunohost,dc=org"
|
||||
ROOTDN = "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth"
|
||||
USERDN = "uid={username},ou=users,dc=yunohost,dc=org"
|
||||
|
||||
|
||||
class LDAPInterface:
|
||||
def __init__(self):
|
||||
logger.debug("initializing ldap interface")
|
||||
|
||||
self.uri = "ldapi://%2Fvar%2Frun%2Fslapd%2Fldapi"
|
||||
self.basedn = "dc=yunohost,dc=org"
|
||||
self.rootdn = "gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth"
|
||||
def __init__(self, user="root", password=None):
|
||||
|
||||
if user == "root":
|
||||
logger.debug("initializing root ldap interface")
|
||||
self.userdn = ROOTDN
|
||||
self._connect = lambda con: con.sasl_non_interactive_bind_s("EXTERNAL")
|
||||
else:
|
||||
logger.debug("initializing user ldap interface")
|
||||
self.userdn = USERDN.format(username=user)
|
||||
self._connect = lambda con: con.simple_bind_s(self.userdn, password)
|
||||
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
|
||||
def _reconnect():
|
||||
con = ldap.ldapobject.ReconnectLDAPObject(
|
||||
self.uri, retry_max=10, retry_delay=0.5
|
||||
URI, retry_max=10, retry_delay=0.5
|
||||
)
|
||||
con.sasl_non_interactive_bind_s("EXTERNAL")
|
||||
self._connect(con)
|
||||
return con
|
||||
|
||||
try:
|
||||
|
@ -113,7 +125,7 @@ class LDAPInterface:
|
|||
logger.warning("Error during ldap authentication process: %s", e)
|
||||
raise
|
||||
else:
|
||||
if who != self.rootdn:
|
||||
if who != self.userdn:
|
||||
raise MoulinetteError("Not logged in with the expected userdn ?!")
|
||||
else:
|
||||
self.con = con
|
||||
|
@ -139,7 +151,7 @@ class LDAPInterface:
|
|||
|
||||
"""
|
||||
if not base:
|
||||
base = self.basedn
|
||||
base = BASEDN
|
||||
|
||||
try:
|
||||
result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
|
||||
|
@ -184,7 +196,7 @@ class LDAPInterface:
|
|||
Boolean | MoulinetteError
|
||||
|
||||
"""
|
||||
dn = rdn + "," + self.basedn
|
||||
dn = f"{rdn},{BASEDN}"
|
||||
ldif = modlist.addModlist(attr_dict)
|
||||
for i, (k, v) in enumerate(ldif):
|
||||
if isinstance(v, list):
|
||||
|
@ -215,7 +227,7 @@ class LDAPInterface:
|
|||
Boolean | MoulinetteError
|
||||
|
||||
"""
|
||||
dn = rdn + "," + self.basedn
|
||||
dn = f"{rdn},{BASEDN}"
|
||||
try:
|
||||
self.con.delete_s(dn)
|
||||
except Exception as e:
|
||||
|
@ -240,7 +252,7 @@ class LDAPInterface:
|
|||
Boolean | MoulinetteError
|
||||
|
||||
"""
|
||||
dn = rdn + "," + self.basedn
|
||||
dn = f"{rdn},{BASEDN}"
|
||||
actual_entry = self.search(base=dn, attrs=None)
|
||||
ldif = modlist.modifyModlist(actual_entry[0], attr_dict, ignore_oldexistent=1)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue