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. Permission Management
15.7.1. PermissionManager
15.7.2. Permission checks for PermissionManager operations
15.8. SSL Security
15.8.1. Overriding the default ports
15.9. CAPTCHA
15.9.1. Configuring the CAPTCHA Servlet
15.9.2. Adding a CAPTCHA to a form
15.9.3. Customising the CAPTCHA algorithm
15.10. Security Events
15.11. Run As
15.12. Extending the Identity component
15.13. OpenID
15.13.1. Configuring OpenID
15.13.2. Presenting an OpenIdDLogin form
15.13.3. Logging in immediately
15.13.4. Deferring login
15.13.5. Logging 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'autentifica degli utenti. Comunque, per requisiti di autentifica meno complessi, Seam offre un metodo di autentifica 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'autentifica 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 autentifica usando le classi entità che sono fornite dall'applicazione o, in alternativa, di esegure l'autentifica con qualche altro fornitore di terze parti. Per configurare questa forma semplificata di autentifica è richiesto di 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.1.xsd
                 http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.1.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 Identiy.instance().getCredentials()). Tutti i ruoli di cui l'utente è membro devono essere assegnati usando Identity.addRole(). Ecco un esempio completo di un metodo di autentifica 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'autentifica è 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 autentifica, 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 autentifica 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 è un 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 un 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 autentifica, 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'autentifica Digest, impostarlo a digest. Se si usa l'autentifica 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, altrimenti 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'autentifica 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 relaivi 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. Se sia un metodo che la classe in cui è 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.

Una degli indici di un'interfaccia utente ben progettata è quando agli utenti non vengono presentate opzioni per le quali non hanno i permessi necessari per usarle. 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 che sono 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"
>Nome</f:facet>
        #{cl.name}
    </h:column>
    <h:column>
        <f:facet name="header"
>Citt�</f:facet>
        #{cl.city}
    </h:column>
    <h:column>
        <f:facet name="header"
>Azione</f:facet>
        <s:link value="Modifica cliente" action="#{clientAction.modify}"
                rendered="#{s:hasPermission(cl,'modifica')"/>
        <s:link value="Cancella cliente" action="#{clientAction.delete}"
                rendered="#{s:hasPermission(cl,'cancella')"/>
    </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 è 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 fornire nel package 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 in 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

As they are cached in the user's session, any custom PermissionResolver implementations must adhere to a couple of restrictions. Firstly, they may not contain any state that is finer-grained than session scope (and the scope of the component itself should either be application or session). Secondly, they must not use dependency injection as they may be accessed from multiple threads simultaneously. In fact, for performance reasons it is recommended that they are annotated with @BypassInterceptors to bypass Seam's interceptor stack altogether.

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.

The default ResolverChain consists of all permission resolvers discovered during application deployment. The org.jboss.seam.security.defaultResolverChainCreated event is raised (and the ResolverChain instance passed as an event parameter) when the default ResolverChain is created. This allows additional resolvers that for some reason were not discovered during deployment to be added, or for resolvers that are in the chain to be re-ordered or removed.

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. Per difetto si aspetta che questa base di regole sia 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.1.xsd
                   http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.1.xsd
                   http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-2.1.xsd"
                   http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.1.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 package. Un package in Drools è essenzialmente una collezione di regole. Il nome del package 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 package 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 per la regola si applichi. 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 il controllo di permesso successivo avvenga, 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 autentifica.

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 una 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).

A permission store is required for PersistentPermissionResolver to connect to the backend storage where permissions are persisted. Seam provides one PermissionStore implementation out of the box, JpaPermissionStore, which is used to store permissions inside a relational database. It is possible to write your own permission store by implementing the PermissionStore interface, which defines the following methods:

Tabella 15.8. PermissionStore interface

Tipo restituito

Metodo

Descrizione

List<Permission>

listPermissions(Object target)

This method should return a List of Permission objects representing all the permissions granted for the specified target object.

List<Permission>

listPermissions(Object target, String action)

This method should return a List of Permission objects representing all the permissions with the specified action, granted for the specified target object.

List<Permission>

listPermissions(Set<Object> targets, String action)

This method should return a List of Permission objects representing all the permissions with the specified action, granted for the specified set of target objects.

boolean

grantPermission(Permission)

This method should persist the specified Permission object to the backend storage, returning true if successful.

boolean

grantPermissions(List<Permission> permissions)

This method should persist all of the Permission objects contained in the specified List, returning true if successful.

boolean

revokePermission(Permission permission)

This method should remove the specified Permission object from persistent storage.

boolean

revokePermissions(List<Permission> permissions)

This method should remove all of the Permission objects in the specified list from persistent storage.

List<String>

listAvailableActions(Object target)

This method should return a list of all the available actions (as Strings) for the class of the specified target object. It is used in conjunction with permission management to build the user interface for granting specific class permissions (see section further down).


This is the default PermissionStore implementation (and the only one provided by Seam), which uses a relational database to store permissions. Before it can be used it must be configured with either one or two entity classes for storing user and role permissions. These entity classes must be annotated with a special set of security annotations to configure which properties of the entity correspond to various aspects of the permissions being stored.

If you wish to use the same entity (i.e. a single database table) to store both user and role permissions, then only the user-permission-class property is required to be configured. If you wish to use separate tables for storing user and role permissions, then in addition to the user-permission-class property you must also configure the role-permission-class property.

For example, to configure a single entity class to store both user and role permissions:


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

To configure separate entity classes for storing user and role permissions:


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

As mentioned, the entity classes that contain the user and role permissions must be configured with a special set of annotations, contained within the org.jboss.seam.annotations.security.permission package. The following table lists each of these annotations along with a description of how they are used:

Tabella 15.9. Entity Permission annotations

Annotazione

Target

Descrizione

@PermissionTarget

FIELD,METHOD

This annotation identifies the property of the entity that will contain the permission target. The property should be of type java.lang.String.

@PermissionAction

FIELD,METHOD

This annotation identifies the property of the entity that will contain the permission action. The property should be of type java.lang.String.

@PermissionUser

FIELD,METHOD

This annotation identifies the property of the entity that will contain the recipient user for the permission. It should be of type java.lang.String and contain the user's username.

@PermissionRole

FIELD,METHOD

This annotation identifies the property of the entity that will contain the recipient role for the permission. It should be of type java.lang.String and contain the role name.

@PermissionDiscriminator

FIELD,METHOD

This annotation should be used when the same entity/table is used to store both user and role permissions. It identifies the property of the entity that is used to discriminate between user and role permissions. By default, if the column value contains the string literal user, then the record will be treated as a user permission. If it contains the string literal role, then it will be treated as a role permission. It is also possible to override these defaults by specifying the userValue and roleValue properties within the annotation. For example, to use u and r instead of user and role, the annotation would be written like this:

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

Here is an example of an entity class that is used to store both user and role permissions. The following class can be found inside the SeamSpace example:



@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;
   }
}          
          

As can be seen in the above example, the getDiscriminator() method has been annotated with the @PermissionDiscriminator annotation, to allow JpaPermissionStore to determine which records represent user permissions and which represent role permissions. In addition, it can also be seen that the getRecipient() method is annotated with both @PermissionUser and @PermissionRole annotations. This is perfectly valid, and simply means that the recipient property of the entity will either contain the name of the user or the name of the role, depending on the value of the discriminator property.

When storing or looking up permissions, JpaPermissionStore must be able to uniquely identify specific object instances to effectively operate on its permissions. To achieve this, an identifier strategy may be assigned to each target class for the generation of unique identifier values. Each identifier strategy implementation knows how to generate unique identifiers for a particular type of class, and it is a simple matter to create new identifier strategies.

The IdentifierStrategy interface is very simple, declaring only two methods:

public interface IdentifierStrategy {

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

The first method, canIdentify() simply returns true if the identifier strategy is capable of generating a unique identifier for the specified target class. The second method, getIdentifier() returns the unique identifier value for the specified target object.

Seam provides two IdentifierStrategy implementations, ClassIdentifierStrategy and EntityIdentifierStrategy (see next sections for details).

To explicitly configure a specific identifier strategy to use for a particular class, it should be annotated with org.jboss.seam.annotations.security.permission.Identifier, and the value should be set to a concrete implementation of the IdentifierStrategy interface. An optional name property can also be specified, the effect of which is dependent upon the actual IdentifierStrategy implementation used.

This identifier strategy is used to generate unique identifiers for entity beans. It does so by concatenating the entity name (or otherwise configured name) with a string representation of the primary key value of the entity. The rules for generating the name section of the identifier are similar to ClassIdentifierStrategy. The primary key value (i.e. the id of the entity) is obtained using the PersistenceProvider component, which is able to correctly determine the value regardless of which persistence implementation is used within the Seam application. For entities not annotated with @Entity, it is necessary to explicitly configure the identifier strategy on the entity class itself, for example:

@Identifier(value = EntityIdentifierStrategy.class)

public class Customer { 

For an example of the type of identifier values generated, assume we have the following entity class:

@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; }
}

For a Customer instance with an id value of 1, the value of the identifier would be "Customer:1". If the entity class is annotated with an explicit identifier name, like so:

@Entity

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

Then a Customer with an id value of 123 would have an identifier value of "cust:123".

In much the same way that Seam Security provides an Identity Management API for the management of users and roles, it also provides a Permissions Management API for the management of persistent user permissions, via the PermissionManager component.

The PermissionManager component is an application-scoped Seam component that provides a number of methods for managing permissions. Before it can be used, it must be configured with a permission store (although by default it will attempt to use JpaPermissionStore if it is available). To explicitly configure a custom permission store, specify the permission-store property in components.xml:



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

The following table describes each of the available methods provided by PermissionManager:


Seam includes basic support for serving sensitive pages via the HTTPS protocol. This is easily configured by specifying a scheme for the page in pages.xml. The following example shows how the view /login.xhtml is configured to use HTTPS:


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

This configuration is automatically extended to both s:link and s:button JSF controls, which (when specifying the view) will also render the link using the correct protocol. Based on the previous example, the following link will use the HTTPS protocol because /login.xhtml is configured to use it:


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

Browsing directly to a view when using the incorrect protocol will cause a redirect to the same view using the correct protocol. For example, browsing to a page that has scheme="https" using HTTP will cause a redirect to the same page using HTTPS.

It is also possible to configure a default scheme for all pages. This is useful if you wish to use HTTPS for a only few pages. If no default scheme is specified then the normal behavior is to continue use the current scheme. So once the user accessed a page that required HTTPS, then HTTPS would continue to be used after the user navigated away to other non-HTTPS pages. (While this is good for security, it is not so great for performance!). To define HTTP as the default scheme, add this line to pages.xml:


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

Of course, if none of the pages in your application use HTTPS then it is not required to specify a default scheme.

You may configure Seam to automatically invalidate the current HTTP session each time the scheme changes. Just add this line to components.xml:


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

This option helps make your system less vulnerable to sniffing of the session id or leakage of sensitive data from pages using HTTPS to other pages using HTTP.

Though strictly not part of the security API, Seam provides a built-in CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) algorithm to prevent automated processes from interacting with your application.

The following table describes a number of events (see Capitolo 6, Eventi, interceptor e gestione delle eccezioni) raised by Seam Security in response to certain security-related events.


Sometimes it may be necessary to perform certain operations with elevated privileges, such as creating a new user account as an unauthenticated user. Seam Security supports such a mechanism via the RunAsOperation class. This class allows either the Principal or Subject, or the user's roles to be overridden for a single set of operations.

The following code example demonstrates how RunAsOperation is used, by calling its addRole() method to provide a set of roles to masquerade as for the duration of the operation. The execute() method contains the code that will be executed with the elevated privileges.

    new RunAsOperation() {       

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

In a similar way, the getPrincipal() or getSubject() methods can also be overriden to specify the Principal and Subject instances to use for the duration of the operation. Finally, the run() method is used to carry out the RunAsOperation.

Sometimes it might be necessary to extend the Identity component if your application has special security requirements. The following example (contrived, as credentials would normally be handled by the Credentials component instead) shows an extended Identity component with an additional companyCode field. The install precendence of APPLICATION ensures that this extended Identity gets installed in preference to the built-in Identity.

@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 is a community standard for external web-based authentication. The basic idea is that any web application can supplement (or replace) its local handling of authentication by delegating responsibility to an external OpenID server of the user's chosing. This benefits the user, who no longer has to remember a name and password for every web application he uses, and the developer, who is relieved of some of the burden of maintaining a complex authentication system.

When using OpenID, the user selects an OpenID provider, and the provider assigns the user an OpenID. The id will take the form of a URL, for example http://maximoburrito.myopenid.com however, it's acceptable to leave off the http:// part of the identifier when logging into a site. The web application (known as a relying party in OpenID-speak) determines which OpenID server to contact and redirects the user to the remote site for authentication. Upon successful authentication the user is given the (cryptographically secure) token proving his identity and is redirected back to the original web application.The local web application can then be sure the user accessing the application controls the OpenID he presented.

It's important to realize at this point that authentication does not imply authorization. The web application still needs to make a determination of how to use that information. The web application could treat the user as instantly logged in and give full access to the system or it could try and map the presented OpenID to a local user account, prompting the user to register if he hasn't already. The choice of how to handle the OpenID is left as a design decision for the local application.