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: 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 %}

{% if isFeatureDisabled('my_feature_2') %}
    {% include 'feature2_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

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

  • 189