BONJOUR ! đź‘‹
@Florian_FB
@Florian_FB

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



@Florian_FB
Florian-B
GL events
@Florian_FB

Rôles & Permissions​
@Florian_FB
Comment développer une marque blanche avec du Feature Flipping
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 AcceptConferenceController
{
    #[Route('/admin/cfp/accept/{id}', name: 'admin_cfp_accept')]
    #[IsGranted('ROLE_SUPER_ADMIN')]
    public function __invoke(Conference $conference): 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
@Florian_FB








Attribute : edit_document
subject : document
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 AcceptConferenceController
{
    #[Route('/admin/cfp/accept/{id}', name: 'admin_cfp_accept')]
    #[IsGranted('ROLE_SUPER_ADMIN')]
    #[IsGranted('accept_cfp', 'conference')]
    public function __invoke(Conference $conference): Response
    {
        // do amazing stuff here
    }
}@Florian_FB
Role vs Permission
@Florian_FB






















Affirmative (défaut)
Consensus
Unanimous
Priority






prio : 2
prio : 1
prio : 0
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
 - 
	
Objectif pouvoir 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
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 afup
# Nginx
set $app_white_label afup;@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']);
};
@Florian_FB
Créons une MARQUE BLANCHE
# .env.wl.afup
APP_STYLE_ENTRY=afup
APP_LOGO=logo-afup.png
APP_FAVICON=favicon-afup.png
APP_MY_VAR=foo
...@Florian_FB
Créons une MARQUE BLANCHE
# config/services.yaml
App\Twig\WhiteLabelExtension:
  arguments:
    $appStyleEntry: '%env(string:APP_STYLE_ENTRY)%'
    $appLogo: '%env(string:APP_LOGO)%'
    $appFavicon: '%env(string:APP_FAVICON)%'
@Florian_FB
Créons une MARQUE BLANCHE
#  src/Twig/WhiteLabelExtension.php 
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);
    }
}@Florian_FB
Créons une MARQUE BLANCHE
feature flipping
aussi appelé Feature Flag
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: trueif ($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 %}
{% if isFeatureDisabled('my_feature_2') %}
    {% include 'feature2_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
AFUP Day
Forum PHP
Symfony Live
Sponsor 1
Sponsor 2
WL AFUP
WL Symfony
@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
/**
 * Computes if attribute is part of current Permission according to the WHITELABEL
 */
public function whiteLabelHasAttribute(string $attribute): bool
{
    // do check
}@Florian_FB
Permission Manager
/**
 * Computes if attribute is part of current Permission according to the WHITELABEL
 */
public function whiteLabelHasAttribute(string $attribute): bool
{
    // do check
}
/**
 * Computes if an attribute is part of current Permission according to the Exhibition
 */
public function exhibitionHasAttribute(Exhibition $exhibition, string $attribute): bool
{
    // do check
}@Florian_FB
Permission Manager
/**
 * Computes if attribute is part of current Permission according to the WHITELABEL
 */
public function whiteLabelHasAttribute(string $attribute): bool
{
    // do check
}
/**
 * Computes if an attribute is part of current Permission according to the Exhibition
 */
public function exhibitionHasAttribute(Exhibition $exhibition, string $attribute): bool
{
    // do check
}
/**
 * Compute if an attributes is part of current Permission according to User and Exhibitor
 */
public function userHasAttributeForExhibitor(Exhibitor $exhibitor, string $attribute): bool 
{
    // do check
}@Florian_FB
Voter !
abstract class CustomVoter extends Voter
{
    abstract protected function getExhibitor(mixed $subject): 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);
    if (!$this->whiteLabelHasAttribute($attribute)) {
        $this->logger->warning(...);
        return false;
    }
    if (!$this->exhibitionHasAttribute($exhibitor->getExhibition(), $attribute)) {
        $this->logger->warning(...);
        return false;
    }
    if (!$this->userAttributeForExhibitor($exhibitor, $attribute)) {
        $this->logger->warning(...);
        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 les perfs ?
public function __construct(private readonly CacheInterface $cacheRedis) {}
private function whiteLabelHasAttribute(Attribute $attribute): bool
{
    return $this->cacheRedis->get(
        $cacheKey,
        function (ItemInterface $item) use ($attribute) {
            $item->expiresAfter(self::CACHE_TTL);
            // do check                
            return ...;
        }
    );
}- 
	
Requêtes très simple
 - 
	
Redis
 - 
	
Impact faible
 
@Florian_FB
De la discipline ...
- 
	
Template Jira
 - 
	
Recette
 - 
	
Template Gitlab
 - 
	
Architectural Decision Records
 - 
	
Et bien-sur des outils !
 
@Florian_FB
... et des outils
- 
	
Règle PHPStan
 - 
	
Scripts de validation
 - 
	
Tests
 - 
	
CRUD en backoffice
 - 
	
Dashboard Kibana
 
@Florian_FB
Merci !
@Florian_FB

Pour me faire un retour
[AFUP Day] Roles & Permissions
By Florian Bogey
[AFUP Day] Roles & Permissions
- 205