BONJOUR ! đź‘‹
@Florian_FB



Magnifique photo corporate
Florian Bogey
Lead Développeur PHP/Symfony



@Florian_FB
Florian-B
GL events
@Florian_FB


@Florian_FB

Rôles & Permissions​
Comment développer une marque blanche avec du Feature Flipping
@Florian_FB

Role vs Permission
Les RĂ´les
-
Catégorie attribuée à l'utilisateur
-
Attribué de manière statique
-
Détermine les actions autorisées
@Florian_FB

Role vs Permission
# config/packages/security.yaml
security:
role_hierarchy:
ROLE_SUPER_ADMIN: [ROLE_ADMIN]
access_control:
- { path: ^/admin/login, roles: PUBLIC_ACCESS }
- { path: ^/admin/cfp, roles: ROLE_SUPER_ADMIN }
- { path: ^/admin, roles: ROLE_ADMIN }
@Florian_FB

Role vs Permission
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class ListPropositionController
{
#[Route('/admin/cfp/{id}/list', name: 'admin_cfp_list')]
#[IsGranted('ROLE_SUPER_ADMIN')]
public function __invoke(Event $event): Response
{
// do amazing stuff here
}
}
@Florian_FB

Role vs Permission
{% if is_granted('ROLE_SUPER_ADMIN') %}
<a href="{{ path('my_path') }}">button title</a>
{% endif %}
use Symfony\Bundle\SecurityBundle\Security;
public function __construct(private readonly Security $security) {}
public function myAmazingFunction(): void
{
if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
// do something
}
}
@Florian_FB

Role vs Permission
les Permissions
Règle métier spécifique
Appelée de manière dynamique
Roles & Permissions sont complémentaires
Voter
@Florian_FB

Role vs Permission









Attribute : edit_document
subject : document
@Florian_FB

Role vs Permission
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
abstract class Voter implements VoterInterface
{
abstract protected function supports(
string $attribute,
mixed $subject
): bool;
abstract protected function voteOnAttribute(
string $attribute,
mixed $subject,
TokenInterface $token
): bool;
}
@Florian_FB

Role vs Permission
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class ListPropositionController
{
#[Route('/admin/cfp/{id}/list', name: 'admin_cfp_list')]
#[IsGranted('ROLE_SUPER_ADMIN')]
#[IsGranted('list_cfp', 'event')]
public function __invoke(Event $event): Response
{
// do amazing stuff here
}
}
@Florian_FB

Role vs Permission


























Affirmative (défaut)
Consensus
Unanimous
Priority






prio : 2
prio : 1
prio : 0
@Florian_FB

Mais pourquoi je vous parle de ça ?
🤔
@Florian_FB

Le client exigeant
Identité visuelle
Nom de domaine
Mire de login
Propres fonctionnalités
Cacher des fonctionnalités
Profils d'utilisateurs
Vendre l'application en externe 🚀
@Florian_FB

Le client exigeant

@Florian_FB

Le client exigeant
​
-
Identité visuelle
-
Nom de domaine
-
Mire de login
-
Propres fonctionnalités
-
Cacher des fonctionnalités
-
Profils d'utilisateurs
Marque blanche
Feature flipping
@Florian_FB

Marque blanche
@Florian_FB

Marque blanche
-
Une seule application
-
Multi clients
-
Nom de domaine dédié
-
Charte graphique dédiée
-
Fonctionnalités dédiées
-
Impression d'un développement en interne
@Florian_FB

MARQUE BLANCHE
architecturE SINGLE-Tenant









@Florian_FB

MARQUE BLANCHE
architecturE Multi-Tenant






@Florian_FB

Créons une MARQUE BLANCHE
# my_white_label.conf
# Apache
SetEnv app_white_label symfony
# Nginx
set $app_white_label symfony;
@Florian_FB

# public/index.php
use App\Kernel;
use Symfony\Component\Dotenv\Dotenv;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$whiteLabel = $context['APP_WHITE_LABEL'] ?? null;
$whiteLabelEnvFile = sprintf('%s/.env.wl.%s', dirname(__DIR__), $whiteLabel);
if (null !== $whiteLabel && file_exists($whiteLabelEnvFile)) {
(new Dotenv())->loadEnv($whiteLabelEnvFile);
}
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
Créons une MARQUE BLANCHE
@Florian_FB

# .env.wl.symfony
APP_STYLE_ENTRY=symfony
APP_LOGO=logo-symfony.png
APP_FAVICON=favicon-symfony.png
APP_MY_VAR=foo
...
Créons une MARQUE BLANCHE
@Florian_FB

# config/services.yaml
App\Twig\WhiteLabelExtension:
arguments:
$appStyleEntry: '%env(string:APP_STYLE_ENTRY)%'
$appLogo: '%env(string:APP_LOGO)%'
$appFavicon: '%env(string:APP_FAVICON)%'
Créons une MARQUE BLANCHE
@Florian_FB

class WhiteLabelExtension extends AbstractExtension
{
public function __construct(private readonly string $appLogo) {}
public function getFunctions(): array
{
return [
new TwigFunction('get_logo', [$this, 'getLogo']),
];
}
public function getLogo(): string
{
return sprintf('path/to/logo/%s', $this->appLogo);
}
}
Créons une MARQUE BLANCHE
@Florian_FB

feature flipping
aussi appelé Feature Flag
@Florian_FB

Feature Flipping
-
Déploiement continu
-
Déploiement progressif
-
Déploiement sécurisé
-
En fonction de l'utilisateur
-
Fonctionnement dégradé
@Florian_FB

Bundles Symfony
@Florian_FB

Bundles Symfony
features:
my_feature_1: false
my_feature_2: true
if ($this->featureManager->isEnabled('my_feature_1')) {
// my_feature_1 is enabled
}
if ($this->featureManager->isDisabled('my_feature_2')) {
// my_feature_2 is not enabled
}
{% if isFeatureEnabled('my_feature_1') %}
{% include 'feature1_template.html.twig' %}
{% endif %}
@Florian_FB

Avec gitlab ou jira
-
Features et états dans Gitlab
-
Gitlab expose une API
-
Application consomme l'API
-
Le fonctionnement est "basique"
-
Bundle Symfony
@Florian_FB

Une solution maison
-
En fonction de l'utilisateur
-
En fonction du contexte
-
En fonction de la marque blanche
@Florian_FB

Un peu de contextE





John Doe
Admin
Facturation
Lecture seule
SymfonyLive
SymfonyCon
Forum PHP
Sponsor 1
Sponsor 2
WL Symfony
WL AFUP
@Florian_FB

Un peu de contexte











WL AFUP
WL Symfony
AFUP Day
SF Live
@Florian_FB

Et alors on fait comment ?
-
Utiliser des rĂ´les
-
Utiliser des permissions
@Florian_FB

La Solution !
-
Voters & attributs
-
1 action = 1 attribut unique
-
1 feature = N attributs
-
1 attribut = 1 feature
-
Migration Doctrine
-
CRUD
@Florian_FB

Le modèle de données
Attribut
list_document
add_document
...
Feature
document
product
...
Profile
admin
facturation
...



@Florian_FB

Permission Manager
public function whiteLabelHasAttribute(string $attribute): bool
{
// do check
}
@Florian_FB

Permission Manager
public function whiteLabelHasAttribute(string $attribute): bool
{
// do check
}
public function exhibitionHasAttribute(Exhibition $exhibition, string $attribute): bool
{
// do check
}
@Florian_FB

Permission Manager
public function whiteLabelHasAttribute(string $attribute): bool
{
// do check
}
public function exhibitionHasAttribute(Exhibition $exhibition, string $attribute): bool
{
// do check
}
public function userHasAttributeForExhibitor(Exhibitor $exhibitor, string $attribute): bool
{
// do check
}
@Florian_FB

Voter !
abstract class CustomVoter extends Voter
{
abstract protected function getExhibitor(mixed $subj): Exhibitor;
abstract protected function checkAttribute(
string $attribute,
mixed $subject,
TokenInterface $token
): bool;
final protected function voteOnAttribute(
string $attribute,
mixed $subject,
TokenInterface $token
): bool {
// some check here
}
}
@Florian_FB

final protected function voteOnAttribute(
string $attribute,
mixed $subject,
TokenInterface $token
): bool {
$exhibitor = $this->getExhibitor($subject);
$exhibition = $exhibitor->getExhibition()
if (!$this->whiteLabelHasAttribute($attribute)) {
// write log
return false;
}
if (!$this->exhibitionHasAttribute($exhibition, $attribute)) {
// write log
return false;
}
if (!$this->userAttributeForExhibitor($exhibitor, $attribute)) {
// write log
return false;
}
return $this->checkAttribute($attribute, $subject, $token);
}
@Florian_FB

Et dans l'interface ?
{% if user_has_attribute(exhibitor, 'add_product') %}
// todo
{% endif %}
-
Fonction twig
-
Reprends les check du voter
@Florian_FB

Et dans l'interface ?
$frontendAppContext = new FrontendAppContext();
$attributes = $this->featureFlipping->getAttributesForApp($app);
foreach ($attributes as $attribute) {
if (!$this->checker->userHasAttribute($attribute, ...)) {
continue;
}
$frontendAppContext->permissions[] = $attribute;
}
return $frontendAppContext;
-
API pour le VueJS
-
Expose les attributs accessibles pour l'app
@Florian_FB

Et les perfs ?
-
Requêtes SQL très simple
-
Cache Redis
-
Impact faible
@Florian_FB

Et les perfs ?
public function __construct(private readonly CacheInterface $cacheRedis) {}
private function whiteLabelHasAttribute(string $attribute): bool
{
return $this->cacheRedis->get(
$cacheKey,
function (ItemInterface $item) use ($attribute) {
$item->expiresAfter(self::CACHE_TTL);
// do check
return ...;
}
);
}
@Florian_FB

De la discipline ...
-
Template Jira
-
Recette
-
Template Gitlab
-
Architectural Decision Records
-
Et bien-sur des outils !
@Florian_FB

... et des outils
-
Règles PHPStan
-
Maker
-
Scripts de validation
-
CI
-
Tests
-
CRUD en backoffice
-
Dashboard Kibana
@Florian_FB

Merci !
@Florian_FB

[SF Live] Roles & Permissions
By Florian Bogey
[SF Live] Roles & Permissions
- 227