Introduction à Spring Security ACL

Introduction à Spring Security ACL

1. introduction

Access Control List (ACL) est une liste d'autorisations attachées à un objet. UnACL spécifie quelles identités sont accordées quelles opérations sur un objet donné.

Spring Security _Access Control List_ esta Spring component which supports Domain Object Security. En termes simples, Spring ACL aide à définir les autorisations pour un utilisateur / rôle spécifique sur un seul objet de domaine - au lieu de le faire à tous les niveaux, au niveau typique par opération.

Par exemple, un utilisateur avec le rôleAdmin peut voir (READ) et éditer (WRITE) tous les messages sur unCentral Notice Box, mais l'utilisateur normal ne peut voir que les messages, se rapporter à et ne peut pas les modifier. Pendant ce temps, d'autres utilisateurs avec le rôleEditor peuvent voir et modifier certains messages spécifiques.

Par conséquent, différents utilisateurs / rôles ont des autorisations différentes pour chaque objet spécifique. Dans ce cas,Spring ACL est capable d'accomplir la tâche. Nous allons découvrir comment configurer la vérification des autorisations de base avecSpring ACL dans cet article.

2. Configuration

2.1. Base de données ACL

Pour utiliserSpring Security ACL, nous devons créer quatre tables obligatoires dans notre base de données.

La première table estACL_CLASS, qui stocke le nom de classe de l'objet de domaine, les colonnes incluent:

  • ID

  • CLASS: le nom de classe des objets de domaine sécurisé, par exemple:org.example.acl.persistence.entity.NoticeMessage

Deuxièmement, nous avons besoin de la tableACL_SID qui nous permet d'identifier universellement tout principe ou autorité dans le système. La table a besoin de:

  • ID

  • SID: qui est le nom d'utilisateur ou le nom du rôle. SID signifieSecurity Identity

  • PRINCIPAL:0 ou1, pour indiquer que leSID correspondant est un mandant (utilisateur, tel quemary, mike, jack…) ou une autorité (rôle, tel queROLE_ADMIN, ROLE_USER, ROLE_EDITOR…)

La table suivante estACL_OBJECT_IDENTITY, qui stocke les informations pour chaque objet de domaine unique:

  • ID

  • OBJECT_ID_CLASS: définit la classe d'objets du domaine, table_ links to _ACL_CLASS

  • Les objets du domaineOBJECT_ID_IDENTITY: peuvent être stockés dans de nombreuses tables en fonction de la classe. Par conséquent, ce champ stocke la clé primaire de l'objet cible

  • PARENT_OBJECT: spécifie le parent de ceObject Identity dans cette table

  • OWNER_SID:ID du propriétaire de l'objet, liens vers la tableACL_SID

  • ENTRIES_INHERITTING: siACL Entries de cet objet hérite de l'objet parent (ACL Entries sont définis dans la tableACL_ENTRY)

Enfin, l'autorisation individuelle de stockageACL_ENTRY est attribuée à chaqueSID sur unObject Identity:

  • ID

  • ACL_OBJECT_IDENTITY: spécifie l'identité de l'objet, les liens vers la tableACL_OBJECT_IDENTITY

  • ACE_ORDER: l'ordre de l'entrée courante dans la listeACL entries desObject Identity correspondants

  • SID: la cibleSID pour laquelle l'autorisation est accordée ou refusée, est liée à la tableACL_SID

  • MASK: le masque de bits entier qui représente l'autorisation réelle accordée ou refusée

  • GRANTING: valeur 1 signifie octroi, valeur0 signifie refus

  • AUDIT_SUCCESS etAUDIT_FAILURE: à des fins d'audit

2.2. Dépendance

Pour pouvoir utiliserSpring ACL dans notre projet, définissons d'abord nos dépendances:


    org.springframework.security
    spring-security-acl


    org.springframework.security
    spring-security-config


    org.springframework
    spring-context-support


    net.sf.ehcache
    ehcache-core
    2.6.11

Spring ACL nécessite un cache pour stockerObject Identity etACL entries, nous allons donc utiliserEhcache ici. Et, pour prendre en chargeEhcache dansSpring,, nous avons également besoin desspring-context-support.

Lorsque vous ne travaillez pas avec Spring Boot, nous devons ajouter des versions explicitement. Ceux-ci peuvent être vérifiés sur Maven Central:spring-security-acl,spring-security-config,spring-context-support,ehcache-core.

Nous devons sécuriser toutes les méthodes qui retournent des objets de domaine sécurisés, ou apportent des modifications à l'objet, en activantGlobal Method Security:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclMethodSecurityConfiguration
  extends GlobalMethodSecurityConfiguration {

    @Autowired
    MethodSecurityExpressionHandler
      defaultMethodSecurityExpressionHandler;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return defaultMethodSecurityExpressionHandler;
    }
}

Activons égalementExpression-Based Access Control en définissantprePostEnabled surtrue pour utiliserSpring Expression Language (SpEL). De plus, nous avons besoin d'un gestionnaire d'expressions avec le support deACL:

@Bean
public MethodSecurityExpressionHandler
  defaultMethodSecurityExpressionHandler() {
    DefaultMethodSecurityExpressionHandler expressionHandler
      = new DefaultMethodSecurityExpressionHandler();
    AclPermissionEvaluator permissionEvaluator
      = new AclPermissionEvaluator(aclService());
    expressionHandler.setPermissionEvaluator(permissionEvaluator);
    return expressionHandler;
}

Par conséquent,, nous affectonsAclPermissionEvaluator auxDefaultMethodSecurityExpressionHandler. L'évaluateur a besoin d'unMutableAclService pour charger les paramètres d'autorisation et les définitions d'objet de domaine à partir de la base de données.

Pour plus de simplicité, nous utilisons lesJdbcMutableAclService fournis:

@Bean
public JdbcMutableAclService aclService() {
    return new JdbcMutableAclService(
      dataSource, lookupStrategy(), aclCache());
}

Comme son nom, leJdbcMutableAclService utiliseJDBCTemplate pour simplifier l'accès à la base de données. Il a besoin d'unDataSource (forJDBCTemplate),LookupStrategy (fournit une recherche optimisée lors de l'interrogation de la base de données), et d'unAclCache (cachingACLEntries etObject Identity).

Encore une fois, pour plus de simplicité, nous utilisons lesBasicLookupStrategy etEhCacheBasedAclCache fournis.

@Autowired
DataSource dataSource;

@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
    return new AclAuthorizationStrategyImpl(
      new SimpleGrantedAuthority("ROLE_ADMIN"));
}

@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
    return new DefaultPermissionGrantingStrategy(
      new ConsoleAuditLogger());
}

@Bean
public EhCacheBasedAclCache aclCache() {
    return new EhCacheBasedAclCache(
      aclEhCacheFactoryBean().getObject(),
      permissionGrantingStrategy(),
      aclAuthorizationStrategy()
    );
}

@Bean
public EhCacheFactoryBean aclEhCacheFactoryBean() {
    EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
    ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
    ehCacheFactoryBean.setCacheName("aclCache");
    return ehCacheFactoryBean;
}

@Bean
public EhCacheManagerFactoryBean aclCacheManager() {
    return new EhCacheManagerFactoryBean();
}

@Bean
public LookupStrategy lookupStrategy() {
    return new BasicLookupStrategy(
      dataSource,
      aclCache(),
      aclAuthorizationStrategy(),
      new ConsoleAuditLogger()
    );
}

Ici, leAclAuthorizationStrategy est chargé de déterminer si un utilisateur actuel possède ou non toutes les autorisations requises sur certains objets.

Il a besoin du support dePermissionGrantingStrategy, qui définit la logique pour déterminer si une autorisation est accordée à unSID particulier.

3. Sécurité des méthodes avec Spring ACL

Jusqu'à présent, nous avons effectué toutes les configurations nécessaires. Maintenant, nous pouvons mettre la règle de vérification requise sur nos méthodes sécurisées.

Par défaut,Spring ACL fait référence à la classeBasePermission pour toutes les autorisations disponibles. Fondamentalement, nous avons une autorisationREAD, WRITE, CREATE, DELETE etADMINISTRATION.

Essayons de définir quelques règles de sécurité:

@PostFilter("hasPermission(filterObject, 'READ')")
List findAll();

@PostAuthorize("hasPermission(returnObject, 'READ')")
NoticeMessage findById(Integer id);

@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
NoticeMessage save(@Param("noticeMessage")NoticeMessage noticeMessage);

Après l'exécution de la méthodefindAll(),@PostFilter sera déclenché. La règle requisehasPermission(filterObject, ‘READ'), signifie ne renvoyer que lesNoticeMessage sur lesquels l'utilisateur actuel a l'autorisationREAD.

De même,@PostAuthorize est déclenché après l'exécution de la méthodefindById(), assurez-vous de ne renvoyer l'objetNoticeMessage que si l'utilisateur actuel a l'autorisationREAD dessus. Sinon, le système lancera unAccessDeniedException.

De l'autre côté, le système déclenche l'annotation@PreAuthorize avant d'appeler la méthodesave(). Il décidera où la méthode correspondante est autorisée à s'exécuter ou non. Sinon,AccessDeniedException sera renvoyé.

4. En action

Nous allons maintenant tester toutes ces configurations en utilisantJUnit. Nous utiliserons la base de données deH2pour garder la configuration aussi simple que possible.

Nous devrons ajouter:


  com.h2database
  h2



  org.springframework
  spring-test
  test



  org.springframework.security
  spring-security-test
  test

4.1. Le scénario

Dans ce scénario, nous aurons deux utilisateurs (manager, hr) et un rôle utilisateur (ROLE_EDITOR),, donc nosacl_sid seront:

INSERT INTO acl_sid (id, principal, sid) VALUES
  (1, 1, 'manager'),
  (2, 1, 'hr'),
  (3, 0, 'ROLE_EDITOR');

Ensuite, nous devons déclarer la classeNoticeMessage dansacl_class. Et trois instances de la classeNoticeMessage seront insérées danssystem_message.

De plus, les enregistrements correspondants pour ces 3 instances doivent être déclarés enacl_object_identity:

INSERT INTO acl_class (id, class) VALUES
  (1, 'org.example.acl.persistence.entity.NoticeMessage');

INSERT INTO system_message(id,content) VALUES
  (1,'First Level Message'),
  (2,'Second Level Message'),
  (3,'Third Level Message');

INSERT INTO acl_object_identity
  (id, object_id_class, object_id_identity,
  parent_object, owner_sid, entries_inheriting)
  VALUES
  (1, 1, 1, NULL, 3, 0),
  (2, 1, 2, NULL, 3, 0),
  (3, 1, 3, NULL, 3, 0);

Dans un premier temps, nous accordons les autorisationsREAD etWRITE sur le premier objet (id =1) à l'utilisateurmanager. Pendant ce temps, tout utilisateur avecROLE_EDITOR aura l'autorisationREAD sur les trois objets mais ne possédera que l'autorisationWRITE sur le troisième objet (id=3). De plus, l'utilisateurhr n'aura que l'autorisationREAD sur le deuxième objet.

Ici, comme nous utilisons la classe par défautSpring ACLBasePermission pour la vérification des autorisations, la valeur de masque de l'autorisationREAD sera 1 et la valeur de masque de l'autorisationWRITE sera 2 . Nos données enacl_entry seront:

INSERT INTO acl_entry
  (id, acl_object_identity, ace_order,
  sid, mask, granting, audit_success, audit_failure)
  VALUES
  (1, 1, 1, 1, 1, 1, 1, 1),
  (2, 1, 2, 1, 2, 1, 1, 1),
  (3, 1, 3, 3, 1, 1, 1, 1),
  (4, 2, 1, 2, 1, 1, 1, 1),
  (5, 2, 2, 3, 1, 1, 1, 1),
  (6, 3, 1, 3, 1, 1, 1, 1),
  (7, 3, 2, 3, 2, 1, 1, 1);

4.2. Cas de test

Tout d'abord, nous essayons d'appeler la méthodefindAll.

Comme notre configuration, la méthode retourne uniquement lesNoticeMessage sur lesquels l'utilisateur a l'autorisationREAD.

Par conséquent, nous nous attendons à ce que la liste de résultats ne contienne que le premier message:

@Test
@WithMockUser(username = "manager")
public void
  givenUserManager_whenFindAllMessage_thenReturnFirstMessage(){
    List details = repo.findAll();

    assertNotNull(details);
    assertEquals(1,details.size());
    assertEquals(FIRST_MESSAGE_ID,details.get(0).getId());
}

Ensuite, nous essayons d'appeler la même méthode avec n'importe quel utilisateur qui a le rôle -ROLE_EDITOR. Notez que, dans ce cas, ces utilisateurs ont l'autorisationREAD sur les trois objets.

Par conséquent, nous nous attendons à ce que la liste de résultats contienne les trois messages:

@Test
@WithMockUser(roles = {"EDITOR"})
public void
  givenRoleEditor_whenFindAllMessage_thenReturn3Message(){
    List details = repo.findAll();

    assertNotNull(details);
    assertEquals(3,details.size());
}

Ensuite, en utilisant l'utilisateurmanager, nous essaierons d'obtenir le premier message par identifiant et de mettre à jour son contenu - ce qui devrait bien fonctionner:

@Test
@WithMockUser(username = "manager")
public void
  givenUserManager_whenFind1stMessageByIdAndUpdateItsContent_thenOK(){
    NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID);
    assertNotNull(firstMessage);
    assertEquals(FIRST_MESSAGE_ID,firstMessage.getId());

    firstMessage.setContent(EDITTED_CONTENT);
    repo.save(firstMessage);

    NoticeMessage editedFirstMessage = repo.findById(FIRST_MESSAGE_ID);

    assertNotNull(editedFirstMessage);
    assertEquals(FIRST_MESSAGE_ID,editedFirstMessage.getId());
    assertEquals(EDITTED_CONTENT,editedFirstMessage.getContent());
}

Mais si un utilisateur avec le rôleROLE_EDITOR met à jour le contenu du premier message - notre système lancera unAccessDeniedException:

@Test(expected = AccessDeniedException.class)
@WithMockUser(roles = {"EDITOR"})
public void
  givenRoleEditor_whenFind1stMessageByIdAndUpdateContent_thenFail(){
    NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID);

    assertNotNull(firstMessage);
    assertEquals(FIRST_MESSAGE_ID,firstMessage.getId());

    firstMessage.setContent(EDITTED_CONTENT);
    repo.save(firstMessage);
}

De même, l'utilisateur dehr peut trouver le deuxième message par id, mais ne parviendra pas à le mettre à jour:

@Test
@WithMockUser(username = "hr")
public void givenUsernameHr_whenFindMessageById2_thenOK(){
    NoticeMessage secondMessage = repo.findById(SECOND_MESSAGE_ID);
    assertNotNull(secondMessage);
    assertEquals(SECOND_MESSAGE_ID,secondMessage.getId());
}

@Test(expected = AccessDeniedException.class)
@WithMockUser(username = "hr")
public void givenUsernameHr_whenUpdateMessageWithId2_thenFail(){
    NoticeMessage secondMessage = new NoticeMessage();
    secondMessage.setId(SECOND_MESSAGE_ID);
    secondMessage.setContent(EDITTED_CONTENT);
    repo.save(secondMessage);
}

5. Conclusion

Nous avons passé en revue la configuration de base et l'utilisation deSpring ACL dans cet article.

Comme nous le savons,Spring ACL nécessitait des tables spécifiques pour gérer les paramètres d'objet, de principe / d'autorité et d'autorisation. Toutes les interactions avec ces tables, en particulier l'action de mise à jour, doivent passer parAclService. Nous explorerons ce service pour les actions de base deCRUD dans un prochain article.

Par défaut, nous sommes limités à l'autorisation prédéfinie dans la classeBasePermission.

Enfin, l'implémentation de ce tutoriel peut être trouvéeover on Github.