Add authentication support

This commit is contained in:
Benjamin Renard 2023-02-25 05:02:27 +01:00
parent 16659cdaf6
commit 610cdb0f7c
28 changed files with 1515 additions and 119 deletions

View file

@ -27,7 +27,8 @@
"ext-pdo": "^7.3", "ext-pdo": "^7.3",
"ext-json": "*", "ext-json": "*",
"ext-yaml": "^2.0", "ext-yaml": "^2.0",
"league/mime-type-detection": "^1.11" "league/mime-type-detection": "^1.11",
"apereo/phpcas": "^1.6"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^1.9" "phpstan/phpstan": "^1.9"

View file

@ -134,6 +134,140 @@ db:
#datetime_format: '%Y-%m-%d %H:%M:%S' # Exemple : 2018-10-12 18:06:59 #datetime_format: '%Y-%m-%d %H:%M:%S' # Exemple : 2018-10-12 18:06:59
#
# Authentication
#
auth:
# Enabled authentication
enabled: false
# Methods to authenticate users
methods:
- form
- http
#- cas
# User backends
backends:
- ldap
#
# Login form
#
login_form:
# Display link for other authentication methods
# Note: method as key and label as value
display_other_methods:
http: "HTTP"
cas: "SSO"
#
# HTTP Authentication Configuration
#
http:
# HTTP Auth methods :
# * AUTHORIZATION : use HTTP_AUTHORIZATION environnement. This mode could be use with PHP FPM.
# Specific configuration is need in Apache to use this mode :
# RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# * REMOTE_USER : use REMOTE_USER environnement variable to retreive authenticated user's login
# This method could be only used with $http_auth_trust_without_password_challenge
# enabled.
# * PHP_AUTH : use PHP_AUTH_USER and PHP_AUTH_PW environnement variables (only available
# using mod_php with Apache. (default)
method: 'PHP_AUTH'
# Trust HTTP server authentication
# If enabled, the application will no check user credentials and only retreive user's login
# from HTTP server.
#trust_without_password_challenge: true
# Realm (use when force HTTP login, optional)
#realm: "Authentication required"
#
# CAS Configuration
#
cas:
# CAS host (just the domain name)
host: 'idp.example.com'
# CAS context (the root path, default: '/idp/cas')
# Example: for 'http://idp.example.com/idp/cas', put '/idp/cas'
context: '/idp/cas'
# CAS HTTP port (default: 443)
#port: 443
# CAS procotol version
# Posssible values: "1.0", "2.0" (default), "3.0" or "S1" (SAML1)
#version: '2.0'
# CAS server SSL certificate validation (set to false to disable)
ca_cert_certificate_path: "/etc/ssl/certs/ca-certificates.crt"
# CAS Debug log file
#debug_log_file: "${root_directory_path}/data/logs/cas.log"
# CAS Logout
#logout: true # Enable CAS logout on app logout
#logout_url: "https://my.example.fr/logout/" # Specify custom CAS logout URL
# CAS Fake authenticated user
#fake_authenticated_user: 'myusername'
#
# LDAP user backend
#
ldap:
# LDAP host (required, multiple hosts could be specified with comma separator)
host: 'ldap://localhost'
# LDAP port (optional)
#port: 389
# Enable STARTTLS (optional, default: false)
#starttls: false
# LDAP directory base DN (required)
basedn: 'o=example'
# LDAP bind DN (optional)
#bind_dn: 'uid=eesyphp,ou=sysaccounts,${auth.ldap.basedn}'
# LDAP bind password (optional)
#bind_password: 'secret'
# User search filter by username. The keyword "[username]" will be replace before search by
# the looked username (default: "uid=[username]")
#user_filter_by_uid: 'uid=[username]'
# User base DN
user_basedn: 'ou=people,${auth.ldap.basedn}'
# Bind with username instead of user DN (optional, default: false)
#bind_with_username: true
# LDAP user attributes to retreive with their properties:
# [LDAP attr name]:
# name: [map name] # optional, default: LDAP attr name
# type: [type of value] # optional, default: 'string', possible values: string, bool, int, float
# multivalued: true # optional, default: false
# default: null # optional, default: null
user_attributes:
uid:
name: 'login'
multivalued: false
default: null
cn:
name: 'name'
multivalued: false
default: null
mail:
type: 'string'
# PEAR Net_LDAP2 library path (optional, default: Net/LDAP2.php)
#netldap2_path: 'Net/LDAP2.php'
# #
# Email configuration # Email configuration
# #

View file

@ -1,7 +1,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2023-02-14 01:17+0100\n" "POT-Creation-Date: 2023-02-25 04:54+0100\n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
"Last-Translator: Benjamin Renard <brenard@zionetrix.net>\n" "Last-Translator: Benjamin Renard <brenard@zionetrix.net>\n"
"Language-Team: \n" "Language-Team: \n"
@ -70,6 +70,22 @@ msgstr "Une exception est survenue en exécutant la commande %s"
msgid "Unable to connect to the database." msgid "Unable to connect to the database."
msgstr "Impossible de se connecter à la base de données." msgstr "Impossible de se connecter à la base de données."
#: Auth/Form.php:32
msgid "Invalid username or password."
msgstr "Nom d'utilisateur ou mot de passe invalide."
#: Auth/Http.php:34 Url.php:176
msgid "Authentication required"
msgstr "Authentification requise"
#: Auth/Http.php:115 Auth/Http.php:118 Url.php:180
msgid "Access denied"
msgstr "Accès interdit"
#: Auth/Http.php:119 Auth/Http.php:123
msgid "You must login to access this page."
msgstr "Vous devez vous connecter pour accéder à cette page."
#: Email.php:141 #: Email.php:141
#, php-format #, php-format
msgid "</hr><p><small>Mail initialy intended for %s.</small></p>" msgid "</hr><p><small>Mail initialy intended for %s.</small></p>"
@ -102,35 +118,27 @@ msgstr ""
"\n" "\n"
"%s: %s" "%s: %s"
#: Tpl.php:355 #: Tpl.php:394
msgid "No template specified." msgid "No template specified."
msgstr "Aucun template spécifié." msgstr "Aucun template spécifié."
#: Tpl.php:386 #: Tpl.php:425
msgid "An error occurred while displaying this page." msgid "An error occurred while displaying this page."
msgstr "Une erreur est survenue en affichant cette page." msgstr "Une erreur est survenue en affichant cette page."
#: Url.php:141 #: Url.php:172
msgid "Bad request" msgid "Bad request"
msgstr "Mauvaise requête" msgstr "Mauvaise requête"
#: Url.php:142 #: Url.php:173
msgid "Invalid request." msgid "Invalid request."
msgstr "Requête invalide." msgstr "Requête invalide."
#: Url.php:145 #: Url.php:177
msgid "Authentication required"
msgstr "Authentification requise"
#: Url.php:146
msgid "You have to be authenticated to access to this page." msgid "You have to be authenticated to access to this page."
msgstr "Vous devez être authentifié pour accéder à cette page." msgstr "Vous devez être authentifié pour accéder à cette page."
#: Url.php:149 #: Url.php:181
msgid "Access denied"
msgstr "Accès interdit"
#: Url.php:150
msgid "" msgid ""
"You do not have access to this application. If you think this is an error, " "You do not have access to this application. If you think this is an error, "
"please contact support." "please contact support."
@ -138,25 +146,25 @@ msgstr ""
"Vous n'avez pas accès à cette application. Si vous pensez qu'il s'agit d'une " "Vous n'avez pas accès à cette application. Si vous pensez qu'il s'agit d'une "
"erreur, merci de prendre contact avec le support." "erreur, merci de prendre contact avec le support."
#: Url.php:153 #: Url.php:184
msgid "Whoops ! Page not found" msgid "Whoops ! Page not found"
msgstr "Oups ! Page introuvable" msgstr "Oups ! Page introuvable"
#: Url.php:154 #: Url.php:185
msgid "The requested page can not be found." msgid "The requested page can not be found."
msgstr "La page demandée est introuvable." msgstr "La page demandée est introuvable."
#: Url.php:162 #: Url.php:193
msgid "Error" msgid "Error"
msgstr "Erreur" msgstr "Erreur"
#: Url.php:163 #: Url.php:194
msgid "An unknown error occurred. If problem persist, please contact support." msgid "An unknown error occurred. If problem persist, please contact support."
msgstr "" msgstr ""
"Une erreur inconnue est survenue. Si le problème persiste, merci de prendre " "Une erreur inconnue est survenue. Si le problème persiste, merci de prendre "
"contact avec le support." "contact avec le support."
#: Url.php:226 #: Url.php:257
msgid "" msgid ""
"Unable to determine the requested page. If the problem persists, please " "Unable to determine the requested page. If the problem persists, please "
"contact support." "contact support."
@ -164,7 +172,7 @@ msgstr ""
"Impossible de déterminer la page demandée. Si le problème persiste, merci de " "Impossible de déterminer la page demandée. Si le problème persiste, merci de "
"prendre contact avec le support." "prendre contact avec le support."
#: Url.php:376 #: Url.php:411
msgid "" msgid ""
"Unable to determine the requested page (loop detected). If the problem " "Unable to determine the requested page (loop detected). If the problem "
"persists, please contact support." "persists, please contact support."
@ -172,22 +180,19 @@ msgstr ""
"Impossible de déterminer la page demandée (boucle détectée). Si le problème " "Impossible de déterminer la page demandée (boucle détectée). Si le problème "
"persiste, merci de prendre contact avec le support." "persiste, merci de prendre contact avec le support."
#: Url.php:407 #: Url.php:441
msgid "This request cannot be processed." msgid "This request cannot be processed."
msgstr "Cette requête ne peut être traitée." msgstr "Cette requête ne peut être traitée."
#: Url.php:420 #: Url.php:451
msgid "" msgid "Authentication required but fail to authenticate you."
"Authentication required but force_authentication function is not defined." msgstr "Authentification requise mais impossible pour vous authentifier."
msgstr ""
"Authentification requise mais la fonction force_authentication n'est pas "
"définie."
#: Url.php:429 #: Url.php:460
msgid "This request could not be processed correctly." msgid "This request could not be processed correctly."
msgstr "Cette requête n'a put être traitée correctement." msgstr "Cette requête n'a put être traitée correctement."
#: I18n.php:122 App.php:120 #: I18n.php:122 App.php:124
msgid "Hello world !" msgid "Hello world !"
msgstr "Bonjour tout le monde !" msgstr "Bonjour tout le monde !"
@ -379,10 +384,18 @@ msgstr "Impossible d'écrire le fichier du catalogue JS %s (%s)."
msgid "%s JS catalog writed (%s)." msgid "%s JS catalog writed (%s)."
msgstr "Catalogue JS %s créé (%s)." msgstr "Catalogue JS %s créé (%s)."
#: App.php:122 #: App.php:126
msgid "Hello world!" msgid "Hello world!"
msgstr "Salut tout le monde !" msgstr "Salut tout le monde !"
#: App.php:137
msgid "Disconnected"
msgstr "Déconnecté"
#: App.php:139
msgid "You are now disconnected."
msgstr "Vous êtes maintenant déconnecté."
#: static/js/myconfirm.js:4 static/js/myconfirm.js:171 #: static/js/myconfirm.js:4 static/js/myconfirm.js:171
#: static/js/myconfirm.js:200 #: static/js/myconfirm.js:200
msgid "Confirmation" msgid "Confirmation"
@ -460,3 +473,29 @@ msgstr ""
"framework EesyPHP. Configurez votre propre dossier de templates et créer le " "framework EesyPHP. Configurez votre propre dossier de templates et créer le "
"fichier <em>homepage.tpl</em> pour l'écraser. Vous pouvez également écraser " "fichier <em>homepage.tpl</em> pour l'écraser. Vous pouvez également écraser "
"le gestionnaire de l'URL racine de l'application web." "le gestionnaire de l'URL racine de l'application web."
#: templates/empty.tpl:60
msgid "Logout"
msgstr "Déconnexion"
#: templates/login.tpl:2
msgid "Connection"
msgstr "Connexion"
#: templates/login.tpl:7
msgid "Username"
msgstr "Nom d'utilisateur"
#: templates/login.tpl:11
msgid "Password"
msgstr "Mot de passe"
#: templates/login.tpl:14
msgid "Submit"
msgstr "Envoyer"
#~ msgid ""
#~ "Authentication required but force_authentication function is not defined."
#~ msgstr ""
#~ "Authentification requise mais la fonction force_authentication n'est pas "
#~ "définie."

View file

@ -1,7 +1,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"POT-Creation-Date: 2023-02-14 01:17+0100\n" "POT-Creation-Date: 2023-02-25 04:54+0100\n"
"PO-Revision-Date: 2023-02-14 01:17+0100\n" "PO-Revision-Date: 2023-02-25 04:54+0100\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"

View file

@ -1,7 +1,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"POT-Creation-Date: 2023-02-14 01:17+0100\n" "POT-Creation-Date: 2023-02-25 04:54+0100\n"
"PO-Revision-Date: 2023-02-14 01:17+0100\n" "PO-Revision-Date: 2023-02-25 04:54+0100\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
@ -61,6 +61,22 @@ msgstr ""
msgid "Unable to connect to the database." msgid "Unable to connect to the database."
msgstr "" msgstr ""
#: Auth/Form.php:32
msgid "Invalid username or password."
msgstr ""
#: Auth/Http.php:34 Url.php:176
msgid "Authentication required"
msgstr ""
#: Auth/Http.php:115 Auth/Http.php:118 Url.php:180
msgid "Access denied"
msgstr ""
#: Auth/Http.php:119 Auth/Http.php:123
msgid "You must login to access this page."
msgstr ""
#: Email.php:141 #: Email.php:141
#, php-format #, php-format
msgid "</hr><p><small>Mail initialy intended for %s.</small></p>" msgid "</hr><p><small>Mail initialy intended for %s.</small></p>"
@ -87,82 +103,73 @@ msgid ""
"%s: %s" "%s: %s"
msgstr "" msgstr ""
#: Tpl.php:355 #: Tpl.php:394
msgid "No template specified." msgid "No template specified."
msgstr "" msgstr ""
#: Tpl.php:386 #: Tpl.php:425
msgid "An error occurred while displaying this page." msgid "An error occurred while displaying this page."
msgstr "" msgstr ""
#: Url.php:141 #: Url.php:172
msgid "Bad request" msgid "Bad request"
msgstr "" msgstr ""
#: Url.php:142 #: Url.php:173
msgid "Invalid request." msgid "Invalid request."
msgstr "" msgstr ""
#: Url.php:145 #: Url.php:177
msgid "Authentication required"
msgstr ""
#: Url.php:146
msgid "You have to be authenticated to access to this page." msgid "You have to be authenticated to access to this page."
msgstr "" msgstr ""
#: Url.php:149 #: Url.php:181
msgid "Access denied"
msgstr ""
#: Url.php:150
msgid "" msgid ""
"You do not have access to this application. If you think this is an error, " "You do not have access to this application. If you think this is an error, "
"please contact support." "please contact support."
msgstr "" msgstr ""
#: Url.php:153 #: Url.php:184
msgid "Whoops ! Page not found" msgid "Whoops ! Page not found"
msgstr "" msgstr ""
#: Url.php:154 #: Url.php:185
msgid "The requested page can not be found." msgid "The requested page can not be found."
msgstr "" msgstr ""
#: Url.php:162 #: Url.php:193
msgid "Error" msgid "Error"
msgstr "" msgstr ""
#: Url.php:163 #: Url.php:194
msgid "An unknown error occurred. If problem persist, please contact support." msgid "An unknown error occurred. If problem persist, please contact support."
msgstr "" msgstr ""
#: Url.php:226 #: Url.php:257
msgid "" msgid ""
"Unable to determine the requested page. If the problem persists, please " "Unable to determine the requested page. If the problem persists, please "
"contact support." "contact support."
msgstr "" msgstr ""
#: Url.php:376 #: Url.php:411
msgid "" msgid ""
"Unable to determine the requested page (loop detected). If the problem " "Unable to determine the requested page (loop detected). If the problem "
"persists, please contact support." "persists, please contact support."
msgstr "" msgstr ""
#: Url.php:407 #: Url.php:441
msgid "This request cannot be processed." msgid "This request cannot be processed."
msgstr "" msgstr ""
#: Url.php:420 #: Url.php:451
msgid "" msgid "Authentication required but fail to authenticate you."
"Authentication required but force_authentication function is not defined."
msgstr "" msgstr ""
#: Url.php:429 #: Url.php:460
msgid "This request could not be processed correctly." msgid "This request could not be processed correctly."
msgstr "" msgstr ""
#: I18n.php:122 App.php:120 #: I18n.php:122 App.php:124
msgid "Hello world !" msgid "Hello world !"
msgstr "" msgstr ""
@ -321,10 +328,18 @@ msgstr ""
msgid "%s JS catalog writed (%s)." msgid "%s JS catalog writed (%s)."
msgstr "" msgstr ""
#: App.php:122 #: App.php:126
msgid "Hello world!" msgid "Hello world!"
msgstr "" msgstr ""
#: App.php:137
msgid "Disconnected"
msgstr ""
#: App.php:139
msgid "You are now disconnected."
msgstr ""
#: static/js/myconfirm.js:4 static/js/myconfirm.js:171 #: static/js/myconfirm.js:4 static/js/myconfirm.js:171
#: static/js/myconfirm.js:200 #: static/js/myconfirm.js:200
msgid "Confirmation" msgid "Confirmation"
@ -395,3 +410,23 @@ msgid ""
"it. You could also overwrite the URL handler for the root of the web " "it. You could also overwrite the URL handler for the root of the web "
"application." "application."
msgstr "" msgstr ""
#: templates/empty.tpl:60
msgid "Logout"
msgstr ""
#: templates/login.tpl:2
msgid "Connection"
msgstr ""
#: templates/login.tpl:7
msgid "Username"
msgstr ""
#: templates/login.tpl:11
msgid "Password"
msgstr ""
#: templates/login.tpl:14
msgid "Submit"
msgstr ""

View file

@ -53,6 +53,22 @@ msgstr ""
msgid "Unable to connect to the database." msgid "Unable to connect to the database."
msgstr "" msgstr ""
#: Auth/Form.php:32
msgid "Invalid username or password."
msgstr ""
#: Auth/Http.php:34 Url.php:176
msgid "Authentication required"
msgstr ""
#: Auth/Http.php:115 Auth/Http.php:118 Url.php:180
msgid "Access denied"
msgstr ""
#: Auth/Http.php:119 Auth/Http.php:123
msgid "You must login to access this page."
msgstr ""
#: Email.php:141 #: Email.php:141
#, php-format #, php-format
msgid "</hr><p><small>Mail initialy intended for %s.</small></p>" msgid "</hr><p><small>Mail initialy intended for %s.</small></p>"
@ -79,82 +95,73 @@ msgid ""
"%s: %s" "%s: %s"
msgstr "" msgstr ""
#: Tpl.php:355 #: Tpl.php:394
msgid "No template specified." msgid "No template specified."
msgstr "" msgstr ""
#: Tpl.php:386 #: Tpl.php:425
msgid "An error occurred while displaying this page." msgid "An error occurred while displaying this page."
msgstr "" msgstr ""
#: Url.php:141 #: Url.php:172
msgid "Bad request" msgid "Bad request"
msgstr "" msgstr ""
#: Url.php:142 #: Url.php:173
msgid "Invalid request." msgid "Invalid request."
msgstr "" msgstr ""
#: Url.php:145 #: Url.php:177
msgid "Authentication required"
msgstr ""
#: Url.php:146
msgid "You have to be authenticated to access to this page." msgid "You have to be authenticated to access to this page."
msgstr "" msgstr ""
#: Url.php:149 #: Url.php:181
msgid "Access denied"
msgstr ""
#: Url.php:150
msgid "" msgid ""
"You do not have access to this application. If you think this is an error, " "You do not have access to this application. If you think this is an error, "
"please contact support." "please contact support."
msgstr "" msgstr ""
#: Url.php:153 #: Url.php:184
msgid "Whoops ! Page not found" msgid "Whoops ! Page not found"
msgstr "" msgstr ""
#: Url.php:154 #: Url.php:185
msgid "The requested page can not be found." msgid "The requested page can not be found."
msgstr "" msgstr ""
#: Url.php:162 #: Url.php:193
msgid "Error" msgid "Error"
msgstr "" msgstr ""
#: Url.php:163 #: Url.php:194
msgid "An unknown error occurred. If problem persist, please contact support." msgid "An unknown error occurred. If problem persist, please contact support."
msgstr "" msgstr ""
#: Url.php:226 #: Url.php:257
msgid "" msgid ""
"Unable to determine the requested page. If the problem persists, please " "Unable to determine the requested page. If the problem persists, please "
"contact support." "contact support."
msgstr "" msgstr ""
#: Url.php:376 #: Url.php:411
msgid "" msgid ""
"Unable to determine the requested page (loop detected). If the problem " "Unable to determine the requested page (loop detected). If the problem "
"persists, please contact support." "persists, please contact support."
msgstr "" msgstr ""
#: Url.php:407 #: Url.php:441
msgid "This request cannot be processed." msgid "This request cannot be processed."
msgstr "" msgstr ""
#: Url.php:420 #: Url.php:451
msgid "" msgid "Authentication required but fail to authenticate you."
"Authentication required but force_authentication function is not defined."
msgstr "" msgstr ""
#: Url.php:429 #: Url.php:460
msgid "This request could not be processed correctly." msgid "This request could not be processed correctly."
msgstr "" msgstr ""
#: I18n.php:122 App.php:120 #: I18n.php:122 App.php:124
msgid "Hello world !" msgid "Hello world !"
msgstr "" msgstr ""
@ -313,6 +320,14 @@ msgstr ""
msgid "%s JS catalog writed (%s)." msgid "%s JS catalog writed (%s)."
msgstr "" msgstr ""
#: App.php:122 #: App.php:126
msgid "Hello world!" msgid "Hello world!"
msgstr "" msgstr ""
#: App.php:137
msgid "Disconnected"
msgstr ""
#: App.php:139
msgid "You are now disconnected."
msgstr ""

View file

@ -19,6 +19,14 @@ msgstr ""
msgid "Back" msgid "Back"
msgstr "" msgstr ""
#: templates/logout.tpl:5
msgid "Disconnected"
msgstr ""
#: templates/logout.tpl:6
msgid "You are now disconnected."
msgstr ""
#: templates/homepage.tpl:5 #: templates/homepage.tpl:5
msgid "Hello, world!" msgid "Hello, world!"
msgstr "" msgstr ""
@ -30,3 +38,31 @@ msgid ""
"it. You could also overwrite the URL handler for the root of the web " "it. You could also overwrite the URL handler for the root of the web "
"application." "application."
msgstr "" msgstr ""
#: templates/empty.tpl:60
msgid "Logout"
msgstr ""
#: templates/login.tpl:2
msgid "Connection"
msgstr ""
#: templates/login.tpl:7
msgid "Username"
msgstr ""
#: templates/login.tpl:11
msgid "Password"
msgstr ""
#: templates/login.tpl:14
msgid "Submit"
msgstr ""
#: templates/must_login.tpl:5
msgid "Access denied"
msgstr ""
#: templates/must_login.tpl:6
msgid "You must login to access this page."
msgstr ""

View file

@ -8,8 +8,22 @@ parameters:
- example/includes/config.local.php - example/includes/config.local.php
- example/data/tmp/templates_c - example/data/tmp/templates_c
universalObjectCratesClasses: universalObjectCratesClasses:
- EesyPHP\HookEvent
- EesyPHP\UrlRequest - EesyPHP\UrlRequest
- EesyPHP\Auth\User
ignoreErrors: ignoreErrors:
-
message: "#Property EesyPHP\\\\Auth\\\\Ldap::\\$connection has unknown class Net_LDAP2 as its type\\.#"
path: src/Auth/Ldap.php
-
message: "#Call to method search\\(\\) on an unknown class Net_LDAP2\\.#"
path: src/Auth/Ldap.php
-
message: "#Call to static method connect\\(\\) on an unknown class Net_LDAP2\\.#"
path: src/Auth/Ldap.php
-
message: "#Call to static method escape\\(\\) on an unknown class Net_LDAP2_Filter\\.#"
path: src/Auth/Ldap.php
- -
message: "#Instantiated class Mail_mime not found\\.#" message: "#Instantiated class Mail_mime not found\\.#"
path: src/Email.php path: src/Email.php

View file

@ -64,6 +64,10 @@ class App {
Url::init(); Url::init();
Url :: add_url_handler('#^$#', array('EesyPHP\\App', 'handle_homepage')); Url :: add_url_handler('#^$#', array('EesyPHP\\App', 'handle_homepage'));
} }
if (Auth :: enabled()) {
Auth :: init();
Url :: add_url_handler('#^logout$#', array('EesyPHP\\App', 'handle_logout'), null, false);
}
if (self :: get('mail.enabled', true, 'bool')) if (self :: get('mail.enabled', true, 'bool'))
Email :: init(); Email :: init();
if (self :: get('i18n.enabled', true, 'bool')) if (self :: get('i18n.enabled', true, 'bool'))
@ -122,4 +126,17 @@ class App {
echo "<h1>".I18n::_("Hello world!")."</h1>"; echo "<h1>".I18n::_("Hello world!")."</h1>";
} }
/**
* Default logout handler
* @param UrlRequest $request
* @return void
*/
public static function handle_logout($request) {
Auth::logout();
if (Tpl::initialized())
Tpl :: display("logout.tpl", I18n::_("Disconnected"));
else
echo "<h1>".I18n::_("You are now disconnected.")."</h1>";
}
} }

306
src/Auth.php Normal file
View file

@ -0,0 +1,306 @@
<?php
namespace EesyPHP;
class Auth {
/**
* Initialized methods
* @var array<string,string>
*/
private static $methods = array();
/**
* Method name used to authenticate current user
* @var string|null
*/
private static $logged_method = null;
/**
* Initialized backends
* @var array<string,string>
*/
private static $backends = array();
/**
* Current authenticated user
* @var \EesyPHP\Auth\User|null
*/
private static $user = null;
/**
* Initialize
* @return void
*/
public static function init() {
if (!self :: enabled()) return;
self :: $methods = array();
foreach(App::get('auth.methods', array(), 'array') as $method) {
if (!$method || !is_string($method)) {
Log::warning(
'Auth Init: Invalid auth method retreive from configuration, ignore it: %s',
vardump($method));
continue;
}
$class = (
$method[0] == '\\'?
$method:
"\\EesyPHP\\Auth\\".ucfirst($method)
);
if (!class_exists($class)) {
Log::warning(
"Auth Init: Unknown auth method '%s' retreived from configuration, ignore it",
$method
);
continue;
}
$parents = class_parents($class);
if (!is_array($parents) || !in_array('EesyPHP\\Auth\\Method', $parents)) {
Log::warning(
'Auth Init: Auth method %s class (%s) do not derivate from \\EesyPHP\\Auth\\Method '.
'class, ignore it.',
$method, $class);
continue;
}
if (!call_user_func(array($class, 'init'))) {
Log::warning(
'Auth Init: fail to initialize auth method %s, ignore it',
$method, $class);
continue;
}
Log::trace('Auth method %s initialized (class %s)', $method, $class);
self :: $methods[strtolower($method)] = $class;
}
self :: $backends = array();
foreach(App::get('auth.backends', array(), 'array') as $backend) {
if (!$backend || !is_string($backend)) {
Log::warning(
'Auth Init: Invalid auth backend retreive from configuration, ignore it: %s',
vardump($backend));
continue;
}
$class = (
$backend[0] == '\\'?
$backend:
"\\EesyPHP\\Auth\\".ucfirst($backend)
);
if (!class_exists($class)) {
Log::warning(
"Auth Init: Unknown auth backend '%s' retreived from configuration, ignore it",
$backend
);
continue;
}
$parents = class_parents($class);
if (!is_array($parents) || !in_array('EesyPHP\\Auth\\Backend', $parents)) {
Log::warning(
'Auth Init: Auth backend %s class (%s) do not derivate from \\EesyPHP\\Auth\\Backend '.
'class, ignore it.',
$backend, $class);
continue;
}
if (!call_user_func(array($class, 'init'))) {
Log::warning(
'Auth Init: fail to initialize auth backend %s, ignore it',
$backend, $class);
continue;
}
Log::trace('Auth backend %s initialized (class %s)', $backend, $class);
self :: $backends[strtolower($backend)] = $class;
}
}
/**
* Check if authentication is enabled
* @return bool
*/
public static function enabled() {
if (!is_null(App::get('auth.enabled', null, 'bool')))
return App::get('auth.enabled', false, 'bool');
if (App::get('auth.methods', array(), 'array') && App::get('auth.backends', array(), 'array'))
return true;
Log :: trace('Authentication is disabled');
return false;
}
/**
* Check if a authentification method is enabled
* @param string $method
* @return bool
*/
public static function method_is_enabled($method) {
return array_key_exists(strtolower($method), self :: $methods);
}
/**
* Get user by username
* @param string $username
* @param boolean $first Return only the first matched user (if authorized, optional, default: true)
* @return \EesyPHP\Auth\User|array<\EesyPHP\Auth\User>|null|false The user object if found, null it not, false in case of error
*/
public static function get_user($username, $first=true) {
if (!self :: $backends) {
Log :: warning("No auth backend registered, can't retreive user");
return false;
}
$users = array();
foreach (self :: $backends as $backend) {
$user = call_user_func(array($backend, 'get_user'), $username);
if ($user) $users[] = $user;
}
if (!$users) return null;
if (count($users) > 1 && !App::get('auth.allow_multiple_match', false, 'bool')) {
Log :: error('Multiple user found for username "%s":\n%s', $username, vardump($users));
return false;
}
return $first?$users[0]:$users;
}
/**
* Search user by username and check its password
* @param string $username The username
* @param string $password The password to check
* @return \EesyPHP\Auth\User|null|false
*/
public static function authenticate($username, $password) {
$users = self :: get_user($username, false);
if (!$users) return $users === false?false:null;
$auth_users = array();
foreach ($users as $user) {
if ($user->check_password($password))
$auth_users[] = $user;
}
if (!$auth_users) return null;
if (
count($auth_users) > 1
&& !App::get(
'auth.allow_multiple_match_with_valid_password',
App::get('auth.allow_multiple_match', false, 'bool'),
'bool'
)
) {
Log :: error(
'Multiple user match for username "%s" and provided password:\n%s',
$username, vardump($auth_users));
return false;
}
return $auth_users[0];
}
/**
* Log user
* @param bool|string $force Force user authentication: could specified desired method or true
* to use the first one (optional, default: false)
* @param null|string $with_method Specify with which method(s) login user (optional, default: all methods)
* @return \EesyPHP\Auth\User|null|false
*/
public static function login($force=false, $with_method=null) {
// Check if already logged in
if (self :: $user)
return self :: $user;
// Check if logged in session
if (isset($_SESSION['user']) && isset($_SESSION['auth_method'])) {
$user = unserialize($_SESSION['user']);
if (is_a($user, '\\EesyPHP\\Auth\\User')) {
self :: $user = $user;
self :: $logged_method = (
array_key_exists($_SESSION['auth_method'], self :: $methods)?
$_SESSION['auth_method']:null
);
Log :: debug(
'User %s authenticated from session (method %s and backend %s)',
$user->username,
self :: $logged_method?self :: $logged_method:'unknown',
$user->backend);
return $user;
}
Log::warning('Invalid user data in session, drop it');
// Otherwise, drop user in session
unset($_SESSION['user']);
unset($_SESSION['auth_method']);
}
if (!self :: $methods) {
Log :: warning("No auth method registered, can't authenticate users");
return false;
}
// Otherwise, log without enforcing by using registered methods
foreach (self :: $methods as $method => $class) {
if ($with_method && !in_array($method, array_map('strtolower', ensure_is_array($with_method))))
continue;
$user = call_user_func(array($class, 'login'));
if ($user) {
self :: set_user($user, $method);
return $user;
}
}
// If still not logged and force mode enable, force login using specified method (or the first one)
if ($force) {
$method = (
is_string($force) && array_key_exists($force, self :: $methods)?
$force:key(self :: $methods)
);
Log::debug('Force authentication using method %s', $method);
$user = call_user_func(array(self :: $methods[$method], 'login'), true);
if ($user) {
self :: set_user($user, $method);
return $user;
}
return false;
}
return null;
}
/**
* Helper to set current authenticated user
* @param \EesyPHP\Auth\User $user The current authenticated user object
* @param string $method Method used to authenticate the user
* @return void
*/
private static function set_user($user, $method) {
Log :: debug(
'User %s authenticated using method %s and backend %s',
$user->username, $method, $user->backend);
self :: $user = $user;
self :: $logged_method = $method;
$_SESSION['user'] = serialize($user);
$_SESSION['auth_method'] = $method;
Hook :: trigger('logged_in', array('method' => $method, 'user' => $user));
}
/**
* Logout
* @return void
*/
public static function logout() {
$method = (
self :: $logged_method?
self :: $logged_method:
(isset($_SESSION['auth_method'])?$_SESSION['auth_method']:null)
);
self :: $user = null;
self :: $logged_method = null;
if (isset($_SESSION['user']))
unset($_SESSION['user']);
if (isset($_SESSION['auth_method']))
unset($_SESSION['auth_method']);
if ($method)
call_user_func(array(self :: $methods[$method], 'logout'));
}
/**
* Get current authenticated user
* @return \EesyPHP\Auth\User|null
*/
public static function user() {
return self :: $user;
}
}

33
src/Auth/Backend.php Normal file
View file

@ -0,0 +1,33 @@
<?php
namespace EesyPHP\Auth;
class Backend {
/**
* Initialize
* @return boolean
*/
public static function init() {
return true;
}
/**
* Retreive a user by its username
* @param string $username
* @return \EesyPHP\Auth\User|null|false The user object if found, null it not, false in case of error
*/
public static function get_user($username) {
return null;
}
/**
* Check a user password
* @param \EesyPHP\Auth\User $user The user object
* @param string $password The password to check
* @return boolean
*/
public static function check_password($user, $password) {
return false;
}
}

132
src/Auth/Cas.php Normal file
View file

@ -0,0 +1,132 @@
<?php
namespace EesyPHP\Auth;
use EesyPHP\App;
use EesyPHP\Auth;
use EesyPHP\Log;
use EesyPHP\Url;
use phpCAS;
class Cas extends Method {
/**
* Fake authenticated user login
* @var string|null
*/
private static $fake_authenticated_user = null;
/**
* Initialize
* @return boolean
*/
public static function init() {
self :: $fake_authenticated_user = App :: get(
'auth.cas.fake_authenticated_user', null, 'string');
if (self :: $fake_authenticated_user) return true;
if (App::get('auth.cas.debug_log_file'))
phpCAS::setDebug(App::get('auth.cas.debug_log_file'));
if (!App::get('auth.cas.host')) {
Log :: error('CAS host not configured. Check your configuration!');
return false;
}
$cas_version = App :: get('auth.cas.version', '2.0', 'string');
$supported_cas_versions = phpCAS::getSupportedProtocols();
if (!array_key_exists($cas_version, $supported_cas_versions)) {
Log :: error(
'Unsupported CAS version (%s). Check your configuration!',
$cas_version
);
return false;
}
// Init phpCAS client
phpCAS::client(
$cas_version,
App :: get('auth.cas.host'),
App :: get('auth.cas.port', 443, 'int'),
App :: get('auth.cas.context', '/idp/cas', 'string'),
Url :: get_absolute_url("/")
);
if (App :: get('auth.cas.ca_cert_certificate_path'))
phpCAS::setCasServerCACert(App :: get('auth.cas.ca_cert_certificate_path'));
else
phpCAS::setNoCasServerValidation();
Url :: add_url_handler(
'#^login/cas_callback$#', array('EesyPHP\\Auth\\Cas', 'handle_cas_callback'), null, false);
return true;
}
/**
* Compute CAS callback URL
* @return string
*/
private static function get_cas_callback_url() {
return Url :: get_absolute_url(
'login/cas_callback?next='.(
isset($_REQUEST['next'])?
$_REQUEST['next']:
urlencode(Url :: get_current_url())
)
);
}
/**
* Log user
* @param bool $force Force user authentication
* @return \EesyPHP\Auth\User|null
*/
public static function login($force=false) {
if (!phpCAS :: isAuthenticated() && $force) {
$_SESSION['cas_callback_url'] = self :: get_cas_callback_url();
phpCAS :: setFixedServiceURL($_SESSION['cas_callback_url']);
phpCAS :: forceAuthentication();
}
$user = (
phpCAS :: isAuthenticated()?
Auth :: get_user(phpCAS :: getUser()):
null
);
if ($force && !$user)
Log :: fatal('Fail to authenticate you');
return $user;
}
/**
* Logout
* @return void
*/
public static function logout() {
if (App :: get('auth.cas.logout', true, 'bool') && !self :: $fake_authenticated_user) {
if (App :: get('auth.cas.logout_url')) {
Url :: redirect(App :: get('auth.cas.logout_url'));
exit();
}
phpCAS::logout();
}
else {
session_unset();
session_destroy();
}
}
/**
* The CAS callback view
* @param \EesyPHP\UrlRequest $request
* @return void
*/
public static function handle_cas_callback($request) {
if (isset($_SESSION['cas_callback_url'])) {
phpCAS :: setFixedServiceURL($_SESSION['cas_callback_url']);
unset($_SESSION['cas_callback_url']);
}
$user = Auth :: login(false, 'Cas');
if ($user)
Url :: redirect(isset($_REQUEST['next'])?urldecode($_REQUEST['next']):null);
Log :: fatal('No CAS ticket or fail to authenticate you');
}
}

76
src/Auth/Form.php Normal file
View file

@ -0,0 +1,76 @@
<?php
namespace EesyPHP\Auth;
use EesyPHP\App;
use EesyPHP\Auth;
use EesyPHP\Hook;
use EesyPHP\Url;
use EesyPHP\Tpl;
class Form extends Method {
/**
* Initialize
* @return boolean
*/
public static function init() {
Url :: add_url_handler('#^login$#', array('EesyPHP\\Auth\\Form', 'handle_login'), null, false);
Hook :: register('logged_in', array('\\EesyPHP\\Auth\\Form', 'logged_in_hook'));
return true;
}
/**
* Log user
* @param bool $force Force user authentication
* @return \EesyPHP\Auth\User|null
*/
public static function login($force=false) {
$user = null;
if (isset($_REQUEST['username']) && isset($_REQUEST['password'])) {
$user = Auth :: authenticate($_REQUEST['username'], $_REQUEST['password']);
if (!$user) Tpl::add_error(_('Invalid username or password.'));
}
if ($force && !$user) {
if (Url :: get_current_url() != 'login')
Url :: redirect('login?next='.urlencode(Url :: get_current_url()));
return null;
}
return $user;
}
/**
* The login form view
* @param \EesyPHP\UrlRequest $request
* @return void
*/
public static function handle_login($request) {
$user = Auth :: login(false, 'Form');
$display_other_methods = array();
foreach (App::get('auth.login_form.display_other_methods', array(), 'array') as $method => $label)
if (Auth::method_is_enabled($method))
$display_other_methods[$method] = $label;
if (
!$user && isset($_REQUEST['method']) &&
array_key_exists($_REQUEST['method'], $display_other_methods)
) {
$user = Auth :: login($_REQUEST['method'], $_REQUEST['method']);
}
if ($user)
Url :: redirect(isset($_REQUEST['next'])?urldecode($_REQUEST['next']):null);
else
Tpl :: assign('next', (isset($_REQUEST['next'])?urldecode($_REQUEST['next']):''));
Tpl :: assign('display_other_methods', $display_other_methods);
Tpl :: display('login.tpl', 'Connection');
}
/**
* Logged in hook
* @param \EesyPHP\HookEvent $event
* @return void
*/
public static function logged_in_hook($event) {
if ($event->method == 'Form' && isset($_REQUEST['next']))
Url :: redirect(urldecode($_REQUEST['next']));
}
}

137
src/Auth/Http.php Normal file
View file

@ -0,0 +1,137 @@
<?php
namespace EesyPHP\Auth;
use EesyPHP\App;
use EesyPHP\Auth;
use EesyPHP\I18n;
use EesyPHP\Log;
use EesyPHP\Tpl;
use EesyPHP\Url;
use function EesyPHP\vardump;
class Http extends Method {
/**
* Method to retreive HTTP credentials
* @var string
*/
private static $method;
/**
* HTTP realm string (use to compute WWW-Authenticate HTTP header)
* @var string
*/
private static $realm;
/**
* Initialize
* @return boolean
*/
public static function init() {
self :: $method = App::get('auth.http.method', 'PHP_AUTH', 'string');
self :: $realm = App::get(
'auth.http.realm', _('Authentication required'), 'string');
return true;
}
/**
* Retreive HTTP authentication data
* @return array|false array('username' => '[login]', 'password' => '[password]') or false
*/
private static function get_auth_data() {
switch(self :: $method) {
case 'AUTHORIZATION':
Log :: debug("Auth HTTP: use AUTHORIZATION method");
if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION'])) {
$auth_data = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
if (is_array($auth_data) && count($auth_data) == 2) {
return array(
'username' => $auth_data[0],
'password' => $auth_data[1],
);
}
else
Log :: error("Fail to parse HTTP_AUTHORIZATION environnement variable.");
}
break;
case 'REMOTE_USER':
Log :: debug("Auth HTTP : use REMOTE_USER method");
if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER'])) {
return array(
'username' => $_SERVER['REMOTE_USER'],
'password' => false,
);
}
break;
case 'PHP_AUTH':
default:
Log :: debug("Auth HTTP : use PHP_AUTH method");
if (isset($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_USER'])) {
return array(
'username' => $_SERVER['PHP_AUTH_USER'],
'password' => $_SERVER['PHP_AUTH_PW'],
);
}
break;
}
Log :: trace("HTTP::get_auth_data(): no auth data found\n".vardump($_SERVER));
return false;
}
/**
* Log user
* @param bool $force Force user authentication
* @return \EesyPHP\Auth\User|null
*/
public static function login($force=false) {
$auth_data = self :: get_auth_data();
if (!$auth_data) {
if ($force) self :: force_login();
return null;
}
if (App :: get('auth.http.trust_without_password_challenge', false, 'bool'))
$user = Auth :: get_user($auth_data['username']);
else
$user = Auth :: authenticate($auth_data['username'], $auth_data['password']);
if (!$user && $force)
self :: force_login();
return $user;
}
/**
* Force HTTP user authentification
* @return void
*/
public static function force_login() {
header('HTTP/1.1 401 Authorization Required');
header(
sprintf('WWW-Authenticate: Basic realm="%s"', addslashes(self :: $realm))
);
if (Tpl::initialized()) {
Tpl :: display("must_login.tpl", I18n::_("Access denied"));
}
else {
printf("<h1>%s</h1>", I18n::_("Access denied"));
printf("<p>%s</p>", I18n::_("You must login to access this page."));
printf(
"<p><a href='%s'>%s</a></p>",
Url :: public_root_url(),
I18n::_("You must login to access this page.")
);
}
exit();
}
/**
* Logout
* @return void
*/
public static function logout() {
self :: force_login();
}
}

260
src/Auth/Ldap.php Normal file
View file

@ -0,0 +1,260 @@
<?php
namespace EesyPHP\Auth;
use EesyPHP\App;
use EesyPHP\Auth\User;
use EesyPHP\Config;
use EesyPHP\Log;
use function EesyPHP\ensure_is_array;
use function EesyPHP\cast;
use function EesyPHP\vardump;
use PEAR;
use Net_LDAP2;
use Net_LDAP2_Filter;
class Ldap extends Backend {
/**
* LDAP configuration as expected by Net_LDAP2
* @var array
*/
private static $ldap_config;
/**
* Net_LDAP2 connection (if connected)
* @var Net_LDAP2|null
*/
private static $connection = null;
/**
* Default LDAP user attributes configuration
* @var array<string,array>
*/
private static $default_user_attributes = array(
'uid' => array(
'name' => 'login',
'type' => 'string',
'multivalued' => false,
'default' => null,
),
'mail' => array(
'type' => 'string',
'multivalued' => false,
'default' => null,
),
'cn' => array(
'name' => 'name',
'type' => 'string',
'multivalued' => false,
'default' => null,
),
);
/**
* Initialize
* @return bool
*/
public static function init() {
if (!class_exists('Net_LDAP2')) {
$path = App::get('auth.ldap.netldap2_path', 'Net/LDAP2.php', 'string');
if (!@include($path)) {
Log::error('Fail to load Net_LDAP2 (%s)', $path);
return false;
}
}
foreach(array('host', 'basedn') as $param) {
if (!App::get("auth.ldap.$param")) {
Log :: error('LDAP %s not configured. Check your configuration!', $param);
return false;
}
}
self :: $ldap_config = array (
'host' => implode(' ', App :: get('auth.ldap.host', array(), 'array')),
'basedn' => App :: get('auth.ldap.basedn', null, 'string'),
'binddn' => App :: get('auth.ldap.bind_dn', null, 'string'),
'bindpw' => App :: get('auth.ldap.bind_password', null, 'string'),
'starttls' => App :: get('starttls', false, 'bool'),
);
if ($port = App :: get('auth.ldap.port', null, 'int'))
self :: $ldap_config['port'] = $port;
return true;
}
/**
* Connect on the LDAP directory
* @return bool
*/
private static function connect() {
if (is_a(self :: $connection, 'Net_LDAP2')) return true;
Log :: debug(
'Connect on LDAP host "%s" as %s (base DN="%s")',
self :: $ldap_config['host'],
isset(self :: $ldap_config['binddn'])?self :: $ldap_config['binddn']:"anonymous",
self :: $ldap_config['basedn']
);
// @phpstan-ignore-next-line
self :: $connection = Net_LDAP2::connect(self :: $ldap_config);
// @phpstan-ignore-next-line
if (PEAR::isError(self :: $connection)) {
Log :: error(
'Could not connect to LDAP server (%s): %s',
self :: $ldap_config['host'], self :: $connection->getMessage());
self :: $connection = null;
return false;
}
return true;
}
/**
* Make a search in the LDAP directory
* @param string $filter The LDAP filter string
* @param array|null $attrs Expected attributes (optional, default: all existing attributes)
* @param string|null $basedn The base DN of the search (optional, default: configured root base
* DN of the LDAP connection)
* @param string|array<string>|null $sorted If defined, sort return objects by specified attribute(s)
* @param array|null $options Search options as expected by NetLDAP::search() (optional, default: null)
*/
public static function search($filter, $attrs=null, $basedn=null, $sorted=null, $options=null) {
if (!self :: connect()) return false;
$options = is_array($options)?$options:array();
if (!is_null($attrs))
$options['attributes'] = $attrs;
Log :: debug(
'Run search in LDAP directory with filter "%s" on base DN "%s"',
$filter, $basedn?$basedn:"unset");
$search = self :: $connection -> search($basedn, $filter, $options);
// @phpstan-ignore-next-line
if (PEAR::isError($search)) {
Log :: error(
'Error occured searching in LDAP with filter "%s" on base DN "%s": %s',
$filter, $basedn?$basedn:"unset", $search->getMessage()
);
return false;
}
$entries = (
$sorted?
$search -> sorted(ensure_is_array($sorted)):
$search -> entries()
);
$result = array();
foreach ($entries as $entry)
$result[$entry->dn()] = $entry -> getValues();
return $result;
}
/**
* Cast an LDAP value
* @param mixed $value The raw LDAP value
* @param string $type The expected type: see cast() for supported types, but boolean value will
* be casted as LDAP boolean string.
* @return mixed The casted value
*/
public static function cast($value, $type) {
switch($type) {
case 'bool':
case 'boolean':
return $value == 'TRUE';
case 'array_of_bool':
case 'array_of_boolean':
$values = array();
foreach(ensure_is_array($value) as $value)
$values[] = $value == 'TRUE';
return $values;
default:
return cast($value, $type);
}
}
/**
* Retreive LDAP attribute value(s) from LDAP entry
* @param array<string,mixed> $entry The LDAP entry
* @param string $attr The LDAP attribute name
* @param bool $all_values Return all values or just the first one (optional, default: false)
* @param mixed $default The default value to return if the LDAP attribute is undefined
* (optional, default: an empty array if $all_values, null otherwise)
* @param string|null $cast The expected type of value (optional, default: string)
*/
public static function get_attr($entry, $attr, $all_values=False, $default=null, $cast=null) {
$values = self :: cast(
isset($entry[$attr])?ensure_is_array($entry[$attr]):array(),
"array_of_".($cast?$cast:'string')
);
if ($values)
return $all_values?$values:$values[0];
if ($all_values)
return !is_null($default)?$default:array();
return $default;
}
/**
* Retreive a user by its username
* @param string $username
* @return \EesyPHP\Auth\User|null|false The user object if found, null it not, false in case of error
*/
public static function get_user($username) {
$attrs = App::get('auth.ldap.user_attributes', self :: $default_user_attributes, 'array');
$users = self :: search(
str_replace(
'[username]', Net_LDAP2_Filter::escape($username),
App::get('auth.ldap.user_filter_by_uid', 'uid=[username]', 'string')
),
array_keys($attrs),
App::get('auth.ldap.user_basedn', null, 'string')
);
if (!is_array($users)) {
Log::warning('An error occured looking for user "%s" in LDAP directory', $username);
return false;
}
if (!$users) {
Log::debug('User "%s" not found in LDAP directory', $username);
return null;
}
if (count($users) > 1) {
Log::warning(
'More than on users found with username "%s": %s',
$username, implode(' / ', array_keys($users))
);
}
$dn = key($users);
$info = array('dn' => $dn);
foreach($attrs as $attr => $attr_config) {
$info[Config::get("name", $attr, 'string', false, $attr_config)] = self :: get_attr(
$users[$dn],
$attr,
Config::get("multivalued", false, 'bool', false, $attr_config),
Config::get("default", null, null, false, $attr_config)
);
}
Log::debug('User "%s" found in LDAP directory (%s):\n%s', $username, $dn, vardump($info));
return new User($username, '\\EesyPHP\\Auth\\LDAP', $info);
}
/**
* Check a user password
* @param \EesyPHP\Auth\User $user The user object
* @param string $password The password to check
* @return boolean
*/
public static function check_password($user, $password) {
$config = self :: $ldap_config;
$config['binddn'] = (
App::get('auth.ldap.bind_with_username', false, 'bool')?
$user->username:
$user->dn
);
$config['bindpw'] = $password;
$result = Net_LDAP2::connect($config);
// @phpstan-ignore-next-line
return !PEAR::isError($result);
}
}

34
src/Auth/Method.php Normal file
View file

@ -0,0 +1,34 @@
<?php
namespace EesyPHP\Auth;
use EesyPHP\Log;
class Method {
/**
* Initialize
* @return boolean
*/
public static function init() {
return true;
}
/**
* Log user
* @param bool $force Force user authentication
* @return \EesyPHP\Auth\User|null
*/
public static function login($force=false) {
Log :: fatal('login() is not implement for this authentication method.');
return null;
}
/**
* Logout
* @return void
*/
public static function logout() {
return;
}
}

86
src/Auth/User.php Normal file
View file

@ -0,0 +1,86 @@
<?php
namespace EesyPHP\Auth;
use EesyPHP\Log;
class User {
/**
* Username
* @var string
*/
private $username;
/**
* User backend class name
* @var string
*/
private $backend;
/**
* User info
* @var array<string,mixed>
*/
private $info;
/**
* Constructor
* @param string $username The username
* @param string $backend The backend class name
* @param array<string,mixed>|null $info User info (optional)
*/
public function __construct($username, $backend, $info=null) {
$this -> username = $username;
$this -> backend = $backend;
$this -> info = is_array($info)?$info:array();
}
/**
* Magic method to get a dynamic property
* @param string $key The property
* @return mixed
*/
public function __get($key) {
switch ($key) {
case 'username':
return $this -> username;
case 'backend':
return $this -> backend;
default:
if (array_key_exists($key, $this -> info))
return $this -> info[$key];
}
Log::warning(
'Ask for unknown user property %s:\n%s', $key, Log::get_debug_backtrace_context());
return null;
}
/**
* Magic method to check if a dynamic property is set
* @param string $key The property
* @return bool
*/
public function __isset($key) {
switch ($key) {
case 'username':
case 'backend':
return true;
default:
return array_key_exists($key, $this -> info);
}
}
/**
* Check user password
* @param string $password
* @return bool
*/
public function check_password($password) {
return call_user_func(
array($this -> backend, 'check_password'),
$this, $password
);
}
}

View file

@ -132,7 +132,7 @@ class Log {
* @return true * @return true
*/ */
public static function log($level, $message, ...$extra_args) { public static function log($level, $message, ...$extra_args) {
global $auth_user, $argv; global $argv;
if (!array_key_exists($level, self :: $levels)) $level = self :: $default_level; if (!array_key_exists($level, self :: $levels)) $level = self :: $default_level;
if (self :: $levels[$level] < self :: $levels[self :: $level]) return true; if (self :: $levels[$level] < self :: $levels[self :: $level]) return true;
@ -162,8 +162,8 @@ class Log {
$_SERVER['REQUEST_URI'], $_SERVER['REQUEST_URI'],
$_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_ADDR'],
); );
if (isset($auth_user)) if (Auth::enabled())
$msg[] = ($auth_user['username']?$auth_user['username']:'anonymous'); $msg[] = (Auth::user()?Auth::user()->username:'anonymous');
$msg[] = $level; $msg[] = $level;
$msg[] = $message; $msg[] = $message;
$msg = implode(' - ', $msg)."\n"; $msg = implode(' - ', $msg)."\n";

View file

@ -47,11 +47,8 @@ class SentryIntegration {
]); ]);
\Sentry\configureScope(function (\Sentry\State\Scope $scope): void { \Sentry\configureScope(function (\Sentry\State\Scope $scope): void {
global $auth_user;
$scope->setUser([ $scope->setUser([
'id' => isset($auth_user) && $auth_user?$auth_user['uid']:null, 'username' => Auth::user()?Auth::user()->username:null,
'email' => isset($auth_user) && $auth_user?$auth_user['mail']:null,
'segment' => isset($auth_user) && $auth_user?$auth_user['type']:null,
'ip_address' => php_sapi_name()=='cli'?null:$_SERVER['REMOTE_ADDR'], 'ip_address' => php_sapi_name()=='cli'?null:$_SERVER['REMOTE_ADDR'],
]); ]);
}); });

View file

@ -356,7 +356,6 @@ class Tpl {
* @return void * @return void
*/ */
protected static function define_common_variables($pagetitle=null) { protected static function define_common_variables($pagetitle=null) {
global $auth_user;
self :: assign('public_root_url', Url :: public_root_url()); self :: assign('public_root_url', Url :: public_root_url());
self :: assign('pagetitle', $pagetitle); self :: assign('pagetitle', $pagetitle);
self :: assign('main_pagetitle', App::get('main_pagetitle', null, 'string')); self :: assign('main_pagetitle', App::get('main_pagetitle', null, 'string'));
@ -379,8 +378,8 @@ class Tpl {
self :: assign('TEXT_DOMAIN', I18n :: TEXT_DOMAIN); self :: assign('TEXT_DOMAIN', I18n :: TEXT_DOMAIN);
// Authenticated user info // Authenticated user info
if (isset($auth_user)) if (Auth::user())
self :: assign('auth_user', $auth_user); self :: assign('auth_user', Auth::user());
} }
/** /**

View file

@ -77,8 +77,8 @@ class Url {
* @param array|null $additional_info Array of information to pass to the URL handler * @param array|null $additional_info Array of information to pass to the URL handler
* @param boolean $authenticated Permit to define if this URL is accessible only for * @param boolean $authenticated Permit to define if this URL is accessible only for
* authenticated users (optional, default: true if the * authenticated users (optional, default: true if the
* special force_authentication function is defined, * EesyPHP Authentication feature is enabled, false
* false otherwise) * otherwise)
* @param boolean $overwrite Allow overwrite if a command already exists with the * @param boolean $overwrite Allow overwrite if a command already exists with the
* same name (optional, default: false) * same name (optional, default: false)
* @param boolean $api_mode Enable API mode (optional, default: false) * @param boolean $api_mode Enable API mode (optional, default: false)
@ -88,12 +88,6 @@ class Url {
public static function add_url_handler($pattern, $handler=null, $additional_info=null, public static function add_url_handler($pattern, $handler=null, $additional_info=null,
$authenticated=null, $overwrite=true, $api_mode=false, $authenticated=null, $overwrite=true, $api_mode=false,
$http_methods=null) { $http_methods=null) {
$authenticated = (
is_null($authenticated)?
function_exists('force_authentication'):
(bool)$authenticated
);
// Check HTTP methods parameter // Check HTTP methods parameter
if (is_null($http_methods)) if (is_null($http_methods))
$http_methods = array('GET', 'POST'); $http_methods = array('GET', 'POST');
@ -422,8 +416,7 @@ class Url {
* Handle the current requested URL * Handle the current requested URL
* *
* Note: if the route required that user is authenticated, this method will * Note: if the route required that user is authenticated, this method will
* invoke the force_authentication() special function (or trigger a fatal error * invoke Auth::login() in force mode (or trigger a fatal error if fail).
* if it's not defined).
* *
* @param string|null $default_url The default URL if current one does not * @param string|null $default_url The default URL if current one does not
* match with any configured pattern. * match with any configured pattern.
@ -448,11 +441,8 @@ class Url {
Tpl :: assign('request', $request); Tpl :: assign('request', $request);
// Check authentication (if need) // Check authentication (if need)
if($request -> authenticated) if($request -> authenticated && !Auth::login(true))
if (function_exists('force_authentication')) Log :: fatal(I18n::_("Authentication required but fail to authenticate you."));
force_authentication();
else
Log :: fatal(I18n::_("Authentication required but force_authentication function is not defined."));
try { try {
call_user_func($request -> handler, $request); call_user_func($request -> handler, $request);

View file

@ -54,7 +54,7 @@ class UrlRequest {
$this -> handler = $handler_info['handler']; $this -> handler = $handler_info['handler'];
$this -> authenticated = ( $this -> authenticated = (
isset($handler_info['authenticated'])? isset($handler_info['authenticated'])?
boolval($handler_info['authenticated']):true); $handler_info['authenticated']:null);
$this -> api_mode = ( $this -> api_mode = (
isset($handler_info['api_mode'])? isset($handler_info['api_mode'])?
boolval($handler_info['api_mode']):false); boolval($handler_info['api_mode']):false);
@ -78,7 +78,11 @@ class UrlRequest {
if ($key == 'handler') if ($key == 'handler')
return $this -> handler; return $this -> handler;
if ($key == 'authenticated') if ($key == 'authenticated')
return $this -> authenticated; return (
is_null($this -> authenticated)?
Auth::enabled():
(bool)$this -> authenticated
);
if ($key == 'api_mode') if ($key == 'api_mode')
return $this -> api_mode; return $this -> api_mode;
if ($key == 'referer') if ($key == 'referer')

View file

@ -99,6 +99,13 @@ function ensure_is_array($value) {
* @return mixed The cast value * @return mixed The cast value
**/ **/
function cast($value, $type, $split=false) { function cast($value, $type, $split=false) {
if (strpos($type, 'array_of_') === 0) {
$type = substr($type, 9);
$values = array();
foreach(ensure_is_array($value) as $key => $value)
$values[$key] = cast($value, $type);
return $values;
}
switch($type) { switch($type) {
case 'bool': case 'bool':
case 'boolean': case 'boolean':

View file

@ -53,11 +53,11 @@
<ul class="navbar-nav ml-md-auto"> <ul class="navbar-nav ml-md-auto">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-item nav-link dropdown-toggle mr-md-2" href="#" id="bd-versions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-item nav-link dropdown-toggle mr-md-2" href="#" id="bd-versions" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-user" aria-hidden="true"></i> {$auth_user.name|escape:"htmlall"} <span class="caret"></span> <i class="fa fa-user" aria-hidden="true"></i> {$auth_user->name|escape:"htmlall"} <span class="caret"></span>
</a> </a>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
{block name="navbar-user-menu"}{/block} {block name="navbar-user-menu"}{/block}
<a class="dropdown-item" href="?logout=1"><i class="fas fa-sign-out-alt"></i> Déconnexion</a> <a class="dropdown-item" href="logout"><i class="fas fa-sign-out-alt"></i> {t domain=$CORE_TEXT_DOMAIN}Logout{/t}</a>
</div> </div>
</li> </li>
</ul> </ul>

22
templates/login.tpl Normal file
View file

@ -0,0 +1,22 @@
{extends file='Tpl:empty.tpl'}
{block name="pagetitle"}<h2 class="center"><i class="fas fa-sign-in-alt"></i> {t domain=$CORE_TEXT_DOMAIN}Connection{/t}</h2>{/block}
{block name="content"}
<form action="login" method="POST">
<input type="hidden" name="next" value="{$next|escape:"quotes"}"/>
<div class="form-group">
<label for="username">{t domain=$CORE_TEXT_DOMAIN}Username{/t}</label>
<input type="text" class="form-control" name="username" />
</div>
<div class="form-group">
<label for="password">{t domain=$CORE_TEXT_DOMAIN}Password{/t}</label>
<input type="password" class="form-control" name="password">
</div>
<button type="submit" class="btn btn-primary">{t domain=$CORE_TEXT_DOMAIN}Submit{/t}</button>
{foreach $display_other_methods as $method => $name}
<a class='btn btn-secondary' href='login?method={$method}&next={$next|escape:"url"}'>{$name}</a>
{/foreach}
</form>
{/block}
{*
# vim: autoindent expandtab tabstop=2 shiftwidth=2 softtabstop=2
*}

11
templates/logout.tpl Normal file
View file

@ -0,0 +1,11 @@
{extends file='Tpl:empty.tpl'}
{block name="pagetitle"}{/block}
{block name="content"}
<div class="jumbotron">
<h1 class="display-4">{t domain=$CORE_TEXT_DOMAIN}Disconnected{/t}</h1>
<p class="lead">{t escape=off domain=$CORE_TEXT_DOMAIN}You are now disconnected.{/t}</p>
</div>
{/block}
{*
# vim: autoindent expandtab tabstop=2 shiftwidth=2 softtabstop=2
*}

11
templates/must_login.tpl Normal file
View file

@ -0,0 +1,11 @@
{extends file='Tpl:empty.tpl'}
{block name="pagetitle"}{/block}
{block name="content"}
<div class="jumbotron">
<h1 class="display-4">{t domain=$CORE_TEXT_DOMAIN}Access denied{/t}</h1>
<p class="lead">{t escape=off domain=$CORE_TEXT_DOMAIN}You must login to access this page.{/t}</p>
</div>
{/block}
{*
# vim: autoindent expandtab tabstop=2 shiftwidth=2 softtabstop=2
*}