Architecture du module de restriction
L'implémentation d'une fonctionnalité de bannissement au sein d'un framework d'authentification nécessite une approche modulaire. L'idée centrale repose sur l'utilisation d'un adaptateur de stockage (Key-Value) pour persister les états de restriction. La clé est construite à partir de l'identifiant de l'utilisateur et du service concerné, tandis que la valeur représente le niveau de sévérité du bannissement.
Cette structure permet de gérer finement les accès : un utilisateur peut être banni d'un service spécifique (ex: "commentaires") avec un certain niveau de restriction, tout en conservant l'accès à d'autres fonctionnalités.
Définition de la méthode de bannissement
La fonction principale doit valider les paramètres d'entrée, notammment s'assurer que le niveau de restriction est positif. Une fois les validations effectuées, l'état est persisté avec une durée de vie (TTL) définie. Il est également recommandé de notifier les composants tiers via un système d'observation (Watcher) ou de journalisation (Logger).
// RestrictUser applique une sanction à un compte utilisateur
func (m *SecurityManager) RestrictUser(userID string, scope string, severity int, duration int64) error {
if userID == "" || scope == "" {
return fmt.Errorf("identifiant utilisateur ou scope invalide")
}
if severity < 1 {
return fmt.Errorf("le niveau de sévérité (%d) doit être supérieur ou égal à 1", severity)
}
storageKey := m.formatRestrictionKey(userID, scope)
valStr := strconv.Itoa(severity)
err := m.store.Set(storageKey, valStr, duration)
if err != nil {
return err
}
// Notification des interfaces de monitoring
m.log.Info("Utilisateur restreint", "id", userID, "scope", scope, "niveau", severity)
if m.observer != nil {
m.observer.OnBan(m.contextType, userID, scope, severity, duration)
}
return nil
}
Levée des sanctions
Pour réactiver l'accès d'un utilisateur, il suffit de supprimer l'entrée correspondante dans le système de stockage. Cette méthode accepte une liste variable de services pour permettre une réactivation groupée.
// LiftRestrictions supprime les limitations sur un ou plusieurs services
func (m *SecurityManager) LiftRestrictions(userID string, scopes ...string) error {
if userID == "" {
return errors.New("l'ID utilisateur ne peut pas être vide")
}
if len(scopes) == 0 {
return errors.New("aucun scope spécifié")
}
for _, scope := range scopes {
key := m.formatRestrictionKey(userID, scope)
if err := m.store.Delete(key); err != nil {
return err
}
m.log.Info("Restriction levée", "id", userID, "scope", scope)
if m.observer != nil {
m.observer.OnUnban(m.contextType, userID, scope)
}
}
return nil
}
Consultation de l'état de bannissement
Trois fonctions utilitaires sont essentielles pour intégrer ce système dans la logique métier : la vérification de présence, la récupération du niveau de sévérité et l'obtention du temps restent avant expiration.
Vérification du statut
// HasRestriction vérifie si l'utilisateur est actuellement banni d'un service
func (m *SecurityManager) HasRestriction(userID string, scope string) bool {
res := m.store.Get(m.formatRestrictionKey(userID, scope))
return res != ""
}
Récupération de la sévérité
// GetRestrictionLevel retourne le degré de la sanction appliquée
func (m *SecurityManager) GetRestrictionLevel(userID string, scope string) (int, error) {
rawVal := m.store.Get(m.formatRestrictionKey(userID, scope))
if rawVal == "" {
return 0, fmt.Errorf("aucune restriction pour l'utilisateur %s sur %s", userID, scope)
}
lvl, err := strconv.Atoi(rawVal)
if err != nil {
return 0, err
}
return lvl, nil
}
Temps restant (TTL)
// GetRemainingTime retourne la durée restante de la sanction en secondes
func (m *SecurityManager) GetRemainingTime(userID string, scope string) int64 {
return m.store.GetTTL(m.formatRestrictionKey(userID, scope))
}
Exemple d'utilisation métier
Dans un scénario complexe comme un forum, on peut définir des paliers : le niveau 1 interdit l'écriture, le niveau 2 interdit l'écriture et les réactions (likes), tandis que le niveau 3 suspend totalement l'accès au module de lecture. L'intégration se fait simplement en interrogeant GetRestrictionLevel avant chaque action sensible.
func TestRestrictionFlow(t *testing.T) {
manager := NewSecurityManager()
// Appliquer un bannissement de 2 minutes pour le module "social"
_ = manager.RestrictUser("user_123", "social", 2, 120)
if !manager.HasRestriction("user_123", "social") {
t.Error("L'utilisateur devrait être restreint")
}
// Nettoyage
_ = manager.LiftRestrictions("user_123", "social")
if manager.HasRestriction("user_123", "social") {
t.Error("L'utilisateur ne devrait plus être restreint")
}
}