SeamFramework.orgCommunity Documentation

Capitolo 15. Sicurezza

15.1. Panoramica
15.2. Disabilitare la sicurezza
15.3. Autenticazione
15.3.1. Configurare un componente Authenticator
15.3.2. Scrivere un metodo di autenticazione
15.3.3. Scrivere una form di accesso
15.3.4. Riepilogo della configurazione
15.3.5. Ricordami su questo computer
15.3.6. Gestire le eccezioni della sicurezza
15.3.7. Redirezione alla pagina di accesso
15.3.8. Autenticazione HTTP
15.3.9. Caratteristiche di autenticazione avanzate
15.4. Gestione delle identità
15.4.1. Configurare l'IdentityManager
15.4.2. JpaIdentityStore
15.4.3. LdapIdentityStore
15.4.4. Scrivere il proprio IdentityStore
15.4.5. L'autenticazione con la gestione delle identità
15.4.6. Usare IdentityManager
15.5. Messaggi di errore
15.6. Autorizzazione
15.6.1. Concetti principali
15.6.2. Rendere sicuri i componenti
15.6.3. La sicurezza nell'interfaccia utente
15.6.4. Rendere sicure le pagine
15.6.5. Rendere sicure le entità
15.6.6. Annotazioni tipizzate per i permessi
15.6.7. Annotazioni tipizzate per i ruoli
15.6.8. Il modello di autorizzazione dei permessi
15.6.9. RuleBasedPermissionResolver
15.6.10. PersistentPermissionResolver
15.7. Gestione dei permessi
15.7.1. PermissionManager
15.7.2. Verifica dei permessi sulle operazioni di PermissionManager
15.8. Sicurezza SSL
15.8.1. Modificare le porte di default
15.9. CAPTCHA
15.9.1. Configurare la servlet CAPTCHA
15.9.2. Aggiungere un CAPTCHA ad una form
15.9.3. Personalizzare l'algoritmo CAPTCHA
15.10. Eventi della sicurezza
15.11. Run As
15.12. Estendere il componente Identity
15.13. OpenID
15.13.1. Configurare OpenID
15.13.2. Persentare una form di login OpenID
15.13.3. Eseguire il login immediatamente
15.13.4. Rimandare il login
15.13.5. Log out

Le API della sicurezza di Seam forniscono una serie di caratteristiche relative alla sicurezza di un'applicazione basata su Seam, coprendo le seguenti aree:

Queste capitolo si occuperà in dettaglio di ciascuna di queste caratteristiche.

In determinate situazioni può essere necessario disabilitare la gestione della sicurezza in Seam, ad esempio durante i test oppure perché si sta usando un diverso approccio alla sicurezza, come l'uso diretto di JAAS. Per disabilitare l'infrastruttura della sicurezza chiamare semplicemente il metodo statico Identity.setSecurityEnabled(false). Ovviamente non è molto pratico dover chiamare un metodo statico quando si vuole configurare un'applicazione, perciò in alternativa è possibile controllare questa impostazione in components.xml:

Assumendo che si stia pianificando di sfruttare i vantaggi che la sicurezza Seam ha da offrire, il resto di questo capitolo documenta l'insieme delle opzioni disponibili per dare agli utenti un'identità dal punto di vista del modello di sicurezza (autenticazione) e un accesso limitato all'applicazione secondo dei vincoli stabiliti (autorizzazione). Iniziamo con la questione dell'autenticazione poiché è il fondamento di ogni modello di sicurezza.

Le caratteristiche relative all'autenticazione nella gestione della sicurezza di Seam sono costruite su JAAS (Java Authentication and Authorization Service, servizio di autenticazione e autorizzazione Java) e, come tali, forniscono una API robusta e altamente configurabile per gestire l'autenticazione degli utenti. Comunque, per requisiti di autenticazione meno complessi, Seam offre un metodo di autenticazione molto semplificato che nasconde la complessità di JAAS.

Il metodo di autenticazione semplificato fornito da Seam usa un modulo di login JAAS già fatto, SeamLoginModule, il quale delega l'autenticazione ad uno dei componenti dell'applicazione. Questo modulo di login è già configurato all'interno di Seam come parte dei criteri di gestione di default e in quanto tale non richiede alcun file di configurazione aggiuntivo. Esso consente di scrivere un metodo di autenticazione usando le classi entità che sono fornite dall'applicazione o, in alternativa, di esegure l'autenticazione con qualche altro fornitore di terze parti. Per configurare questa forma semplificata di autenticazione è richiesto di configurare il componente Identity in components.xml:


<components xmlns="http://jboss.com/products/seam/components"
            xmlns:core="http://jboss.com/products/seam/core"
            xmlns:security="http://jboss.com/products/seam/security"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation=
                "http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.2.xsd
                 http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.2.xsd">

    <security:identity authenticate-method="#{authenticator.authenticate}"/>

</components
>

L'espressione EL #{authenticator.authenticate} è la definizione di un metodo tramite la quale si indica che il metodo authenticate del componente authenticator verrà usato per autenticare l'utente.

La proprietà authenticate-method specificata per identity in components.xml specifica quale metodo sarà usato dal SeamLoginModule per autenticare l'utente. Questo metodo non ha parametri ed è previsto che restituisca un boolean, il quale indica se l'autenticazione ha avuto successo o no. Il nome utente e la password possono essere ottenuti da Credentials.getUsername() e Credentials.getPassword() rispettivamente (è possibile avere un riferimento al componente credentials tramite Identity.instance().getCredentials()). Tutti i ruoli di cui l'utente è membro devono essere assegnati usando Identity.addRole(). Ecco un esempio completo di un metodo di autenticazione all'interno di un componente POJO:

@Name("authenticator")

public class Authenticator {
   @In EntityManager entityManager;
   @In Credentials credentials;
   @In Identity identity;
   public boolean authenticate() {
      try {
         User user = (User) entityManager.createQuery(
            "from User where username = :username and password = :password")
            .setParameter("username", credentials.getUsername())
            .setParameter("password", credentials.getPassword())
            .getSingleResult();
         if (user.getRoles() != null) {
            for (UserRole mr : user.getRoles())
               identity.addRole(mr.getName());
         }
         return true;
      }
      catch (NoResultException ex) {
         return false;
      }
   }
}

Nell'esempio precedente sia User che UserRole sono entity bean specifici dell'applicazione. Il parametro roles è popolato con i ruoli di cui l'utente è membro, che devono essere aggiunti alla Set come valori stringa, ad esempio "amministratore", "utente". In questo caso, se il record dell'utente non viene trovato e una NoResultException viene lanciata, il metodo di autenticazione restituisce false per indicare che l'autenticazione è fallita.

La sicurezza di Seam gestisce lo stesso tipo di funzionalità "Ricordami su questo computer" che si incontra comunemente in molte applicazioni basate sull'interfaccia web. In effetti essa è gestita in due diverse "varietà" o modalità. La prima modalità consente al nome utente di essere memorizzato nel browser dell'utente come un cookie e lascia che sia il browser ad inserire la password (molti browser moderni sono in grado di ricordare le password).

La seconda modalità gestisce la memorizzazione di un identificativo unico in un cookie e consente all'utente di autenticarsi automaticamente non appena ritorna sul sito, senza dover fornire una password.

Avvertimento

L'autenticazione automatica tramite un cookie persistente memorizzato sulla macchina client è pericolosa. Benché sia conveniente per gli utenti, qualsiasi debolezza nella sicurezza che consenta un cross-site scripting nel sito avrebbe effetti drammaticamente più gravi del solito. Senza il cookie di autenticazione, il solo cookie che un malintenzionato può prelevare tramite un attacco XSS è il cookie della sessione corrente dell'utente. Ciò significa che l'attacco funziona solo quando l'utente ha una sessione aperta, ovvero per un intervallo di tempo limitato. Al contrario è molto più allettante e pericoloso se un malintenzionato ha la possibilità di prelevare il cookie relativo alla funzione "Ricordami su questo computer", il quale gli consentirebbe di accedere senza autenticazione ogni volta che vuole. Notare che questo dipende anche da quanto è efficace la protezione del sito dagli attacchi XSS. Sta a chi scrive l'applicazione fare in modo che il sito sia sicuro al 100% dagli attacchi XSS, un obiettivo non banale per qualsiasi sito che consente di rappresentare sulle pagine un contenuto scritto dagli utenti.

I produttori di browser hanno riconosciuto questo problema e hanno introdotto la funzione "Ricorda la password", oggi disponibile su quasi tutti i browser. In questo caso il browser ricorda il nome utente e la password per un certo sito e dominio, e riempie la form di accesso automaticamente quando non è attiva una sessione con il sito. Se poi il progettista del sito offre una scorciatoia da tastiera conveniente, questo approccio è quasi altrettanto immediato come il cookie "Ricordami su questo computer", ma molto più sicuro. Alcuni browser (ad esempio Safari su OS X) memorizzano addirittura i dati delle form di accesso nel portachiavi cifrato di sistema. Oppure, in un ambiente di rete, il portachiavi può essere trasportato dall'utente (tra il portatile e il desktop, ad esempio), mentre i cookie del browser di solito non sono sincronizzati.

In definitiva: benché tutti lo stiano facendo, il cookie "Ricordami su questo computer" con l'autenticazione automatica è una cattiva pratica e non dovrebbe essere usata. I cookie che "ricordano" solo il nome dell'utente e riempiono la form di accesso con quel nome utente per praticità, non comportano rischi.

Per abilitare la funzione "Ricordami su questo computer" nella modalità di default (quella sicura, con il solo nome utente) non è richiesta alcuna speciale configurazione. Basta collegare un checkbox "Ricordami su questo computer" a rememberMe.enabled nella form di accesso, come nel seguente esempio:


  <div>
    <h:outputLabel for="name" value="Nome utente"/>
    <h:inputText id="name" value="#{credentials.username}"/>
  </div>
  
  <div>
    <h:outputLabel for="password" value="Password"/>
    <h:inputSecret id="password" value="#{credentials.password}" redisplay="true"/>
  </div
>      
  
  <div class="loginRow">
    <h:outputLabel for="rememberMe" value="Ricordami su questo computer"/>
    <h:selectBooleanCheckbox id="rememberMe" value="#{rememberMe.enabled}"/>
  </div
>

Per usare la modalità automatica, attraverso il token, della funzione "Ricordami su questo computer", occorre prima configurare la memorizzazione del token. Nello scenario più comune (gestito da Seam) questi token di autenticazione vengono memorizzati nel database, comunque è possibile implementare la propria memorizzazione dei token implementando l'interfaccia org.jboss.seam.security.TokenStore. In questo paragrafo si suppone che per la memorizzazione dei token in una tabella del database si stia usando l'implementazione fornita con Seam JpaTokenStore.

Il primo passo consiste nel creare una nuova entità che conterrà i token. Il seguente esempio mostra una possibile struttura che può essere usata:

@Entity

public class AuthenticationToken implements Serializable {  
   private Integer tokenId;
   private String username;
   private String value;
   
   @Id @GeneratedValue
   public Integer getTokenId() {
      return tokenId;
   }
   
   public void setTokenId(Integer tokenId) {
      this.tokenId = tokenId;
   }
   
   @TokenUsername
   public String getUsername() {
      return username;
   }
   
   public void setUsername(String username) {
      this.username = username;
   }
   
   @TokenValue
   public String getValue() {
      return value;
   }
   
   public void setValue(String value) {
      this.value = value;
   }
}

Come si può vedere dal listato, vengono usate un paio di annotazioni speciali, @TokenUsername e @TokenValue, per configurare le proprietà token e nome utente dell'entità. Queste annotazioni sono richieste per l'entità che conterrà i token di autenticazione.

Il passo successivo consiste nel configurare il JpaTokenStore per usare questo entity bean per memorizzare e recuperare i token di autenticazione. Ciò viene fatto in components.xml specificando l'attributo token-class.



  <security:jpa-token-store token-class="org.jboss.seam.example.seamspace.AuthenticationToken"/>        
        

Una volta fatto questo, l'ultima cosa da fare è configurare anche il componente RememberMe in components.xml. La sua proprietà mode dovrà essere impostata a autoLogin:


  <security:remember-me mode="autoLogin"/>        
        

Questo è tutto ciò che è necessario. L'autenticazione automatica degli utenti avverrà quando torneranno a visitare il sito (purché abbiano impostato il checkbox "Ricordami su questo computer").

Per essere sicuri che gli utenti siano autenticati automaticamente quando tornano sul sito, il seguente codice deve essere posizionato in components.xml:


  <event type="org.jboss.seam.security.notLoggedIn">
    <action execute="#{redirect.captureCurrentView}"/>
    <action execute="#{identity.tryLogin()}"/>
  </event>
  <event type="org.jboss.seam.security.loginSuccessful">
    <action execute="#{redirect.returnToCapturedView}"/>
  </event
>

Per prevenire il fatto che gli utenti ricevano la pagina di errore di default in risposta ad un errore di sicurezza, si raccomanda che in pages.xml sia configurata una redirezione degli errori di sicurezza ad una pagina più "carina". I due principali tipi di eccezione lanciati dalle API della sicurezza sono:

Nel caso della NotLoggedInException, si raccomanda che l'utente venga rediretto o sulla pagina di accesso o su quella di registrazione, così che possa accedere. Per una AuthorizationException, può essere utile redirigere l'utente su una pagina di errore. Ecco un esempio di pages.xml che redirige entrambe queste eccezioni:


<pages>

    ...

    <exception class="org.jboss.seam.security.NotLoggedInException">
        <redirect view-id="/login.xhtml">
            <message
>Per eseguire questa operazione devi prima eseguire l'accesso</message>
        </redirect>
    </exception>

    <exception class="org.jboss.seam.security.AuthorizationException">
        <end-conversation/>
        <redirect view-id="/security_error.xhtml">
            <message
>Non disponi dei privilegi di sicurezza necessari per eseguire questa operazione.</message>
        </redirect>
    </exception>

</pages
>

La maggior parte delle applicazioni web richiede una gestione più sofisticata della redirezione sulla pagina di accesso, perciò Seam include alcune funzionalità speciali per gestire questo problema.

E' possibile chiedere a Seam di redirigere l'utente su una pagina di accesso quando un utente non autenticato tenta di accedere ad una particolare view (o ad una view il cui id corrisponda ad una wildcard), nel modo seguente:


<pages login-view-id="/login.xhtml">

    <page view-id="/members/*" login-required="true"/>

    ...

</pages
>

Dopo che l'utente ha eseguito l'accesso, lo si vorrà rimandare automaticamente indietro da dove è venuto, così che potrà riprovare ad eseguire l'azione che richiedeva l'accesso. Se si aggiungono i seguenti listener in components.xml, i tentativi di accesso ad una view protetta eseguiti quando non si è fatto l'accesso verranno ricordati così, dopo che l'utente ha eseguito l'accesso, può essere rediretto alla view che aveva originariamente richiesto, compresi tutti i parametri di pagina che esistevano nella richiesta originale.


<event type="org.jboss.seam.security.notLoggedIn">
    <action execute="#{redirect.captureCurrentView}"/>
</event>

<event type="org.jboss.seam.security.postAuthenticate">
    <action execute="#{redirect.returnToCapturedView}"/>
</event
>

Notare che la redirezione dopo l'accesso è implementata con un meccanismo con visibilità sulla conversazione, perciò occorre evitare di terminare la conversazione nel metodo authenticate().

Benché l'uso non sia raccomandato a meno che non sia assolutamente necessario, Seam fornisce gli strumenti per l'autenticazione in HTTP sia con metodo Basic che Digest (RFC 2617). Per usare entrambe le forme di autenticazione, occorre abilitare il componente authentication-filter in components.xml:



  <web:authentication-filter url-pattern="*.seam" auth-type="basic"/>
      

Per abilitare il filtro per l'autenticazione Basic impostare auth-type a basic, oppure per l'autenticazione Digest, impostarlo a digest. Se si usa l'autenticazione Digest, occorre impostare anche un valore per key e realm:



  <web:authentication-filter url-pattern="*.seam" auth-type="digest" key="AA3JK34aSDlkj" realm="La mia Applicazione"/>
      

key può essere un qualunque valore stringa. realm è il nome del dominio di autenticazione che viene presentato all'utente quando si autentica.

La gestione delle identità fornisce un'API standard per la gestione degli utenti e dei ruoli di una applicazione Seam, a prescindere da quale dispositivo di memorizzazione delle identità è usato internamente (database, LDAP, ecc). Al centro delle API per la gestione delle identità c'è il componente identityManager, il quale fornisce tutti i metodi per creare, modificare e cancellare utenti, concedere e revocare ruoli, cambiare le password, abilitare e disabilitare gli utenti, autenticare gli utenti ed elencare utenti e ruoli.

Prima di essere usato, identityManager deve essere configurato con uno o più IdentityStore. Questi componenti fanno il vero lavoro di interagire con il fornitore di sicurezza sottostante, sia che si tratti di un database, di un server LDAP o di qualcos'altro.

Il componente identityManager consente di separare i dispositivi di memorizzazione configurati per le operazioni di autenticazione e di autorizzazione. Ciò significa che è possibile autenticare gli utenti tramite un dispositivo di memorizzazione, ad esempio una directory LDAP, e poi avere i loro ruoli caricati da un altro dispositivo di memorizzazione, come un database relazionale.

Seam fornisce due implementazioni IdentityStore già pronte. JpaIdentityStore usa un database relazionale per memorizzare le informazioni su utenti e ruoli ed è il dispositivo di memorizzazione di identità di default che viene usato se non viene configurato niente in modo esplicito nel componente identityManager. L'altra implementazione fornita è LdapIdentityStore, che usa una directory LDAP per memorizzare utenti e ruoli.

Ci sono due proprietà configurabili per il componente identityManager, identityStore e roleIdentityStore. Il valore di queste proprietà deve essere un'espressione EL che fa riferimento ad un componente Seam che implementa l'interfaccia IdentityStore. Come già detto, se viene lasciato non configurato allora JpaIdentityStore viene assunto come default. Se è configurata solamente la proprietà identityStore allora lo stesso valore verrà usato anche per roleIdentityStore. Ad esempio la seguente voce in components.xml configura identityManager per usare un LdapIdentityStore sia per le operazioni relative agli utenti che per quelle relative ai ruoli:


      
  <security:identity-manager identity-store="#{ldapIdentityStore}"/>
      

Il seguente esempio configura identityManager per usare un LdapIdentityStore per le operazioni relative agli utenti e un JpaIdentityStore per le operazioni relative ai ruoli.


      
  <security:identity-manager 
    identity-store="#{ldapIdentityStore}" 
    role-identity-store="#{jpaIdentityStore}"/>
      

Il paragrafo seguente spiega con maggiore dettaglio entrambe queste implementazioni di IdentityStore.

Questa memorizzazione delle identità consente agli utenti e ai ruoli di essere memorizzati in un database relazionale. E' progettato per essere il meno restrittivo possibile riguardo allo schema del database, consentendo una grande flessibilità per la struttura delle tabelle sottostanti. Questo si ottiene tramite l'uso di uno speciale insieme di annotazioni, consentendo agli entity bean di essere configurati per memorizzare utenti e ruoli.

Come già menzionato, un apposito insieme di annotazioni viene usato per configurare gli entity bean per la memorizzazione di utenti e ruoli. La seguente tabella elenca ciascuna di queste annotazioni e la relativa descrizione.



Come detto precedentemente, JpaIdentityStore è progettato per essere il più possibile flessibile per ciò che riguarda lo schema del database delle tabelle degli utenti e dei ruoli. Questo paragrafo esamina una serie di possibili schemi di database che possono essere usati per memorizzare i record degli utenti e dei ruoli.

In questo esempio minimale una tabella di utenti e una di ruoli sono legate tramite una relazione molti-a-molti che utilizza una tabella di collegamento chiamata UserRoles.

@Entity

public class User {
  private Integer userId;
  private String username;
  private String passwordHash;
  private Set<Role
> roles;
  
  @Id @GeneratedValue
  public Integer getUserId() { return userId; }
  public void setUserId(Integer userId) { this.userId = userId; }
  
  @UserPrincipal
  public String getUsername() { return username; }
  public void setUsername(String username) { this.username = username; }
  
  @UserPassword(hash = "md5")
  public String getPasswordHash() { return passwordHash; }
  public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
  
  @UserRoles
  @ManyToMany(targetEntity = Role.class)
  @JoinTable(name = "UserRoles", 
    joinColumns = @JoinColumn(name = "UserId"),
    inverseJoinColumns = @JoinColumn(name = "RoleId"))
  public Set<Role
> getRoles() { return roles; }
  public void setRoles(Set<Role
> roles) { this.roles = roles; }
}
@Entity
public class Role {
  private Integer roleId;
  private String rolename;
  
  @Id @Generated
  public Integer getRoleId() { return roleId; }
  public void setRoleId(Integer roleId) { this.roleId = roleId; }
  
  @RoleName
  public String getRolename() { return rolename; }
  public void setRolename(String rolename) { this.rolename = rolename; }
}

Questo esempio è costruito a partire dall'esempio minimo includendo tutti i campi opzionali e consentendo ai ruoli di appartenere ai gruppi.

@Entity

public class User {
  private Integer userId;
  private String username;
  private String passwordHash;
  private Set<Role
> roles;
  private String firstname;
  private String lastname;
  private boolean enabled;
  
  @Id @GeneratedValue
  public Integer getUserId() { return userId; }
  public void setUserId(Integer userId) { this.userId = userId; }
  
  @UserPrincipal
  public String getUsername() { return username; }
  public void setUsername(String username) { this.username = username; }
  
  @UserPassword(hash = "md5")
  public String getPasswordHash() { return passwordHash; }
  public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
  
  @UserFirstName
  public String getFirstname() { return firstname; }
  public void setFirstname(String firstname) { this.firstname = firstname; }
  
  @UserLastName
  public String getLastname() { return lastname; }
  public void setLastname(String lastname) { this.lastname = lastname; }
  
  @UserEnabled
  public boolean isEnabled() { return enabled; }
  public void setEnabled(boolean enabled) { this.enabled = enabled; }
  
  @UserRoles
  @ManyToMany(targetEntity = Role.class)
  @JoinTable(name = "UserRoles", 
    joinColumns = @JoinColumn(name = "UserId"),
    inverseJoinColumns = @JoinColumn(name = "RoleId"))
  public Set<Role
> getRoles() { return roles; }
  public void setRoles(Set<Role
> roles) { this.roles = roles; }
}
@Entity
public class Role {
  private Integer roleId;
  private String rolename;
  private boolean conditional;
  
  @Id @Generated
  public Integer getRoleId() { return roleId; }
  public void setRoleId(Integer roleId) { this.roleId = roleId; }
  
  @RoleName
  public String getRolename() { return rolename; }
  public void setRolename(String rolename) { this.rolename = rolename; }
  
  @RoleConditional
  public boolean isConditional() { return conditional; }
  public void setConditional(boolean conditional) { this.conditional = conditional; }
  
  @RoleGroups
  @ManyToMany(targetEntity = Role.class)
  @JoinTable(name = "RoleGroups", 
    joinColumns = @JoinColumn(name = "RoleId"),
    inverseJoinColumns = @JoinColumn(name = "GroupId"))
  public Set<Role
> getGroups() { return groups; }
  public void setGroups(Set<Role
> groups) { this.groups = groups; }  
  
}

Quando si usa JpaIdentityStore come implementazione della memorizzazione delle identità con IdentityManager, alcuni eventi vengono lanciati in corrispondenza dell'invocazione di certi metodi di IdentityManager.

Questa implementazione della memorizzazione delle identità è progettata per funzionare quando le informazioni sugli utenti sono memorizzate in una directory LDAP. E' molto configurabile consentendo una grande flessibilità sul modo in cui utenti e ruoli sono memorizzati nella directory. ll seguente paragrafo descrive le opzioni di configurazione per questa implementazione e fornisce alcuni esempi di configurazione.

La seguente tabella descrive le proprietà disponibili che possono essere configurate in components.xml per LdapIdentityStore.

Tabella 15.3. Proprietà di configurazione di LdapIdentityStore

Proprietà

Valore di default

Descrizione

server-address

localhost

L'indirizzo del server LDAP

server-port

389

Il numero di porta su cui il server LDAP è in ascolto.

user-context-DN

ou=Person,dc=acme,dc=com

Il Distinguished Name (DN) del contesto contenente le informazioni sugli utenti.

user-DN-prefix

uid=

Questo valore è usato come prefisso anteponendolo al nome utente durante la ricerca delle informazioni sull'utente.

user-DN-suffix

,ou=Person,dc=acme,dc=com

Questo valore è aggiunto alla fine del nome utente per ricercare le informazioni sull'utente.

role-context-DN

ou=Role,dc=acme,dc=com

Il DN del contesto contenente le informazioni sui ruoli.

role-DN-prefix

cn=

Questo valore è usato come prefisso anteponendolo al nome del ruolo per formare il DN nella ricerca delle informazioni sul ruolo.

role-DN-suffix

,ou=Roles,dc=acme,dc=com

Questo valore è aggiunto al nome del ruolo per formare il DN nella ricerca delle informazioni sul ruolo.

bind-DN

cn=Manager,dc=acme,dc=com

Questo è il contesto usato per collegare il server LDAP.

bind-credentials

secret

Queste sono le credenziali (la password) usate per collegare il server LDAP.

user-role-attribute

roles

Questo è il nome dell'attributo sulle informazioni dell'utente che contiene la lista dei ruoli di cui l'utente è membro.

role-attribute-is-DN

true

Questa proprietà boolean indica se l'attributo del ruolo nelle informazioni dell'utente è esso stesso un Distinguished Name.

user-name-attribute

uid

Indica quale attributo delle informazioni sull'utente contiene il nome utente.

user-password-attribute

userPassword

Indica quale attributo nelle informazioni sull'utente contiene la password dell'utente.

first-name-attribute

null

Indica quale attributo nelle informazioni sull'utente contiene il nome proprio dell'utente.

last-name-attribute

sn

Indica quale attributo nelle informazioni sull'utente contiene il cognome dell'utente.

full-name-attribute

cn

Indica quale attributo nelle informazioni sull'utente contiene il nome per esteso dell'utente.

enabled-attribute

null

Indica quale attributo nelle informazioni sull'utente determina se l'utente è abilitato.

role-name-attribute

cn

Indica quale attributo nell'informazioni sul ruolo contiene il nome del ruolo.

object-class-attribute

objectClass

Indica quale attributo determina la classe di un oggetto nella directory.

role-object-classes

organizationalRole

Un elenco di classi di oggetto con cui devono essere create le informazioni su un nuovo ruolo.

user-object-classes

person.uidObject

Un elenco di classi di oggetto con cui devono essere create le informazioni su un nuovo utente.


IdentityManager può essere utilizzato sia iniettandolo in un componente Seam come di seguito:

  @In IdentityManager identityManager;

sia accedendo ad esso tramite il suo metodo statico instance():

  IdentityManager identityManager = IdentityManager.instance();

La seguente tabella descrive i metodi di API per IdentityManager:

Tabella 15.4. API per la gestione delle identità

Metodo

Valore restituito

Descrizione

createUser(String name, String password)

boolean

Crea un nuovo utente con il nome e la password specificate. Restituisce true se l'operazione si è conclusa con successo, oppure false.

deleteUser(String name)

boolean

Elimina le informazioni dell'utente con il nome specificato. Restituisce true se l'operazione si è conclusa con successo, oppure false.

createRole(String role)

boolean

Crea un nuovo ruolo con il nome specificato. Restituisce true se l'operazione si è conclusa con successo, oppure false.

deleteRole(String name)

boolean

Elimina il ruolo con il nome specificato. Restituisce true se l'operazione si è conclusa con successo, oppure false.

enableUser(String name)

boolean

Abilita l'utente con il nome specificato. Gli utenti che non sono abilitati non sono in grado di autenticarsi. Restituisce true se l'operazione si è conclusa con successo, oppure false.

disableUser(String name)

boolean

Disabilita l'utente con il nome specificato. Restituisce true se l'operazione si è conclusa con successo, oppure false.

changePassword(String name, String password)

boolean

Modifica la password dell'utente con il nome specificato. Restituisce true se l'operazione si è conclusa con successo, oppure false.

isUserEnabled(String name)

boolean

Restituisce true se l'utente specificato è abilitato, oppure false se non lo è.

grantRole(String name, String role)

boolean

Concede il ruolo specificato all'utente o al ruolo. Il ruolo deve già esistere per essere concesso. Restituisce true se il ruolo è stato concesso, oppure false se era già stato concesso all'utente.

revokeRole(String name, String role)

boolean

Revoca il ruolo specificato all'utente o al ruolo. Restituisce true se l'utente specificato era membro del ruolo e questo è stato revocato con successo, oppure false se l'utente non è un membro del ruolo.

userExists(String name)

boolean

Restituisce true se l'utente specificato esiste, oppure false se non esiste.

listUsers()

listUsers(String filter)

Restituisce una lista di tutti i nomi utente in ordine alfanumerico.

listUsers(String filter)

listUsers(String filter)

Restituisce una lista di tutti i nomi utente filtrata secondo il parametro di filtro specificato e in ordine alfanumerico.

listRoles()

listUsers(String filter)

Restituisce una lista di tutti i nomi dei ruoli.

getGrantedRoles(String name)

listUsers(String filter)

Restituisce una lista dei nomi di tutti i ruoli esplicitamente concessi all'utente con il nome specificato.

getImpliedRoles(String name)

listUsers(String filter)

Restituisce la lista dei nomi di tutti i ruoli implicitamente concessi all'utente specificato. I ruoli implicitamente concessi includono quelli che non sono concessi direttamente all'utente, ma sono concessi ai ruoli di cui l'utente è membro. Ad esempio, se il ruolo admin è un membro del ruolo user e un utente è membro del ruolo admin, allora i ruoli impliciti per l'utente sono sia admin che user.

authenticate(String name, String password)

boolean

Autenticazione il nome utente e la password specificati usando l'Identity Store configurato. Restituisce true se conclude con successo, oppure false se l'autenticazione fallisce. Il successo dell'autenticazione non implica niente oltre al valore restituito dal metodo. Non cambia lo stato del componente Identity. Per eseguire un vero e proprio login deve essere invece usato il metodo Identity.login().

addRoleToGroup(String role, String group)

boolean

Aggiunge il ruolo specificato come membro del gruppo specificato. Restituisce true se l'operazione va a buon fine.

removeRoleFromGroup(String role, String group)

boolean

Rimuove il ruolo specificato dal gruppo specificato. Restituisce true se l'operazione va a buon fine.

listRoles()

listUsers(String filter)

Elenca i nomi di tutti i ruoli.


L'uso delle API per la gestione delle identità richiede che l'utente chiamante abbia le autorizzazioni appropriate per invocare i suoi metodi. La seguente tabella descrive i permessi richiesti per ciascuno dei metodi in IdentityManager. Gli oggetti dei permessi elencati qui sotto sono valori stringa.


Il seguente listato fornisce un esempio con un insieme di regole di sicurezza che concedono al ruolo admin l'accesso a tutti i metodi relativi alla gestione delle identità:

rule ManageUsers
  no-loop
  activation-group "permissions"
when
  check: PermissionCheck(name == "seam.user", granted == false)
  Role(name == "admin")
then
  check.grant();
end

rule ManageRoles
  no-loop
  activation-group "permissions"
when
  check: PermissionCheck(name == "seam.role", granted == false)
  Role(name == "admin")
then
  check.grant();
end

Le API di sicurezza producono una serie di messaggi di default per i diversi eventi relativi alla sicurezza. La seguente tabella elenca le chiavi dei messaggi che possono essere usate per sovrascrivere questi messaggi specificandoli in un file message.properties. Per sopprimere un messaggio basta mettere nel file la chiave con un valore vuoto.


Ci sono diversi meccanismi di autorizzazione forniti dalle API di sicurezza di Seam per rendere sicuro l'accesso ai componenti, ai metodi dei componenti e alle pagine. Questo paragrafo descrive ognuno di essi. Un aspetto importante da notare è che qualora si voglia utilizzare una delle caratteristiche avanzate (come i permessi basati sulle regole) il components.xml potrebbe dover essere configurato per gestirle. Vedi il paragrafo Configurazione più sopra.

La sicurezza di Seam è costruita intorno alla premessa per cui agli utenti vengono concessi ruoli e/o permessi, consentendo loro di eseguire operazioni che non sarebbero altrimenti permesse agli utenti senza i necessari privilegi di sicurezza. Ognuno dei meccanismi di autorizzazione forniti dalle API di sicurezza di Seam è costruito intorno a questo concetto principale di ruoli e permessi, con un framework espandibile che fornisce più modi per rendere sicure le risorse di un'applicazione.

Iniziamo ad esaminare la forma più semplice di autorizzazione, la sicurezza dei componenti, inziando con l'annotazione @Restrict.

I componenti Seam possono essere resi sicuri sia a livello di metodo che a livello di classe usando l'annotazione @Restrict. Qualora sia un metodo sia la classe in cui questo è dichiarato sono annotati con @Restrict, la restrizione sul metodo ha la precedenza (e la restrizione sulla classe non si applica). Se nell'invocazione di un metodo fallisce il controllo di sicurezza, viene lanciata un'eccezione come definito nel contratto di Identity.checkRestriction() (vedi Restrizioni in linea). Una @Restrict solo sulla classe del componente stesso è equivalente ad aggiungere @Restrict a ciascuno dei suoi metodi.

Una @Restrict vuota implica un controllo di permesso per nomeComponente:nomeMetodo. Prendiamo ad esempio il seguente metodo di un componente:

@Name("account")

public class AccountAction {
    @Restrict public void delete() {
      ...
    }
}

In questo esempio il permesso richiesto per chiamare il metodo delete() è account:delete. L'equivalente di ciò sarebbe stato scrivere @Restrict("#{s:hasPermission('account','delete')}"). Ora vediamo un altro esempio:

@Restrict @Name("account")

public class AccountAction {
    public void insert() {
      ...
    }
    @Restrict("#{s:hasRole('admin')}")
    public void delete() {
      ...
    }
}

Questa volta la classe stessa del componente è annotata con @Restrict. Ciò significa che tutti i metodi senza una annotazione @Restrict a sovrascrivere, richiedono un controllo implicito di permesso. Nel caso di questo esempio il metodo insert() richiede un permesso per account:insert, mentre il metodo delete() richiede che l'utente sia membro del ruolo admin.

Prima di andare avanti, esaminiamo l'espressione #{s:hasRole()} vista nell'esempio precedente. Sia s:hasRole() che s:hasPermission sono funzioni EL, le quali delegano ai metodi con i nomi corrispondenti nella classe Identity. Queste funzioni possono essere usate all'interno di una espressione EL in tutte le API di sicurezza.

Essendo un'espressione EL, il valore dell'annotazione @Restrict può fare riferimento a qualunque oggetto che sia presente in un contesto Seam. Ciò è estremamente utile quando si eseguono i controlli sui permessi per una specifica istanza di un oggetto. Ad esempio:

@Name("account")

public class AccountAction {
    @In Account selectedAccount;
    @Restrict("#{s:hasPermission(selectedAccount,'modifica')}")
    public void modify() {
        selectedAccount.modify();
    }
}

La cosa interessante da notare in questo esempio è il riferimento a selectedAccount che si vede all'interno della chiamata alla funzione hasPermission. Il valore di questa variabile verrà ricercato all'interno del contesto Seam e passato al metodo hasPermission() di Identity, il quale in questo caso può determinare se l'utente ha il permesso richiesto per modificare l'oggetto Account specificato.

Uno degli indicatori di interfaccia utente ben progettata è quando agli utenti non vengono presentate opzioni per le quali essi non hanno i permessi necessari. La sicurezza di Seam consente la visualizzazione condizionale sia di sezioni di una pagina che di singoli controlli, basata sui privilegi dell'utente, usando esattamente le stesse espressioni EL usate nella sicurezza dei componenti.

Diamo un'occhiata ad alcuni esempi della sicurezza nell'interfaccia. Prima di tutto prentendiamo di avere una form di accesso che debba essere visualizzata solo se l'utente non ha già fatto l'accesso. Usando la proprietà identity.isLoggedIn() possiamo scrivere questo:


<h:form class="loginForm" rendered="#{not identity.loggedIn}"
>

Se l'utente non ha eseguito l'accesso, allora la form di accesso verrà visualizzata. Fin qui tutto bene. Ora vogliamo che ci sia un menu sulla pagina che contenga alcune azioni speciali che devono essere accessibili solo agli utenti del ruolo dirigente. Ecco un modo in cui ciò potrebbe essere scritto:


<h:outputLink action="#{reports.listManagerReports}" rendered="#{s:hasRole('dirigente')}">
    Rapporti per i dirigenti
</h:outputLink
>

Anche fin qui tutto bene. Se l'utente non è un membro del ruolo dirigente, allora outputLink non verrà visualizzato. L'attributo rendered in generale può essere usato per il controllo stesso oppure in un controllo <s:div> o <s:span> che ne comprende altri.

Ora andiamo su qualcosa di più complesso. Supponiamo di avere in una pagina un controllo h:dataTable che elenca delle righe per le quali si può volere visualizzare o meno i link alle azioni in funzione dei permessi dell'utente. La funzione EL s:hasPermission ci consente di passare un parametro oggetto che può essere usato per determinare se l'utente ha o meno il permesso richiesto per quell'oggetto. Ecco come può apparire una dataTable con dei link controllati dalla sicurezza:


<h:dataTable value="#{clients}" var="cl">
    <h:column>
        <f:facet name="header"
>Name</f:facet>
        #{cl.name}
    </h:column>
    <h:column>
        <f:facet name="header"
>City</f:facet>
        #{cl.city}
    </h:column>
    <h:column>
        <f:facet name="header"
>Action</f:facet>
        <s:link value="Modify Client" action="#{clientAction.modify}"
                rendered="#{s:hasPermission(cl,'modify')}"/>
        <s:link value="Delete Client" action="#{clientAction.delete}"
                rendered="#{s:hasPermission(cl,'delete')}"/>
    </h:column>
</h:dataTable
>

La sicurezza di Seam consente anche di applicare le restrizioni di sicurezza alle azioni per leggere, inserire, aggiornare e cancellare le entità.

Per rendere sicure tutte le azioni per una classe entità, aggiungere un'annotazione @Restrict alla classe stessa:

@Entity

@Name("customer")
@Restrict
public class Customer {
  ...
}

Se nell'annotazione @Restrict non è indicata alcuna espressione, il controllo di sicurezza di default che viene eseguito è una verifica del permesso entità:azione, dove l'obiettivo del permesso è l'istanza dell'entità e azione è read, insert, update o delete.

E' anche possibile applicare una restrizione solo a determinate azioni, posizionando l'annotazione @Restrict nel corrispondente metodo relativo al ciclo di vita dell'entità (annotato come segue):

Ecco un esempio di come un'entità potrebbe essere configurata per eseguire un controllo di sicurezza per tutte le operazioni insert. Notare che non è richiesto che il metodo faccia qualcosa, la sola cosa importante per quanto riguarda la sicurezza è come questo viene annotato:



  @PrePersist @Restrict
  public void prePersist() {}
   

Ed ecco un esempio di una regola sui permessi di entità che controlla se all'utente autenticato è consentito di inserire un record MemberBlog (dall'applicazione di esempio seamspace). L'entità per la quale viene fatto il controllo di sicurezza è inserita automaticamente nella working memory (in questo caso MemberBlog):

rule InsertMemberBlog
  no-loop
  activation-group "permissions"
when
  principal: Principal()
  memberBlog: MemberBlog(member : member -> (member.getUsername().equals(principal.getName())))
  check: PermissionCheck(target == memberBlog, action == "insert", granted == false)
then
  check.grant();
end;

Questa regola concederà il permesso memberBlog:insert se l'utente attualmente autenticato (indicato dal fatto Principal) ha lo stesso nome del membro per il quale è stata creata la voce del blog. La riga "principal: Principal()" può essere vista nel codice di esempio come un collegamento con una variabile. Essa collega l'istanza dell'oggetto Principal nella working memory (posizionato durante l'autenticazione) e lo assegna ad una variabile chiamata principal. I collegamenti con le variabili consentono di fare riferimento al valore in altri posti, come nella riga successiva che confronta il nome dell'utente con il nome del Principal. Per maggiori dettagli fare riferimento alla documentazione di JBoss Rules.

Infine abbiamo bisogno di installare una classe listener che integra la sicurezza Seam con la libreria JPA.

Seam fornisce una serie di annotazioni che possono essere usate come un'alternativa a @Restrict e che hanno l'ulteriore vantaggio di essere verificabili durante la compilazione, dato che non gestiscono espressioni EL arbitrarie nel modo in cui succede per la @Restrict.

Così com'è, Seam contiene delle annotazioni per i permessi standard per le operazioni CRUD, comunque è solo questione di aggiungerne altre. Le seguenti annotazioni sono fornite nel pacchetto org.jboss.seam.annotations.security:

Per usare queste annotazioni basta metterle sul metodo o sul parametro per il quale si vuole eseguire il controllo di sicurezza. Se messe su un metodo, allora dovranno specificare la classe obiettivo per la quale il permesso deve essere controllato. Si prenda il seguente esempio:

  @Insert(Customer.class)
  public void createCustomer() {
    ...
  }

In questo esempio un controllo di permessi viene fatto sull'utente per assicurarsi che abbia i diritti per creare un nuovo oggetto Customer. L'obiettivo del controllo di permessi sarà Customer.class (l'effettiva istanza di java.lang.Class) e l'azione è la rappresentazione a lettere minuscole del nome dell'annotazione, che in questo esempio è insert.

E' anche possibile annotare i parametri di un metodo di un componente allo stesso modo. Se viene fatto in questo modo non è richiesto di specificare l'obiettivo del permesso (dato che il valore stesso del parametro sarà l'obiettivo del controllo di permessi):

  public void updateCustomer(@Update Customer customer) {
    ...
  }

Per creare una propria annotazione di sicurezza basta annotarla con @PermissionCheck, ad esempio:

@Target({METHOD, PARAMETER})

@Documented
@Retention(RUNTIME)
@Inherited
@PermissionCheck
public @interface Promote {
   Class value() default void.class;
}

Se si vuole modificare il nome dell'azione di default del permesso (che è la versione a lettere minuscole del nome dell'annotazione) con un altro valore, è possibile specificarlo all'interno dell'annotazione @PermissionCheck:

@PermissionCheck("upgrade")

La sicurezza di Seam fornisce un framework espandibile per risolvere i permessi dell'applicazione. Il seguente diagramma di classi mostra una panoramica dei componenti principali del framework dei permessi:

Le classi rilevanti sono spiegate con maggiore dettaglio nel seguente paragrafo.

Questa è in realtà un'interfaccia che fornisce i metodi per risolvere i singoli permessi sugli oggetti. Seam fornisce le seguenti implementazioni già fatte di PermissionResolver, che sono descritte in maggiore dettaglio più avanti in questo capitolo:

E' molto semplice implementare il proprio risolutore di permessi. L'interfaccia PermissionResolver definisce solo due metodi che devono essere implementati, come mostra la seguente tabella. Includendo la propria implementazione di PermissionResolver nel proprio progetto Seam, essa sarà automaticamente rilevata durante l'esecuzione e registrata nel ResolverChain predefinito.


Nota

Essendo conservati nella sessione dell'utente, ogni implementazione di PermissionResolver deve aderire ad un paio di restrizioni. In primo luogo, non possono contenere alcuna informazione di stato che abbia una visibilità inferiore a session (e la visibilità del componente stesso deve essere o application oppure session). In secondo luogo, non devono usare la dependency injection poiché ci potrebbero essere accessi da più thread contemporaneamente. Infatti, per ragioni di prestazioni, è raccomandabile annotare con @BypassInterceptors per evitare del tutto l'insieme degli intercettori Seam.

Un ResolverChain contiene un elenco ordinato di PermissionResolver, con lo scopo di risolvere i permessi sugli oggetti di una determinata classe oppure i permessi obiettivo.

La ResolverChain di default consiste di tutti i risolutori di permessi rilevati durante l'avvio dell'applicazione. L'evento org.jboss.seam.security.defaultResolverChainCreated viene lanciato (e l'istanza di ResolverChain viene passata come parametro dell'evento) quando il ResolverChain di default viene creato. Questo consente di aggiungere ulteriori risolutori che per qualche ragione non erano stati rilevati durante l'avvio, oppure di riordinare o rimuovere i risolutori che sono nell'elenco.

Il seguente diagramma di sequenza mostra l'interazione tra i componenti del framework dei permessi durante la verifica di un permesso (segue la spiegazione). Una verifica di permesso può essere originata da una serie di possibili fonti, ad esempio gli intercettori di sicurezza, la funzione EL s:hasPermission, oppure tramite una chiamata alla API Identity.checkPermission:

Uno dei risolutori di permesso già fatti forniti da Seam, RuleBasedPermissionResolver, consente di valutare i permessi in base ad un insieme di regole di sicurezza Drools (JBoss Rules). Un paio di vantaggi nell'uso di un motore di regole sono: 1) una posizione centralizzata della logica di gestione che è usata per valutare i permessi degli utenti; 2) la velocità, Drools usa algoritmi molto efficienti per valutare grandi quantità di regole complesse comprendenti condizioni multiple.

La configurazione per RuleBasedPermissionResolver richiede che una base di regole venga prima configurata in components.xml. Di default questa base di regole viene chiamata securityRules, come nel seguente esempio:


<components xmlns="http://jboss.com/products/seam/components"
              xmlns:core="http://jboss.com/products/seam/core"
              xmlns:security="http://jboss.com/products/seam/security"
              xmlns:drools="http://jboss.com/products/seam/drools"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation=
                  "http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.2.xsd
                   http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.2.xsd
                   http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-2.2.xsd
                   http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.2.xsd">
  
     <drools:rule-base name="securityRules">
         <drools:rule-files>
             <value
>/META-INF/security.drl</value>
         </drools:rule-files>
     </drools:rule-base>
  
  </components
>

Il nome predefinito della base di regole può essere modificato specificando la proprietà security-rules per RuleBasedPermissionResolver:

  <security:rule-based-permission-resolver security-rules="#{prodSecurityRules}"/>

Una volta che il componente RuleBase è configurato, è il momento di scrivere le regole di sicurezza.

Il primo passo per scrivere delle regole di sicurezza è di creare un nuovo file di regole nella cartella /META-INF del file jar dell'applicazione. Di solito questo file dovrebbe essere chiamato qualcosa come security.drl, comunque lo si può chiamare nel modo che si preferisce purché sia configurato in maniera corrispondente in components.xml.

Dunque, che cosa deve contenere il file delle regole di sicurezza? A questo punto potrebbe essere una buona idea almeno sbirciare nella documentazione Drools, comunque per partire ecco un esempio estremamente semplice:

package MyApplicationPermissions;
  
  import org.jboss.seam.security.permission.PermissionCheck;
  import org.jboss.seam.security.Role;
  
  rule CanUserDeleteCustomers
  when
    c: PermissionCheck(target == "customer", action == "delete")
    Role(name == "admin")
  then
    c.grant();
  end

Dividiamolo passo per passo. La prima cosa che vediamo è la dichiarazione del pacchetto. Un pacchetto in Drools è essenzialmente una collezione di regole. Il nome del pacchetto può essere qualsiasi, non è in relazione con niente che sia al di fuori della visibilità della base di regole.

La cosa successiva che possiano notare è un paio di dichiarazioni import per le classi PermissionCheck e Role. Questi import informano il motore di regole che all'interno delle nostre regole faremo riferimento a queste classi.

Infine abbiamo il codice della regola. Ogni regola all'interno di un pacchetto deve avere un nome univoco (di solito descrive lo scopo della regola). In questo caso la nostra regola si chiama CanUserDeleteCustomers e verrà usata per verificare se ad un utente è consentito di cancellare un record relativo ad un cliente.

Guardando il corpo della definizione della regola si possono notare due distinte sezioni. Le regole hanno quello che è noto come lato sinistro (LHS, left hand side) e un lato destro (RHS, right hand side). Il lato sinistro consiste nella parte condizionale della regola, cioè l'elenco delle condizioni che devono essere soddisfatte affinché si applichi la regola. Il lato sinistro è rappresentato dalla sezione when. Il lato destro è la conseguenza, o la parte di azione della regola che si applica solo se tutte le condizioni del lato sinistro sono verificate. Il lato destro è rappresentato dalla sezione then. La fine della regola è stabilita dalla linea end.

Se guardiamo la parte sinistra della regola vediamo che ci sono due condizioni. Esaminiamo la prima condizione:

c: PermissionCheck(target == "customer", action == "delete")

Letta in inglese questa condizione dice che all'interno della working memory deve esistere un oggetto PermissionCheck con una proprietà target uguale a "customer" e una proprietà action uguale a "delete".

Dunque cos'è la working memory? Nota anche come "stateful session" nella terminologia Drools, la working memory è un oggetto collegato alla sessione che contiene le informazioni contestuali che sono richieste dal motore di regole per prendere una decisione sul controllo di permesso. Ogni volta che il metodo hasPermission() viene chiamato, viene creato un oggetto, o Fatto, temporaneo PermissionCheck, e viene inserito nella working memory. Questo PermissionCheck corrisponde esattamente al permesso che si sta controllando, così, ad esempio, se viene chiamato hasPermission("account", "create") allora verrà inserito nella working memory un oggetto PermissionCheck con target uguale a "account" e action uguale a "create", per la durata del controllo di permesso.

Accanto al fatto PermissionCheck c'è anche un fatto org.jboss.seam.security.Role per ogni ruolo di cui l'utente autenticato è membro. Questi fatti Role sono sincronizzati con i ruoli dell'utente autenticato all'inizio di ogni controllo di permesso. Di conseguenza qualsiasi oggetto Role che venisse inserito nella working memory nel corso del controllo di permesso sarebbe rimosso prima che avvenga il controllo di permesso successivo, a meno che l'utente autenticato non sia effettivamente membro di quel ruolo. Insieme ai fatti PermissionCheck e Role la working memory contiene anche l'oggetto java.security.Principal che era stato creato come risultato del processo di autenticazione.

E' anche possibile inserire ulteriori fatti nella working memory chiamando RuleBasedPermissionResolver.instance().getSecurityContext().insert(), passando l'oggetto come parametro. Fanno eccezione a questo gli oggetti Role che, come già detto, sono sincronizzati all'inizio di ciascun controllo di permesso.

Tornando al nostro esempio, possiamo anche notare che la prima linea della nostra parte sinistra ha il prefisso c:. Questa è una dichiarazione di variabile ed è usata per fare riferimento all'oggetto rilevato dalla condizione (in questo caso il PermissionCheck). Passando alla seconda linea della nostra parte sinistra vediamo questo:

Role(name == "admin")

Questa condizione dichiara semplicemente che ci deve essere un oggetto Role con un name uguale ad "admin" nella working memory. Come già menzionato, i ruoli dell'utente sono inseriti nella working memory all'inizio di ogni controllo di permesso. Così, mettendo insieme entrambe le condizioni, questa regola in pratica dice "mi attiverò quando ci sarà un controllo per il permesso customer:delete e l'utente è un membro del ruolo admin".

Quindi qual è la conseguenza dell'attivazione della regola? Diamo un'occhiata alla parte destra della regola:

c.grant()

La parte destra è costituita da codice Java e, in questo caso, esso invoca il metodo grant() dell'oggetto c il quale, come già detto, è una variabile che rappresenta l'oggetto PermissionCheck. Insieme alle proprietà name e action, nell'oggetto PermissionCheck c'è anche una proprietà granted che inizialmente è impostata a false. Chiamando grant() su un PermissionCheck la proprietà granted viene impostata a true, il che significa che il controllo di permesso è andato a buon fine, consentendo all'utente di portare avanti qualsiasi azione per cui il controlo di permesso era stato inteso.

Un altro risolutore di permessi incluso in Seam, il PersistentPermissionResolver consente di caricare i permessi da un dispositivo di memorizzazione persistente, come un database relazionale. Questo risolutore di permessi fornisce una sicurezza orientata alle istanze in stile ACL (Access Control List), permettendo di assegnare specifici permessi sull'oggetto a utenti e ruoli. Allo stesso modo permette inoltre di assegnare in modo persistente permessi con un nome arbitrario (non necessariamente basato sull'oggetto o la classe).

Il PersistentPermissionManager richiede un permission store per connettersi al dispositivo di memorizzazione dove sono registrati i permessi. Seam fornisce una implementazione di PermissionStore già fatta, JpaPermissionStore, che viene usata per memorizzare i permessi in un database relazionale. E' possibile scrivere il proprio permission store implementando l'interfaccia PermissionStore, che definisce i seguenti metodi:

Tabella 15.8. Interfaccia PermissionStore

Tipo restituito

Metodo

Descrizione

List<Permission>

listPermissions(Object target)

Questo metodo deve restituire una List di oggetti Permission che rappresenti tutti i permessi concessi per l'oggetto indicato come obiettivo.

List<Permission>

listPermissions(Object target, String action)

Questo metodo deve restituire una List di oggetti Permission che rappresenti tutti i permessi sull'azione specificata, concessi per l'oggetto indicato come obiettivo.

List<Permission>

listPermissions(Set<Object> targets, String action)

Questo metodo deve restituire una List di oggetti Permission che rappresenti tutti i permessi sull'azione specificata concessi per l'insieme di oggetti indicati come obiettivi.

boolean

grantPermission(Permission)

Questo metodo deve rendere persistente l'oggetto Permission specificato nel dispositivo di memorizzazione, restituendo vero se l'operazione si conclude senza errori.

boolean

grantPermissions(List<Permission> permissions)

Questo metodo deve rendere persisistenti tutti gli oggetti Permission contenuti nella List specificata, restituendo vero se l'operazione si conclude senza errori.

boolean

revokePermission(Permission permission)

Questo metodo deve rimuove l'oggetto Permission specificato dal dispositivo di memorizzazione.

boolean

revokePermissions(List<Permission> permissions)

Questo metodo deve rimuovere dal dispositivo di memorizzazione tutti gli oggetti Permission specificati nella lista.

List<String>

listAvailableActions(Object target)

Questo metodo deve restituire una lista di tutte le azioni disponibili (sotto forma di String) per la classe dell'oggetto specificato. Viene usato dalla gestione dei permessi per costruire l'interfaccia utente con cui si concedono i permessi sulle varie classi (vedi il paragrafo più avanti).


E' l'implementazione di default di PermissionStore (e l'unica fornita da Seam), che usa un database relazionale per memorizzare i permessi. Prima di poter essere usata deve essere configurata con una o due classi per la memorizzazione dei permessi su utenti e ruoli. Queste classi entità devono essere annotate con uno speciale insieme di annotazioni relative alla sicurezza per configurare quali proprietà dell'entità corrispondono alle diverse caratteristiche dei permessi che devono essere memorizzati.

Se si vuole usare la stessa entità (cioè una sola tabella sul database) per memorizzare sia i permessi degli utenti che quelli dei ruoli, allora è necessario configurare solo la proprietà user-permission-class. Se si vogliono usare due tabelle distinte per memorizzare i permessi degli utenti e quelli dei ruoli, allora in aggiunta alla proprietà user-permission-class si dovrà configurare anche la proprietà role-permission-class.

Ad esempio, per configurare una sola classe entità per memorizzare sia i permessi degli utenti che quelli dei ruoli:


<security:jpa-permission-store user-permission-class="com.acme.model.AccountPermission"/>

Per configurare classi entità separate per la memorizzazione dei permessi di utenti e ruoli:


<security:jpa-permission-store user-permission-class="com.acme.model.UserPermission"
    role-permission-class="com.acme.model.RolePermission"/>

Come già detto, le classi entità che contengono i permessi degli utenti e dei ruoli devono essere configurate con uno speciale insieme di annotazioni contenute nel pacchetto org.jboss.seam.annotations.security.permission. La seguente tabella elenca queste annotazioni insieme ad una descrizione su come sono usate:

Tabella 15.9. Annotazioni per le entità dei permessi

Annotazione

Obiettivo

Descrizione

@PermissionTarget

FIELD,METHOD

Questa annotazione identifica la proprietà dell'entità che contiene l'obiettivo del permesso. La proprietà deve essere di tipo java.lang.Sting.

@PermissionAction

FIELD,METHOD

Questa annotazione identifica la proprietà dell'entità che contiene l'azione. La proprietà deve essere di tipo java.lang.String.

@PermissionUser

FIELD,METHOD

Questa annotazione identifica la proprietà dell'entità che contiene l'utente a cui viene concesso il permesso. Deve essere di tipo java.lang.String e contenere la username dell'utente.

@PermissionRole

FIELD,METHOD

Questa annotazione identifica la proprietà dell'entità che contiene il ruolo a cui viene concesso il premesso. Deve essere di tipo java.lang.String e contenere i nome del ruolo.

@PermissionDiscriminator

FIELD,METHOD

Questa annotazione deve essere usata quando la stessa entità/tabella viene usata per memorizzare sia i permessi degli utenti che quelli dei ruoli. Essa identifica la proprietà dell'entità che è usata per discriminare tra i permessi degli utenti e quelli dei ruoli. Per default, se il valore della colonna contiene la stringa user, allora il record sarà trattato come un permesso utente. Se contiene la stringa role, allora sarà trattato come un permesso del ruolo. E' anche possibile sovrascrivere questi valori specificando le proprietà userValue e roleValue all'interno delle annotazioni. Ad esempio, per usare u e r invece di user e role, le annotazioni dovranno essere scritte in questo modo:

@PermissionDiscriminator(userValue = "u", roleValue = "r")

Ecco un esempio di una classe entità che viene usata per memorizzare sia i permessi degli utenti che quelli dei ruoli. La seguente classe si trova nell'applicazione di esempio SeamSpace:



@Entity
public class AccountPermission implements Serializable {  
   private Integer permissionId;
   private String recipient;
   private String target;
   private String action;
   private String discriminator;
   
   @Id @GeneratedValue
   public Integer getPermissionId() {
      return permissionId;
   }
   
   public void setPermissionId(Integer permissionId) {
      this.permissionId = permissionId;
   }
   
   @PermissionUser @PermissionRole
   public String getRecipient() {
      return recipient;
   }
   
   public void setRecipient(String recipient) {
      this.recipient = recipient;
   }
   
   @PermissionTarget
   public String getTarget() {
      return target;
   }
   
   public void setTarget(String target) {
      this.target = target;
   }
   
   @PermissionAction
   public String getAction() {
      return action;
   }
   
   public void setAction(String action) {
      this.action = action;
   }
   
   @PermissionDiscriminator
   public String getDiscriminator() {
      return discriminator;
   }
   
   public void setDiscriminator(String discriminator) {
      this.discriminator = discriminator;
   }
}          
          

Come si vede dall'esempio precedente, il metodo getDiscriminator() è stato annotato con l'annotazione @PermissionDiscriminator per consentire a JpaPermissionStore di determinare quali record rappresentano i permessi degli utenti e quali rappresentano i permessi dei ruoli. Inoltre si può vedere che il metodo getRecipient() è annotato sia con l'annotazione @PermissionUser che con @PermissionRole. Ciò è perfettamente valido e significa semplicemente che la proprietà recipient dell'entità conterrà sia il nome dell'utente che quello del ruolo, in funzione del valore della proprietà discriminator.

Quando JpaPermissionStore memorizza o cerca un permesso deve essere in grado di identificare univocamente le istanze degli oggetti sui cui permessi deve operare. Per ottenere questo occorre assegnare una strategia di risoluzione dell'identificatore per ciascuna classe obiettivo, in modo da generare i valori identificativi univoci. Ciascuna implementazione della strategia di risoluzione sa come generare gli identificativi univoci per un particolare tipo di classe ed è solo questione di creare nuove strategie di risoluzione.

L'interfaccia IdentifierStrategy è molto semplice e dichiara solo due metodi:

public interface IdentifierStrategy {

   boolean canIdentify(Class targetClass);
   String getIdentifier(Object target);
}

Il primo metodo, canIdentify() restituisce semplicemente true se la strategia di risoluzione è in grado di generare un identificativo univoco per la classe obiettivo specificata. Il secondo metodo, getIdentifier() restituisce il valore dell'identificativo univoco per l'oggetto obiettivo specificato.

Seam fornisce due implementazioni di IdentifierStrategy, ClassIdentifierStrategy e EntityIdentifierStrategy (vedi i prossimi paragrafi per i dettagli).

Per configurare esplicitamente la strategia di risoluzione da usare per una particolare classe, essa deve essere annotata con org.jboss.seam.annotations.security.permission.Identifier, e il valore deve essere impostato a una implementazione dell'interfaccia IdentifierStrategy. Una proprietà facoltativa name può essere specificata, e il suo effetto dipende dall'implementazione di IdentifierStrategy usata.

Questa strategia di risoluzione è usata per generare valori di identificatori univoci per gli entity bean. Quello che fa è concatenare il nome dell'entità (o un nome configurato in altro modo) con una stringa che rappresenta la chiave primaria dell'entità. Le regole per generare la parte nome dell'identificatore sono simili a ClassIdentifierStrategy. La chiave primaria (cioè l'id dell'entità) viene ottenuto usando il componente PersistenceProvider, che è in grado di determinarne il valore a prescindere dall'implementazione della persistenza utilizzata dall'applicazione Seam. Per le entità non annotate con @Entity è necessario configurare esplicitamente la strategia di risoluzione dell'identificatore nella classe entità stessa, ad esempio:

@Identifier(value = EntityIdentifierStrategy.class)

public class Customer { 

Per avere un esempio del tipo di valori di identificatore generati, supponiamo di avere la seguente classe entità:

@Entity

public class Customer {
  private Integer id;
  private String firstName;
  private String lastName;
  
  @Id 
  public Integer getId() { return id; }
  public void setId(Integer id) { this.id = id; }
  
  public String getFirstName() { return firstName; }
  public void setFirstName(String firstName) { this.firstName = firstName; }
  
  public String getLastName() { return lastName; }
  public void setLastName(String lastName) { this.lastName = lastName; }
}

Per una istanza di Customer con un valore di id pari a 1, il valore dell'identificatore sarà "Customer:1". Se l'entità è annotata con un nome esplicito di identificatore, come questo:

@Entity

@Identifier(name = "cust")
public class Customer { 

Allora un Customer con un id pari a 123 avrà come identificatore "cust:123".

In modo del tutto simile a come la sicurezza di Seam fornisce una API per la gestione delle identità per gestire utenti e ruoli, essa fornisce anche una API per la gestione dei permessi, tramite il componente PermissionManager.

Il componente PermissionManager è un componente Seam registrato a livello application che fornisce una serie di metodi per gestire i permessi. Prima di poter essere usato deve essere configurato con un permission store (benché di default tenterà di usare il JpaPermissionStore se disponibile). Per configurare esplicitamente un permission store personalizzato, occorre specificare la proprietà permission-store in components.xml:



<security:permission-manager permission-store="#{ldapPermissionStore}"/>      
      

La seguente tabella descrive ciascuno dei metodi disponibili forniti da PermissionManager:

Tabella 15.11. Metodi della API PermissionManager

Tipo restituito

Metodo

Descrizione

List<Permission>

listPermissions(Object target, String action)

Restituisce un elenco di oggetti Permession che rappresenta tutti i permessi che sono stati concessi all'obiettivo e all'azione specificata.

List<Permission>

listPermissions(Object target)

Restituisce un elenco di oggetti Permession che rappresenta tutti i permessi che sono stati concessi all'obiettivo e all'azione specificata.

boolean

grantPermission(Permission permission)

Memorizza (concede) il Permission specificato nel permission store sottostante. Restituisce true se l'operazione si è conclusa con esito positivo.

boolean

grantPermissions(List<Permission> permissions)

Memorizza (concede) l'elenco di Permission specificato nel permission store sottostante. Restiuisce true se l'operazione si è conclusa con esito positivo.

boolean

revokePermission(Permission permission)

Rimuove (revoca) il Permission specificato dal permission store sottostante. Restituisce true se l'operazione si è conclusa con esito positivo.

boolean

revokePermissions(List<Permission> permissions)

Rimuove (revoca) l'elenco di Permission specificato dal permission store sottostante. Restituisce true se l'operazione si è conclusa con esito positivo.

List<String>

listAvailableActions(Object target)

Restituisce un elenco delle azioni disponibili per l'oggetto obiettivo specificato. Le azioni che questo metodo restituisce dipendono dall'annotazione @Permission configurata nella classe dell'oggetto obiettivo.


Seam include un supporto di base per servire le pagine sensibili tramite il protocollo HTTPS. Si configura facilmente specificando uno scheme per la pagina in pages.xml. Il seguente esempio mostra come la pagina /login.xhtml è configurata per usare HTTPS:


<page view-id="/login.xhtml" scheme="https"/>

Questa configurazione viene automaticamente estesa ai controlli JSF s:link e s:button, i quali (quando viene specificata la view) faranno in modo che venga prodotto il link usando il protocollo corretto. In base al precedente esempio, il seguente link userà il protocollo HTTPS, perché /login.xhtml è configurata per usarlo:


<s:link view="/login.xhtml" value="Login"/>

Navigando direttamente sulla pagina mentre si usa il protocollo non corretto causerà una redirezione alla stessa pagina usando il protocollo corretto. Ad esempio, navigando su una pagina che ha scheme="https" usando HTTP causerà una redirezione alla stessa pagina usando HTTPS.

E' anche possibile configurare un default scheme per tutte le pagine. Questo è utile se si vuole usare HTTPS solo per alcune pagine. Se non è indicato un default scheme, allora il comportamento normale è di continuare ad usare lo schema correntemente usato. Perciò una volta che l'utente ha fatto l'accesso ad una pagina che richiede HTTPS, allora HTTPS continuerà ad essere usato dopo che l'utente si sposta su altre pagine non HTTPS. (Mentre questo è buono per la sicurezza, non lo è per le prestazioni!). Per definire HTTP come default scheme, aggiungere questa riga a pages.xml:


<page view-id="*" scheme="http" />

Chiaramente, se nessuna delle pagine dell'applicazione usa HTTPS allora non è richiesto di specificare alcun default scheme.

E' possibile configurare Seam per invalidare automaticamente la sessione HTTP ogni volta che lo schema cambia. Basta aggiungere questa riga a components.xml:


<web:session invalidate-on-scheme-change="true"/>

Questa opzione aiuta a rendere il sistema meno vulnerabile alle intromissioni che rilevano l'id di sessione o alla mancanza di protezione su dati sensibili dalle pagine che usano HTTPS ad altre che usano HTTP.

Sebbene non faccia strettamente parte delle API di sicurezza, Seam fornisce un algoritmo CAPTCHA (Completely Automated Public Turing test to tell Computer and Humans Apart, test di Turing publico completamente automatico per distinguere gli umani dalle macchine) già fatto per prevenire l'interazione con l'applicazione da parte di procedure automatiche.

La seguente tabella descrive una serie di eventi (vedi Capitolo 6, Eventi, interceptor e gestione delle eccezioni) lanciati dalla sicurezza di Seam in corrispondenza di determinati eventi relativi alla sicurezza.


A volte può essere necessario eseguire determinate operazioni con dei privilegi più elevati, come creare un nuovo utente da parte di un utente non autenticato. La sicurezza di Seam gestisce un tale meccanismo tramite la classe RunAsOperation. Questa classe consente sia al Principal o al Subject, che ai ruoli dell'utente di essere sovrascritti per un singolo insieme di operazioni.

Il seguente codice di esempio mostra come viene usato RunAsOperation, chiamando il suo metodo addRole() per fornire un insieme di ruoli fittizi solo per la durata dell'operazione. Il metodo execute() contiene il codice che verrà eseguito con i privilegi aggiuntivi.

    new RunAsOperation() {       

       public void execute() {
          executePrivilegedOperation();
       }         
    }.addRole("admin")
     .run();

In modo analogo, i metodi getPrincipal() o getSubject() possono anch'essi essere sovrascritti per specificare le istanze di Principal e Subject da usare per la durata dell'operazione. Infine, il metodo run() è usato per eseguire la RunAsOperation.

A volte può essere necessario estendere il componente Identity se l'applicazione ha particolari requisiti di sicurezza. Il seguente esempio (fatto di proposito, dato che le credenziali sarebbero normalmente gestite dal componente Credentials) mostra un componente Identity esteso con un campo companyCode aggiuntivo. La precendenza di installazione impostata a APPLICATION assicura che questo Identity esteso venga installato al posto dell'Identity originale.

@Name("org.jboss.seam.security.identity")

@Scope(SESSION)
@Install(precedence = APPLICATION)
@BypassInterceptors
@Startup
public class CustomIdentity extends Identity
{
   private static final LogProvider log = Logging.getLogProvider(CustomIdentity.class);
   private String companyCode;
   public String getCompanyCode()
   {
      return companyCode;
   }
   public void setCompanyCode(String companyCode)
   {
      this.companyCode = companyCode;
   }
   @Override
   public String login()
   {
      log.info("###### CUSTOM LOGIN CALLED ######");
      return super.login();
   }
}

OpenID è un standard comune per l'autenticazione esterna sul web. L'idea fondamentale è che qualsiasi applicazione web possa integrare (o sostituire) la sua gestione locale dell'autenticazione delegandone la responsabilità ad un server OpenID esterno scelto dall'utente. Questo va a beneficio dell'utente che non deve più ricordare un nome e una password per ogni applicazione web che usa, e dello sviluppatore, che viene sollevato di un po' di problemi nel manutenere un complesso sistema di autenticazione.

Quando si usa OpenID, l'utente sceglie un fornitore OpenID, e il fornitore OpenID assegna all'utente un OpenID. Questo id prende la forma di un URL, ad esempio http://grandepizza.myopenid.com, comunque è accettabile trascurare la parte http:// dell'identificativo quando si fa il login ad un sito. L'applicazione web (detta relying party in termini OpenID) determina quale server OpenID deve contattare e redirige l'utente a quel sito per l'autenticazione. Dopo essersi autenticato con esito positivo, all'utente viene fornito un codice (crittograficamente sicuro) che prova la sua identità e viene rediretto di nuovo all'applicazione web originale. L'applicazione web locale può essere quindi sicura che l'utente che sta accedendo è il proprietario dell'OpenID che aveva fornito.

E' importante rendersi conto, a questo punto, che l'autenticazione non implica l'autorizzazione. L'applicazione web ha ancora bisogno di fare delle considerazioni su come usare quell'informazione. L'applicazione web potrebbe trattare l'utente come immediatamente autenticato e dargli/le pieno accesso al sistema, oppure potrebbe tentare di associare l'OpenID fornito ad un utente locale, chiedendo all'utente di registrarsi se non l'ha già fatto. La scelta su come gestire l'OpenID è lasciata ad una decisione progettuale dell'applicazione locale.