Skip to content

Commit 069cca2

Browse files
authored
Merge pull request #36 from crf-devs/feature/organization-authentication-layer
Make `Organization` usable as authenticated users
2 parents 6257b79 + 6b649b3 commit 069cca2

19 files changed

+428
-44
lines changed

Diff for: config/packages/security.yaml

+27-3
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,43 @@ security:
55
entity:
66
class: App\Entity\User
77

8+
organizations:
9+
entity:
10+
class: App\Entity\Organization
11+
812
encoders:
9-
App\Entity\User:
13+
Symfony\Component\Security\Core\User\UserInterface:
1014
algorithm: auto
1115

1216
firewalls:
1317
dev:
1418
pattern: ^/(_(profiler|wdt)|css|images|js)/
1519
security: false
20+
21+
organizations:
22+
pattern: ^/organizations
23+
anonymous: lazy
24+
provider: organizations
25+
guard:
26+
authenticators:
27+
- App\Security\OrganizationLoginFormAuthenticator
28+
29+
logout:
30+
path: app_organization_logout
31+
target: app_organization_index
32+
33+
remember_me:
34+
secret: '%kernel.secret%'
35+
lifetime: 604800 # 1 week in seconds
36+
path: /
37+
1638
main:
1739
anonymous: lazy
1840
provider: users
1941
guard:
2042
authenticators:
21-
- App\Security\LoginFormAuthenticator
43+
- App\Security\UserLoginFormAuthenticator
44+
2245
logout:
2346
path: app_logout
2447
target: user_home
@@ -29,5 +52,6 @@ security:
2952
path: /
3053

3154
access_control:
32-
- { path: ^/(user\/new|login)$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
55+
- { path: ^/(user\/new|login|organizations\/login)$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
56+
- { path: ^/organizations, roles: ROLE_ORGANIZATION }
3357
- { path: ^/, roles: ROLE_USER }

Diff for: config/routes.yaml

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
#index:
2-
# path: /
3-
# controller: App\Controller\DefaultController::index
1+
_organizations:
2+
resource: ../src/Controller/Organization/
3+
type: annotation
4+
prefix: /organizations

Diff for: config/services.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,6 @@ services:
4646
arguments:
4747
$availableSkillSets: '%app.available_skill_sets%'
4848

49-
App\Security\LoginFormAuthenticator:
49+
App\Security\UserLoginFormAuthenticator:
5050
arguments:
5151
$password: 'covid19'

Diff for: src/Controller/Organization/IndexController.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Organization;
6+
7+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
8+
use Symfony\Component\HttpFoundation\Response;
9+
use Symfony\Component\Routing\Annotation\Route;
10+
11+
/**
12+
* @Route("", name="app_organization_index")
13+
*/
14+
final class IndexController extends AbstractController
15+
{
16+
public function __invoke(): Response
17+
{
18+
return $this->render('organization/index.html.twig');
19+
}
20+
}
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Organization\Security;
6+
7+
use App\Repository\OrganizationRepository;
8+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
9+
use Symfony\Component\HttpFoundation\Response;
10+
use Symfony\Component\Routing\Annotation\Route;
11+
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
12+
13+
/**
14+
* @Route("/login", name="app_organization_login")
15+
*/
16+
final class LoginController extends AbstractController
17+
{
18+
private AuthenticationUtils $authenticationUtils;
19+
private OrganizationRepository $organizationRepository;
20+
21+
public function __construct(AuthenticationUtils $authenticationUtils, OrganizationRepository $organizationRepository)
22+
{
23+
$this->authenticationUtils = $authenticationUtils;
24+
$this->organizationRepository = $organizationRepository;
25+
}
26+
27+
public function __invoke(): Response
28+
{
29+
return $this->render('organization/login.html.twig', [
30+
'organizations' => $this->organizationRepository->loadActiveOrganizations(),
31+
'last_username' => $this->authenticationUtils->getLastUsername(),
32+
'error' => $this->authenticationUtils->getLastAuthenticationError(),
33+
]);
34+
}
35+
}
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Organization\Security;
6+
7+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
8+
use Symfony\Component\HttpFoundation\Response;
9+
use Symfony\Component\Routing\Annotation\Route;
10+
11+
/**
12+
* @Route("/logout", name="app_organization_logout")
13+
*/
14+
final class LogoutController extends AbstractController
15+
{
16+
public function __invoke(): Response
17+
{
18+
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
19+
}
20+
}

Diff for: src/Controller/Security/LoginController.php

-24
This file was deleted.

Diff for: src/Controller/User/Security/LoginController.php

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\User\Security;
6+
7+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
8+
use Symfony\Component\HttpFoundation\Response;
9+
use Symfony\Component\Routing\Annotation\Route;
10+
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
11+
12+
/**
13+
* @Route("/login", name="app_login")
14+
*/
15+
final class LoginController extends AbstractController
16+
{
17+
private AuthenticationUtils $authenticationUtils;
18+
19+
public function __construct(AuthenticationUtils $authenticationUtils)
20+
{
21+
$this->authenticationUtils = $authenticationUtils;
22+
}
23+
24+
public function __invoke(): Response
25+
{
26+
return $this->render('user/login.html.twig', [
27+
'last_username' => $this->authenticationUtils->getLastUsername(),
28+
'error' => $this->authenticationUtils->getLastAuthenticationError(),
29+
]);
30+
}
31+
}

Diff for: src/Controller/Security/LogoutController.php renamed to src/Controller/User/Security/LogoutController.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22

33
declare(strict_types=1);
44

5-
namespace App\Controller\Security;
5+
namespace App\Controller\User\Security;
66

77
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
88
use Symfony\Component\HttpFoundation\Response;
99
use Symfony\Component\Routing\Annotation\Route;
1010

11+
/**
12+
* @Route("/logout", name="app_logout")
13+
*/
1114
final class LogoutController extends AbstractController
1215
{
13-
/**
14-
* @Route("/logout", name="app_logout")
15-
*/
1616
public function __invoke(): Response
1717
{
1818
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');

Diff for: src/DataFixtures/ApplicationFixtures.php

+37-5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use App\Entity\UserAvailability;
1111
use Doctrine\Bundle\FixturesBundle\Fixture;
1212
use Doctrine\Persistence\ObjectManager;
13+
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
1314

1415
final class ApplicationFixtures extends Fixture
1516
{
@@ -35,10 +36,17 @@ final class ApplicationFixtures extends Fixture
3536
];
3637

3738
/** @var Organization[] */
38-
private $organizations = [];
39+
private array $organizations = [];
3940

4041
/** @var User[] */
41-
private $users = [];
42+
private array $users = [];
43+
44+
private EncoderFactoryInterface $encoders;
45+
46+
public function __construct(EncoderFactoryInterface $encoders)
47+
{
48+
$this->encoders = $encoders;
49+
}
4250

4351
public function load(ObjectManager $manager): void
4452
{
@@ -52,12 +60,20 @@ public function load(ObjectManager $manager): void
5260

5361
private function loadOrganizations(ObjectManager $manager): void
5462
{
55-
$this->organizations['DT75'] = $main = new Organization(null, 'DT75');
63+
// Yield same password for all organizations.
64+
// Password generation can be expensive and time consuming.
65+
$encoder = $this->encoders->getEncoder(Organization::class);
66+
$password = $encoder->encodePassword('organization2020', null);
5667

57-
$manager->persist($main);
68+
$this->addOrganization($this->makeOrganization('INACTIVE_ORG'));
69+
$this->addOrganization($this->makeOrganization('DT75', $password));
5870

5971
foreach (self::ORGANIZATIONS as $name) {
60-
$this->organizations[$name] = $organization = new Organization(null, $name, $main);
72+
$this->addOrganization($this->makeOrganization($name, $password, $this->organizations['DT75']));
73+
}
74+
75+
// Persist all organizations
76+
foreach ($this->organizations as $organization) {
6177
$manager->persist($organization);
6278
}
6379
}
@@ -139,4 +155,20 @@ private function loadUserAvailabilities(ObjectManager $manager): void
139155
}
140156
}
141157
}
158+
159+
private function makeOrganization(string $name, string $password = null, Organization $parent = null): Organization
160+
{
161+
$organization = new Organization(null, $name, $parent);
162+
163+
if ($password) {
164+
$organization->password = $password;
165+
}
166+
167+
return $organization;
168+
}
169+
170+
private function addOrganization(Organization $organization): void
171+
{
172+
$this->organizations[$organization->name] = $organization;
173+
}
142174
}

Diff for: src/Entity/Organization.php

+31-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@
55
namespace App\Entity;
66

77
use Doctrine\ORM\Mapping as ORM;
8+
use Symfony\Component\Security\Core\User\UserInterface;
89

910
/**
11+
* @ORM\Table(
12+
* uniqueConstraints={
13+
* @ORM\UniqueConstraint(name="organisation_name_unique", columns={"name"})
14+
* }
15+
* )
1016
* @ORM\Entity(repositoryClass="App\Repository\OrganizationRepository")
1117
*/
12-
class Organization
18+
class Organization implements UserInterface
1319
{
1420
/**
1521
* @ORM\Id
@@ -48,4 +54,28 @@ public function __toString(): string
4854

4955
return $this->name;
5056
}
57+
58+
public function getRoles(): array
59+
{
60+
return ['ROLE_ORGANIZATION'];
61+
}
62+
63+
public function getPassword(): ?string
64+
{
65+
return $this->password;
66+
}
67+
68+
public function getSalt(): ?string
69+
{
70+
return null;
71+
}
72+
73+
public function getUsername(): string
74+
{
75+
return $this->name;
76+
}
77+
78+
public function eraseCredentials(): void
79+
{
80+
}
5181
}

Diff for: src/Migrations/Version20200322144012.php

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
final class Version20200322144012 extends AbstractMigration
11+
{
12+
public function up(Schema $schema): void
13+
{
14+
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
15+
16+
$this->addSql('CREATE UNIQUE INDEX organisation_name_unique ON organization (name)');
17+
}
18+
19+
public function down(Schema $schema): void
20+
{
21+
$this->abortIf('postgresql' !== $this->connection->getDatabasePlatform()->getName(), 'Migration can only be executed safely on \'postgresql\'.');
22+
23+
$this->addSql('DROP INDEX organisation_name_unique');
24+
}
25+
}

Diff for: src/Repository/OrganizationRepository.php

+20
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,24 @@ public function __construct(ManagerRegistry $registry)
2020
{
2121
parent::__construct($registry, Organization::class);
2222
}
23+
24+
public function loadUserByUsername(string $name): ?Organization
25+
{
26+
return $this->findOneBy(['name' => $name]);
27+
}
28+
29+
/**
30+
* @return Organization[]
31+
*/
32+
public function loadActiveOrganizations(): array
33+
{
34+
$qb = $this->createQueryBuilder('o');
35+
36+
return $qb
37+
->where($qb->expr()->isNotNull('o.password'))
38+
->orderBy('o.name', 'ASC')
39+
->getQuery()
40+
->getResult()
41+
;
42+
}
2343
}

0 commit comments

Comments
 (0)