Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 42 additions & 36 deletions app/Resources/views/base.html.twig
Original file line number Diff line number Diff line change
@@ -1,96 +1,102 @@
<!DOCTYPE html>
<html lang="fr">

<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">

<title>To Do List app</title>

<title>
To Do List app
</title>
<!-- Bootstrap Core CSS -->
<link href="{{ asset('css/bootstrap.min.css') }}" rel="stylesheet">

<!-- Custom CSS -->
<link href="{{ asset('css/shop-homepage.css') }}" rel="stylesheet">

<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
<![endif]-->
</head>

<body>
<nav class="navbar navbar-light navbar-fixed-top" style="background-color: #e3f2fd;" role="navigation">
<div class="container">
<body>
<nav class="navbar navbar-light navbar-fixed-top" style="background-color: #e3f2fd;" role="navigation"> <div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">To Do List app</a>

<a class="navbar-brand" href="{{ path('homepage') }}">
To Do List app
</a>
<img src="{{ asset('img/Logo_OpenClassrooms.png') }}" alt="OpenClassrooms" />
</div>
</div>
</nav>

<!-- Page Content -->
<div class="container">
<div class="row">
<a href="{{ path('user_create') }}" class="btn btn-primary">Créer un utilisateur</a>

{# app/Resources/views/base.html.twig (or your navigation template) #}
{% if is_granted('ROLE_ADMIN') is same as(true) %}
<a href="{{ path('user_list') }}" class="btn btn-primary">
Gestion des utilisateurs
</a>
<a href="{{ path('user_create') }}" class="btn btn-primary">
Créer un utilisateur
</a>
{% endif %}
{% if app.user %}
<a href="{{ path('logout') }}" class="pull-right btn btn-danger">Se déconnecter</a>
<a href="{{ path('logout') }}" class="pull-right btn btn-danger">
Se déconnecter
</a>
{% endif %}

{% if not app.user and 'login' != app.request.attributes.get('_route') %}
<a href="{{ path('login') }}" class="btn btn-success">Se connecter</a>
<a href="{{ path('login') }}" class="btn btn-success">
Se connecter
</a>
{% endif %}
</div>

<div class="row">
<div class="col-md-12">
{% for flash_message in app.session.flashBag.get('success') %}
<div class="alert alert-success" role="alert">
<strong>Superbe !</strong> {{ flash_message }}
<strong>
Superbe !
</strong>
{{ flash_message }}
</div>
{% endfor %}

{% for flash_message in app.session.flashBag.get('error') %}
<div class="alert alert-danger" role="alert">
<strong>Oops !</strong> {{ flash_message }}
<strong>
Oops !
</strong>
{{ flash_message }}
</div>
{% endfor %}

{% block header_title %}{% endblock %}
{% block header_img %}<img class="slide-image" src="{{ asset('img/todolist_homepage.jpg') }}" alt="todo list">{% endblock %}
{% block header_img %}
<img class="slide-image" src="{{ asset('img/todolist_homepage.jpg') }}" alt="todo list">
{% endblock %}
</div>
</div>

<br />

<br/>
<div class="row">
<div class="col-md-12">
{% block body %}{% endblock %}
</div>
</div>
</div>
<!-- /.container -->

<div class="container">

<hr>
<footer>
<div class="row">
<div class="col-lg-12">
<p class="pull-right">Copyright &copy; OpenClassrooms</p>
<p class="pull-right">
Copyright &copy; OpenClassrooms
</p>
</div>
</div>
</footer>

</div>

<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script src="{{ asset('js/bootstrap.min.js') }}"></script>
</body>
Expand Down
3 changes: 2 additions & 1 deletion app/config/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ security:

access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/users, roles: IS_AUTHENTICATED_ANONYMOUSLY }
# Protect all user management paths (list, create, edit)
- { path: ^/users, roles: ROLE_ADMIN }
- { path: ^/, roles: ROLE_ADMIN }
- { path: ^/, roles: ROLE_USER }
40 changes: 36 additions & 4 deletions src/AppBundle/Controller/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,57 @@

use AppBundle\Entity\User;
use AppBundle\Form\UserType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
* Class UserController
*
* Manages administrative user operations including list viewing, creation, and updating.
* Access to this entire controller is restricted strictly to users with ROLE_ADMIN.
*
* @package AppBundle\Controller
*/
class UserController extends Controller
{
/**
* Display the list of all registered users.
*
* @Route("/users", name="user_list")
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function listAction()
{
// Restrict access to administrative users only
$this->denyAccessUnlessGranted('ROLE_ADMIN');

return $this->render('user/list.html.twig', [
'users' => $this->getDoctrine()->getRepository('AppBundle:User')->findAll()
]);
}

/**
* Create and store a new user.
*
* @Route("/users/create", name="user_create")
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function createAction(Request $request)
{
// Restrict access to administrative users only
$this->denyAccessUnlessGranted('ROLE_ADMIN');

$user = new User();
$form = $this->createForm(UserType::class, $user);

$form->handleRequest($request);

// Check if the form is submitted and valid before processing
if ($form->isSubmitted() && $form->isValid()) {
if ($form->isSubmitted() === true && $form->isValid() === true) {
$em = $this->getDoctrine()->getManager();

// Encode the plain text password provided in the form
Expand All @@ -50,21 +73,30 @@ public function createAction(Request $request)
}

/**
* Edit an existing user's profile and roles.
*
* @Route("/users/{id}/edit", name="user_edit")
*
* @param User $user
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function editAction(User $user, Request $request)
{
// Restrict access to administrative users only
$this->denyAccessUnlessGranted('ROLE_ADMIN');

// Store the existing hashed password in case the password field is left blank
$oldPassword = $user->getPassword();

$form = $this->createForm(UserType::class, $user);
$form->handleRequest($request);

// Check if the form is submitted and valid before processing
if ($form->isSubmitted() && $form->isValid()) {
if ($form->isSubmitted() === true && $form->isValid() === true) {

// Check if a new password has been filled in the form
if (!empty($user->getPassword())) {
if (empty($user->getPassword()) === false) {
// Encode and set the new password
$password = $this->get('security.password_encoder')->encodePassword($user, $user->getPassword());
$user->setPassword($password);
Expand Down
2 changes: 1 addition & 1 deletion src/AppBundle/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public function getRoles()
$roles = $this->roles;

// Guarantee every user at least has ROLE_USER
if (!in_array('ROLE_USER', $roles, true)) {
if (in_array('ROLE_USER', $roles) === false) {
$roles[] = 'ROLE_USER';
}

Expand Down
2 changes: 1 addition & 1 deletion src/AppBundle/Form/UserType.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
->addModelTransformer(new CallbackTransformer(
function ($rolesAsArray) {
// Convert the entity array (e.g. ['ROLE_USER']) to a single string to auto-select the form option
return count($rolesAsArray) ? $rolesAsArray[0] : null;
return count($rolesAsArray) > 0 ? $rolesAsArray[0] : null;
},
function ($roleAsString) {
// Convert the selected string back into an array (e.g. ['ROLE_USER']) to store it in the database
Expand Down
90 changes: 56 additions & 34 deletions tests/AppBundle/Controller/UserControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,53 +3,75 @@
namespace Tests\AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use AppBundle\Entity\User;

/**
* Class UserControllerTest
*
* Runs functional tests against the UserController routes.
* Validates access control lists (ACL) ensuring user management remains restricted
* to authorized administrative roles.
*
* @package Tests\AppBundle\Controller
*/
class UserControllerTest extends WebTestCase
{
/**
* Test that an administrator can update another user's role to ROLE_ADMIN.
* Helper method to create an authenticated HTTP client.
*
* @return void
* Simulates basic HTTP authentication headers to log in a user
* before sending requests.
*
* @param string $username The username of the user to authenticate
* @param string $password The plain-text password of the user
* @return \Symfony\Bundle\FrameworkBundle\Client An authenticated browser-like client instance
*/
public function testAdminCanChangeUserRole()
private function createAuthenticatedClient($username, $password)
{
// 1. Simulate logging in as an admin
$client = static::createClient([], [
'PHP_AUTH_USER' => 'Mike',
'PHP_AUTH_PW' => 'password123',
return static::createClient([], [
'PHP_AUTH_USER' => $username,
'PHP_AUTH_PW' => $password,
]);
}

$container = $client->getContainer();
$em = $container->get('doctrine')->getManager();

// 2. Retrieve the user we want to modify (SimpleUser)
/** @var User $userToModify */
$userToModify = $em->getRepository('AppBundle:User')->findOneBy(['username' => 'Mike']);
$this->assertNotNull($userToModify, 'The fixture user "SimpleUser" is missing.');

// 3. Request the edit page
$crawler = $client->request('GET', '/users/' . $userToModify->getId() . '/edit');
$this->assertEquals(200, $client->getResponse()->getStatusCode());

// 4. Select and submit the form, shifting the role to ROLE_ADMIN
$form = $crawler->selectButton('Modifier')->form();
/**
* Test that a standard user (ROLE_USER) is restricted from accessing admin routes.
*
* Validates that accessing '/users' and '/users/create' yields a 403 Forbidden
* HTTP status code when requested by unauthorized accounts.
*
* @return void
*/
public function testSimpleUserCannotAccessUserManagement()
{
// 1. Arrange: Authenticate as a regular user (ROLE_USER)
$client = $this->createAuthenticatedClient('JohnDoe', 'password123');

// For choice fields use assignment. If the field accepts multiple values provide an array.
$form['user[roles]'] = 'ROLE_ADMIN';
// 2. Act & Assert: Attempt to browse the user list page
$client->request('GET', '/users');
$this->assertEquals(403, $client->getResponse()->getStatusCode());

$client->submit($form);
// 3. Act & Assert: Attempt to reach the user creation form
$client->request('GET', '/users/create');
$this->assertEquals(403, $client->getResponse()->getStatusCode());
}

// 5. Assert successful redirect to the list page
$this->assertEquals(302, $client->getResponse()->getStatusCode());
$client->followRedirect();
$this->assertContains("utilisateur a bien été modifié", $client->getResponse()->getContent());
/**
* Test that an administrator (ROLE_ADMIN) can successfully manage users.
*
* Validates that accessing '/users' yields a 200 OK HTTP status code
* when the client holds the required administrative credentials.
*
* @return void
*/
public function testAdminCanAccessUserList()
{
// 1. Arrange: Authenticate as an admin user (ROLE_ADMIN)
$client = $this->createAuthenticatedClient('Mike', 'password123');

// 6. Force Doctrine to fetch updated data from the database
$em->clear();
$updatedUser = $em->getRepository('AppBundle:User')->find($userToModify->getId());
// 2. Act: Query the restricted user list route
$client->request('GET', '/users');

// 7. Assert that the role has been successfully modified in DB
$this->assertContains('ROLE_ADMIN', $updatedUser->getRoles());
// 3. Assert: Verify the page loads successfully
$this->assertEquals(200, $client->getResponse()->getStatusCode());
}
}
Loading
Loading