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

@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