mirror of
https://github.com/tchapi/davis.git
synced 2025-01-18 05:18:35 +01:00
Add authMethod for /dav
Add guard authentification for the administration interface, with a login form
This commit is contained in:
parent
75a84eece5
commit
d1f1981ac7
4
.env
4
.env
@ -42,6 +42,10 @@ ADMIN_PASSWORD=test
|
||||
# Auth Realm for HTTP auth
|
||||
AUTH_REALM=SabreDAV
|
||||
|
||||
# Auth Method for the frontend
|
||||
# "Basic" or "Digest"
|
||||
AUTH_METHOD=Digest
|
||||
|
||||
# Do we enable caldav and carddav ?
|
||||
CALDAV_ENABLED=true
|
||||
CARDDAV_ENABLED=true
|
||||
|
@ -1,22 +1,23 @@
|
||||
security:
|
||||
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
|
||||
providers:
|
||||
in_memory: { memory: null }
|
||||
admin_user_provider:
|
||||
id: App\Security\AdminUserProvider
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
main:
|
||||
anonymous: true
|
||||
guard:
|
||||
authenticators:
|
||||
- App\Security\LoginFormAuthenticator
|
||||
provider: admin_user_provider
|
||||
logout:
|
||||
path: app_logout
|
||||
target: dashboard
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#firewalls-authentication
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
- { path: ^/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/dav, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/dashboard, roles: ROLE_ADMIN }
|
||||
- { path: ^/users, roles: ROLE_ADMIN }
|
||||
|
@ -17,23 +17,28 @@ services:
|
||||
resource: '../src/*'
|
||||
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
|
||||
|
||||
App\Services\Utils:
|
||||
arguments:
|
||||
$authRealm: "%env(AUTH_REALM)%"
|
||||
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
App\Controller\:
|
||||
resource: '../src/Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
|
||||
App\Controller\DAVController:
|
||||
arguments:
|
||||
$calDAVEnabled: "%env(bool:CALDAV_ENABLED)%"
|
||||
$cardDAVEnabled: "%env(bool:CARDDAV_ENABLED)%"
|
||||
$webDAVEnabled: "%env(bool:WEBDAV_ENABLED)%"
|
||||
$inviteAddress: "%env(INVITE_FROM_ADDRESS)%"
|
||||
$authMethod: "%env(AUTH_METHOD)%"
|
||||
$authRealm: "%env(AUTH_REALM)%"
|
||||
$publicDir: "%env(PUBLIC_DIR)%"
|
||||
$tmpDir: "%env(TMP_DIR)%"
|
||||
|
||||
App\Controller\AdminController:
|
||||
App\Security\LoginFormAuthenticator:
|
||||
arguments:
|
||||
$authRealm: "%env(AUTH_REALM)%"
|
||||
$adminLogin: "%env(ADMIN_LOGIN)%"
|
||||
$adminPassword: "%env(ADMIN_PASSWORD)%"
|
@ -14,6 +14,7 @@ use App\Entity\User;
|
||||
use App\Form\AddressBookType;
|
||||
use App\Form\CalendarInstanceType;
|
||||
use App\Form\UserType;
|
||||
use App\Services\Utils;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
@ -21,18 +22,6 @@ use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
class AdminController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* HTTP authentication realm.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $authRealm;
|
||||
|
||||
public function __construct(?string $authRealm)
|
||||
{
|
||||
$this->authRealm = $authRealm ?? 'SabreDAV';
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/dashboard", name="dashboard")
|
||||
*/
|
||||
@ -72,7 +61,7 @@ class AdminController extends AbstractController
|
||||
* @Route("/users/new", name="user_create")
|
||||
* @Route("/users/edit/{username}", name="user_edit")
|
||||
*/
|
||||
public function userCreate(Request $request, ?string $username, TranslatorInterface $trans)
|
||||
public function userCreate(Utils $utils, Request $request, ?string $username, TranslatorInterface $trans)
|
||||
{
|
||||
if ($username) {
|
||||
$user = $this->get('doctrine')->getRepository(User::class)->findOneByUsername($username);
|
||||
@ -102,7 +91,7 @@ class AdminController extends AbstractController
|
||||
// The user is not new and does not want to change its password
|
||||
$user->setPassword($oldHash);
|
||||
} else {
|
||||
$hash = md5($user->getUsername().':'.$this->authRealm.':'.$user->getPassword());
|
||||
$hash = $utils->hashPassword($user->getUsername(), $user->getPassword());
|
||||
$user->setPassword($hash);
|
||||
}
|
||||
|
||||
|
@ -3,12 +3,16 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Services\BasicAuth;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
class DAVController extends AbstractController
|
||||
{
|
||||
const AUTH_DIGEST = 'Digest';
|
||||
const AUTH_BASIC = 'Basic';
|
||||
|
||||
/**
|
||||
* Is CalDAV enabled?
|
||||
*
|
||||
@ -58,12 +62,14 @@ class DAVController extends AbstractController
|
||||
*/
|
||||
protected $tmpDir;
|
||||
|
||||
public function __construct(bool $calDAVEnabled = true, bool $cardDAVEnabled = true, bool $webDAVEnabled = false, ?string $inviteAddress, ?string $authRealm, ?string $publicDir, ?string $tmpDir)
|
||||
public function __construct(bool $calDAVEnabled = true, bool $cardDAVEnabled = true, bool $webDAVEnabled = false, ?string $inviteAddress, ?string $authMethod, ?string $authRealm, ?string $publicDir, ?string $tmpDir)
|
||||
{
|
||||
$this->calDAVEnabled = $calDAVEnabled;
|
||||
$this->cardDAVEnabled = $cardDAVEnabled;
|
||||
$this->webDAVEnabled = $webDAVEnabled;
|
||||
$this->inviteAddress = $inviteAddress ?? null;
|
||||
|
||||
$this->authMethod = $authMethod;
|
||||
$this->authRealm = $authRealm ?? User::DEFAULT_AUTH_REALM;
|
||||
|
||||
$this->publicDir = $publicDir;
|
||||
@ -81,15 +87,25 @@ class DAVController extends AbstractController
|
||||
/**
|
||||
* @Route("/dav/{path}", name="dav", requirements={"path":".*"})
|
||||
*/
|
||||
public function dav()
|
||||
public function dav(BasicAuth $basicAuthBackend)
|
||||
{
|
||||
$pdo = $this->get('doctrine')->getEntityManager()->getConnection()->getWrappedConnection();
|
||||
|
||||
/**
|
||||
/*
|
||||
* The backends.
|
||||
*/
|
||||
$authBackend = new \Sabre\DAV\Auth\Backend\PDO($pdo);
|
||||
switch ($this->authMethod) {
|
||||
case self::AUTH_DIGEST:
|
||||
$authBackend = new \Sabre\DAV\Auth\Backend\PDO($pdo);
|
||||
break;
|
||||
case self::AUTH_BASIC:
|
||||
default:
|
||||
$authBackend = $basicAuthBackend;
|
||||
break;
|
||||
}
|
||||
|
||||
$authBackend->setRealm($this->authRealm);
|
||||
|
||||
$principalBackend = new \Sabre\DAVACL\PrincipalBackend\PDO($pdo);
|
||||
|
||||
/**
|
||||
|
36
src/Controller/SecurityController.php
Normal file
36
src/Controller/SecurityController.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/login", name="app_login")
|
||||
*/
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
// if ($this->getUser()) {
|
||||
// return $this->redirectToRoute('target_path');
|
||||
// }
|
||||
|
||||
// get the login error if there is one
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
// last username entered by the user
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/logout", name="app_logout")
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
throw new \Exception('This method can be blank - it will be intercepted by the logout key on your firewall');
|
||||
}
|
||||
}
|
70
src/Entity/AdminUser.php
Normal file
70
src/Entity/AdminUser.php
Normal file
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class AdminUser implements UserInterface
|
||||
{
|
||||
private $username;
|
||||
private $password;
|
||||
|
||||
public function __construct(string $username, string $password)
|
||||
{
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return (Role|string)[] The user roles
|
||||
*/
|
||||
public function getRoles()
|
||||
{
|
||||
return ['ROLE_ADMIN'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the password used to authenticate the user.
|
||||
*
|
||||
* This should be the encoded password. On authentication, a plain-text
|
||||
* password will be salted, encoded, and then compared to this value.
|
||||
*
|
||||
* @return string|null The encoded password if any
|
||||
*/
|
||||
public function getPassword()
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the salt that was originally used to encode the password.
|
||||
*
|
||||
* This can return null if the password was not encoded using a salt.
|
||||
*
|
||||
* @return string|null The salt
|
||||
*/
|
||||
public function getSalt()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username used to authenticate the user.
|
||||
*
|
||||
* @return string The username
|
||||
*/
|
||||
public function getUsername()
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes sensitive data from the user.
|
||||
*
|
||||
* This is important if, at any given point, sensitive information like
|
||||
* the plain-text password is stored on this object.
|
||||
*/
|
||||
public function eraseCredentials()
|
||||
{
|
||||
}
|
||||
}
|
63
src/Security/AdminUserProvider.php
Normal file
63
src/Security/AdminUserProvider.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\AdminUser;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
class AdminUserProvider implements UserProviderInterface
|
||||
{
|
||||
/**
|
||||
* Symfony calls this method if you use features like switch_user
|
||||
* or remember_me.
|
||||
*
|
||||
* If you're not using these features, you do not need to implement
|
||||
* this method.
|
||||
*
|
||||
*
|
||||
* @param mixed $username
|
||||
*
|
||||
* @throws UsernameNotFoundException if the user is not found
|
||||
*
|
||||
* @return UserInterface
|
||||
*/
|
||||
public function loadUserByUsername($username)
|
||||
{
|
||||
throw new \Exception('Not implemented, because not needed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the user after being reloaded from the session.
|
||||
*
|
||||
* When a user is logged in, at the beginning of each request, the
|
||||
* User object is loaded from the session and then this method is
|
||||
* called. Your job is to make sure the user's data is still fresh by,
|
||||
* for example, re-querying for fresh User data.
|
||||
*
|
||||
* If your firewall is "stateless: true" (for a pure API), this
|
||||
* method is not called.
|
||||
*
|
||||
* @return UserInterface
|
||||
*/
|
||||
public function refreshUser(UserInterface $user)
|
||||
{
|
||||
if (!$user instanceof AdminUser) {
|
||||
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells Symfony to use this provider for this User class.
|
||||
*
|
||||
* @param mixed $class
|
||||
*/
|
||||
public function supportsClass($class)
|
||||
{
|
||||
return AdminUser::class === $class;
|
||||
}
|
||||
}
|
92
src/Security/LoginFormAuthenticator.php
Normal file
92
src/Security/LoginFormAuthenticator.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\AdminUser;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Csrf\CsrfToken;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
|
||||
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
|
||||
{
|
||||
use TargetPathTrait;
|
||||
|
||||
private $urlGenerator;
|
||||
private $csrfTokenManager;
|
||||
|
||||
private $adminLogin;
|
||||
private $adminPassword;
|
||||
|
||||
public function __construct(UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, string $adminLogin, string $adminPassword)
|
||||
{
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->csrfTokenManager = $csrfTokenManager;
|
||||
$this->adminLogin = $adminLogin;
|
||||
$this->adminPassword = $adminPassword;
|
||||
}
|
||||
|
||||
public function supports(Request $request)
|
||||
{
|
||||
return 'app_login' === $request->attributes->get('_route')
|
||||
&& $request->isMethod('POST');
|
||||
}
|
||||
|
||||
public function getCredentials(Request $request)
|
||||
{
|
||||
$credentials = [
|
||||
'username' => $request->request->get('username'),
|
||||
'password' => $request->request->get('password'),
|
||||
'csrf_token' => $request->request->get('_csrf_token'),
|
||||
];
|
||||
$request->getSession()->set(
|
||||
Security::LAST_USERNAME,
|
||||
$credentials['username']
|
||||
);
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
|
||||
public function getUser($credentials, UserProviderInterface $userProvider)
|
||||
{
|
||||
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
|
||||
if (!$this->csrfTokenManager->isTokenValid($token)) {
|
||||
throw new InvalidCsrfTokenException();
|
||||
}
|
||||
|
||||
if ($credentials['username'] !== $this->adminLogin) {
|
||||
// fail authentication with a custom error
|
||||
throw new CustomUserMessageAuthenticationException('Username could not be found.');
|
||||
}
|
||||
|
||||
return new AdminUser($this->adminLogin, $this->adminPassword);
|
||||
}
|
||||
|
||||
public function checkCredentials($credentials, UserInterface $user)
|
||||
{
|
||||
return $user->getPassword() === $credentials['password'];
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
|
||||
{
|
||||
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
|
||||
return new RedirectResponse($targetPath);
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->generate('dashboard'));
|
||||
}
|
||||
|
||||
protected function getLoginUrl()
|
||||
{
|
||||
return $this->urlGenerator->generate('app_login');
|
||||
}
|
||||
}
|
51
src/Services/BasicAuth.php
Normal file
51
src/Services/BasicAuth.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Sabre\DAV\Auth\Backend\AbstractBasic;
|
||||
|
||||
final class BasicAuth extends AbstractBasic
|
||||
{
|
||||
/**
|
||||
* Utils class.
|
||||
*
|
||||
* @var Utils
|
||||
*/
|
||||
private $utils;
|
||||
|
||||
/**
|
||||
* Doctrine User repository.
|
||||
*
|
||||
* @var UserRepository
|
||||
*/
|
||||
private $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository, Utils $utils)
|
||||
{
|
||||
$this->utils = $utils;
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function validateUserPass($username, $password)
|
||||
{
|
||||
$user = $this->userRepository->findOneByUsername($username);
|
||||
|
||||
if (!$user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$hash = $this->utils->hashPassword($username, $password);
|
||||
|
||||
if ($hash === $user->getPassword()) {
|
||||
$this->currentUser = $username;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
26
src/Services/Utils.php
Normal file
26
src/Services/Utils.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
final class Utils
|
||||
{
|
||||
/**
|
||||
* Authentication realm.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $authRealm;
|
||||
|
||||
public function __construct(?string $authRealm)
|
||||
{
|
||||
$this->authRealm = $authRealm ?? User::DEFAULT_AUTH_REALM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a password according to the realm.
|
||||
*/
|
||||
public function hashPassword(string $username, string $password): string
|
||||
{
|
||||
return md5($username.':'.$this->authRealm.':'.$password);
|
||||
}
|
||||
}
|
@ -15,6 +15,18 @@
|
||||
<a class="nav-link" href="{{ path('users') }}">{{ "title.users_and_resources"|trans }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% if app.user %}
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
{{ app.user.username }}
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
|
||||
<a class="dropdown-item" href="{{ path('app_logout') }}">Logout</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
31
templates/security/login.html.twig
Normal file
31
templates/security/login.html.twig
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
{% set menu = null %}
|
||||
|
||||
{% block body %}
|
||||
<form method="post">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% if app.user %}
|
||||
<div class="mb-3">
|
||||
You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="inputUsername" class="sr-only">Login</label>
|
||||
<input type="text" value="{{ last_username }}" name="username" id="inputUsername" class="form-control" placeholder="Username" required autofocus>
|
||||
<small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputPassword" class="sr-only">Password</label>
|
||||
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
</form>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user