SeamFramework.orgCommunity Documentation
Le API della sicurezza di Seam forniscono una serie di caratteristiche relative alla sicurezza di un'applicazione basata su Seam, coprendo le seguenti aree:
Autenticazione - uno strato estensibile, basato su JAAS che consente all'utente di autenticarsi con qualsiasi fornitore di servizi di sicurezza.
Gestione delle identità - una API per gestire a run time gli utenti e i ruoli di una applicazione Seam.
Autorizzazione - un framework di autorizzazione estremamente comprensibile, che gestisce i ruoli degli utenti, i permessi persistenti oppure basati sulle regole e un risolutore di permessi modulare che consente di implementare facilmente una logica personalizzata per la gestione della sicurezza.
Gestione dei permessi - un insieme di componenti Seam predefiniti che consente una gestione facile delle politiche di sicurezza dell'applicazione.
Gestione dei CAPTCHA - per assistere nella prevenzione dagli attacchi automatici tramite software o script verso un sito basato su Seam.
E molto altro
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:
Sicurezza delle entità
Intercettore della sicurezza in Hibernate
Intercettore della sicurezza in Seam
Restrizioni sulle pagine
Integrazione con la sicurezza delle API Servlet
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.
Nel caso si utilizzino le funzioni di gestione delle identità di Seam (discusse più avanti in questo capitolo) non è necessario creare un componente Authenticator (e si può saltare questo paragrafo).
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.
Nella scrittura di metodo di autenticazione è importante ridurlo al minimo e libero da ogni effetto collaterale. Il motivo è che non c'è garanzia sul numero di volte che il metodo di autentifica può essere chiamato dalle API della sicurezza, di conseguenza esso potrebbe essere invocato più volte durante una singola richiesta. Perciò qualsiasi codice che si vuole eseguire in seguito ad una autentifica fallita o completata con successo dovrebbe essere scritto implementando un observer. Vedi il paragrafo sugli Eventi di Sicurezza più avanti in questo capitolo per maggiori informazioni su quali eventi sono emessi dalla gestione della sicurezza Seam.
Il metodo Identity.addRole() si comporta in modo diverso a seconda che la sessione corrente sia autenticata o meno. Se la sessione non è autenticata, allora addRole() dovrebbe essere chiamato solo durante il processo di autenticazione. Quando viene chiamato in questo contesto, il nome del ruolo è messo in una lista temporanea di ruoli pre autenticati. Una volta che l'autentifica è completata i ruoli pre autenticati diventano ruoli "reali" e chiamando Identity.hasRole() per questi ruoli si otterrà true. Il seguente diagramma di sequenza rappresenta la lista dei ruoli pre autenticati come oggetto in primo piano per mostrare più chiaramente come si inserisce nel processo di autentifica.

Se la sessione corrente è già autenticata, allora la chiamata Identity.addRole() avrà l'effetto atteso di concedere immediatamente il ruolo specificato all'utente corrente.
Supponiamo, ad esempio, che in seguito ad un accesso concluso con successo debbano essere aggiornate certe statistiche relative all'utente. Questo può essere fatto scrivendo un observer per l'evento org.jboss.seam.security.loginSuccessful, come questo:
@In UserStats userStats;
@Observer("org.jboss.seam.security.loginSuccessful")
public void updateUserStats()
{
userStats.setLastLoginDate(new Date());
userStats.incrementLoginCount();
}
Questo metodo observer può essere messo ovunque, anche nello stesso componente Authenticator. E' possibile trovare maggiori informazioni sugli eventi relativi alla sicurezza più avanti in questo capitolo.
Il componente credentials fornisce sia la proprietà username che la password, soddisfacendo lo scenario di autenticazione più comune. Queste proprietà possono essere collegate direttamente ai campi username e password di una form di accesso. Una volta che queste proprietà sono impostate, chiamando identity.login() si otterrà l'autentifica dell'utente usando le credenziali fornite. Ecco un esempio di una semplice form di accesso:
<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}"/>
</div>
<div>
<h:commandButton value="Accedi" action="#{identity.login}"/>
</div
>
Allo stesso modo, l'uscita dell'utente viene fatta chiamando #{identity.logout}. La chiamata di questa azione cancellerà lo stato della sicurezza dell'utente correntemente autenticato e invaliderà la sessione dell'utente.
Riepilogando, ci sono tre semplici passi per configurare l'autenticazione:
Configurare un metodo di autenticazione in components.xml.
Scrivere un metodo di autenticazione.
Scrivere una form di accesso così che l'utente possa autenticarsi.
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.
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:
NotLoggedInException - Questa eccezione viene lanciata se l'utente tenta di accedere ad un'azione o ad una pagina protetta quando non ha fatto l'accesso.
AuthorizationException - Questa eccezione viene lanciata solo se l'utente ha già fatto l'accesso e ha tentato di accedere ad un'azione o ad una pagina per la quale non ha i privilegi necessari.
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
>
Non è che una banale semplificazione rispetto alla gestione dell'eccezione illustrata prima, ma probabilmente dovrà essere usata insieme ad essa.
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.
Se si usa l'autenticazione Digest, la classe authenticator deve estendere la classe astratta org.jboss.seam.security.digest.DigestAuthenticator e usare il metodo validatePassword() per validare la password in chiaro dell'utente con la richiesta Digest. Ecco un esempio:
public boolean authenticate()
{
try
{
User user = (User) entityManager.createQuery(
"from User where username = :username")
.setParameter("username", identity.getUsername())
.getSingleResult();
return validatePassword(user.getPassword());
}
catch (NoResultException ex)
{
return false;
}
}
Questo paragrafo esplora alcune delle caratteristiche avanzate fornite dalle API di sicurezza per affrontare requisiti di sicurezza più complessi.
Se non si vuole usare la configurazione JAAS semplificata fornita dalle API di sicurezza di Seam, è possibile delegare alla configurazione JAAS di default del sistema fornendo una proprietà jaas-config-name in components.xml. Ad esempio, se si sta usando JBoss AS e si vuole usare la politica other (la quale usa il modulo di login UsersRolesLoginModule fornito da JBoss AS), allora la voce da mettere in components.xml sarà simile a questa:
<security:identity jaas-config-name="other"/>
E' il caso di tenere ben presente che facendo in questo modo non significa che l'utente verrà autenticato in qualsiasi container in cui venga eseguita l'applicazione Seam. Questa configurazione istruisce semplicemente la sicurezza di Seam ad autenticarsi usando le politiche di sicurezza JAAS configurate.
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.
JpaIdentityStore richiede che siano configurate sia la proprietà user-class che role-class. Queste proprietà devono riferirsi a classi entità che servono per memorizzare i record relativi agli utente e ai ruoli, rispettivamente. Il seguente esempio illustra la configurazione di components.xml nell'applicazione di esempio SeamSpace:
<security:jpa-identity-store
user-class="org.jboss.seam.example.seamspace.MemberAccount"
role-class="org.jboss.seam.example.seamspace.MemberRole"/>
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.
Tabella 15.1. Annotazioni per l'entità utente
|
Annotazione |
Stato |
Descrizione |
|---|---|---|
|
|
Richiesta |
Questa annotazione contrassegna il campo o il metodo che contiene lo username dell'utente. |
|
|
Richiesta |
Questa annotazione contrassegna il campo o il metodo che contiene la password dell'utente. Consente di specificare un algoritmo di @UserPassword(hash = "md5") Se un'applicazione richiede un algoritmo di hash che non è supportato direttamente da Seam, è possibile estendere il componente |
|
|
Opzionale |
Questa annotazione contrassegna il campo o il metodo contenente il nome dell'utente. |
|
|
Opzionale |
Questa annotazione contrassegna il campo o il metodo contenente il cognome dell'utente. |
|
|
Opzionale |
Questa annotazione contrassegna il campo o il metodo contenente lo stato di abilitazione dell'utente. Questo deve essere una proprietà boolean e, se non presente, tutti gli utenti saranno considerati abilitati. |
|
|
Richiesta |
Questa annotazione contrassegna il campo o il metodo contenente i ruoli dell'utente. Questa proprietà verrà descritta in maggiore dettaglio successivamente. |
Tabella 15.2. Annotazioni per l'entità ruolo
|
Annotazione |
Stato |
Descrizione |
|---|---|---|
|
|
Richiesta |
Questa annotazione contrassegna il campo o il metodo contenente il nome del ruolo. |
|
|
Opzionale |
Questa annotazione contrassegna il campo o il metodo contenente i gruppi di appartenenza del ruolo. |
|
|
Opzionale |
Questa annotazione contrassegna il campo o il metodo che indica se il ruolo è condizionale o no. I ruoli condizionali verranno spiegati più avanti in questo capitolo. |
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.
Questo evento viene lanciato in corrispondenza della chiamata IdentityManager.createUser(). Subito prima che l'entità utente sia resa persistente sul database questo evento viene lanciato passando l'istanza dell'entità come parametro dell'evento. L'entità sarà un istanza di user-class configurata per JpaIdentityStore.
Scrivere un metodo che osserva questo evento può essere utile per impostare valori addizionali sui campi dell'entità che non vengono impostati nell'ambito delle funzionalità standard di createUser().
Anche questo evento viene lanciato in corrispondenza di IdentityMananger.createUser(). Però viene lanciato dopo che l'entità utente è già stata resa persistente sul database. Come per l'evento EVENT_PRE_PERSIST_USER, anche questo passa l'istanza dell'entità come un parametro dell'evento. Può essere utile osservare questo evento se c'è bisogno di rendere persistenti altre entità che fanno riferimento all'entità utente, ad esempio informazioni di dettaglio del contatto o altri dati specifici dell'utente.
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 |
|---|---|---|
|
|
|
L'indirizzo del server LDAP |
|
|
|
Il numero di porta su cui il server LDAP è in ascolto. |
|
|
|
Il Distinguished Name (DN) del contesto contenente le informazioni sugli utenti. |
|
|
|
Questo valore è usato come prefisso anteponendolo al nome utente durante la ricerca delle informazioni sull'utente. |
|
|
|
Questo valore è aggiunto alla fine del nome utente per ricercare le informazioni sull'utente. |
|
|
|
Il DN del contesto contenente le informazioni sui ruoli. |
|
|
|
Questo valore è usato come prefisso anteponendolo al nome del ruolo per formare il DN nella ricerca delle informazioni sul ruolo. |
|
|
|
Questo valore è aggiunto al nome del ruolo per formare il DN nella ricerca delle informazioni sul ruolo. |
|
|
|
Questo è il contesto usato per collegare il server LDAP. |
|
|
|
Queste sono le credenziali (la password) usate per collegare il server LDAP. |
|
|
|
Questo è il nome dell'attributo sulle informazioni dell'utente che contiene la lista dei ruoli di cui l'utente è membro. |
|
|
|
Questa proprietà boolean indica se l'attributo del ruolo nelle informazioni dell'utente è esso stesso un Distinguished Name. |
|
|
|
Indica quale attributo delle informazioni sull'utente contiene il nome utente. |
|
|
|
Indica quale attributo nelle informazioni sull'utente contiene la password dell'utente. |
|
|
|
Indica quale attributo nelle informazioni sull'utente contiene il nome proprio dell'utente. |
|
|
|
Indica quale attributo nelle informazioni sull'utente contiene il cognome dell'utente. |
|
|
|
Indica quale attributo nelle informazioni sull'utente contiene il nome per esteso dell'utente. |
|
|
|
Indica quale attributo nelle informazioni sull'utente determina se l'utente è abilitato. |
|
|
|
Indica quale attributo nell'informazioni sul ruolo contiene il nome del ruolo. |
|
|
|
Indica quale attributo determina la classe di un oggetto nella directory. |
|
|
|
Un elenco di classi di oggetto con cui devono essere create le informazioni su un nuovo ruolo. |
|
|
|
Un elenco di classi di oggetto con cui devono essere create le informazioni su un nuovo utente. |
La seguente configurazione di esempio mostra come LdapIdentityStore può essere configurato per una directory LDAP sul sistema immaginario directory.mycompany.com. Gli utenti sono memorizzati all'interno di questa directory sotto il contesto ou=Person,dc=mycompany,dc=com e sono identificati usando l'attributo uid (che corrisponde al loro nome utente). I ruoli sono memorizzati nel loro contesto, ou=Roles,dc=mycompany,dc=com e referenziati dalla voce dell'utente tramite l'attributo roles. Le voci dei ruoli sono identificate tramite il loro common name (l'attributo cn), che corrisponde al nome del ruolo. In questo esempio gli utenti possono essere disabilitati impostando il valoro del loro attributo enabled a false.
<security:ldap-identity-store
server-address="directory.mycompany.com"
bind-DN="cn=Manager,dc=mycompany,dc=com"
bind-credentials="secret"
user-DN-prefix="uid="
user-DN-suffix=",ou=Person,dc=mycompany,dc=com"
role-DN-prefix="cn="
role-DN-suffix=",ou=Roles,dc=mycompany,dc=com"
user-context-DN="ou=Person,dc=mycompany,dc=com"
role-context-DN="ou=Roles,dc=mycompany,dc=com"
user-role-attribute="roles"
role-name-attribute="cn"
user-object-classes="person,uidObject"
enabled-attribute="enabled"
/>
Scrivere la propria implementazione della memorizzazione delle identità consente di autenticare ed eseguire le operazioni di gestione delle identità su fornitori di sicurezza che non sono gestiti da Seam così com'è. Per ottenere ciò è richiesta una sola classe ed essa deve implementare l'interfaccia org.jboss.seam.security.management.IdentityStore.
Fare riferimento al JavaDoc di IdentityStore per una descrizione dei metodi che devono essere implementati.
Se in un'applicazione Seam si stanno usando le funzioni di gestione delle identità, allora non è richiesto di fornire un componente authenticator (vedi il precedente paragrafo Autenticazione) per abilitare l'autentifica. Basta omettere authenticator-method dalla configurazione di identity in components.xml e il SeamLoginModule userà per default IdentityManger per autenticare gli utenti dell'applicazione, senza nessuna configurazione speciale.
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 |
|---|---|---|
|
|
|
Crea un nuovo utente con il nome e la password specificate. Restituisce |
|
|
|
Elimina le informazioni dell'utente con il nome specificato. Restituisce |
|
|
|
Crea un nuovo ruolo con il nome specificato. Restituisce |
|
|
|
Elimina il ruolo con il nome specificato. Restituisce |
|
|
|
Abilita l'utente con il nome specificato. Gli utenti che non sono abilitati non sono in grado di autenticarsi. Restituisce |
|
|
|
Disabilita l'utente con il nome specificato. Restituisce |
|
|
|
Modifica la password dell'utente con il nome specificato. Restituisce |
|
|
|
Restituisce |
|
|
|
Concede il ruolo specificato all'utente o al ruolo. Il ruolo deve già esistere per essere concesso. Restituisce |
|
|
|
Revoca il ruolo specificato all'utente o al ruolo. Restituisce |
|
|
|
Restituisce |
|
|
|
Restituisce una lista di tutti i nomi utente in ordine alfanumerico. |
|
|
|
Restituisce una lista di tutti i nomi utente filtrata secondo il parametro di filtro specificato e in ordine alfanumerico. |
|
|
|
Restituisce una lista di tutti i nomi dei ruoli. |
|
|
|
Restituisce una lista dei nomi di tutti i ruoli esplicitamente concessi all'utente con il nome specificato. |
|
|
|
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 |
|
|
|
Autenticazione il nome utente e la password specificati usando l'Identity Store configurato. Restituisce |
|
|
|
Aggiunge il ruolo specificato come membro del gruppo specificato. Restituisce |
|
|
|
Rimuove il ruolo specificato dal gruppo specificato. Restituisce |
|
|
|
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.
Tabella 15.5. Permessi di sicurezza nella gestione delle identità
|
Metodo |
Oggetto del permesso |
Azione del permesso |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
Tabella 15.6. Chiavi dei messaggi di sicurezza
|
Chiave del messaggio |
Descrizione |
|---|---|
|
|
Questo messaggio viene prodotto quando un utente porta a buon fine un login tramite le API di sicurezza. |
|
|
Questo messaggio viene prodotto quando il processo di login fallisce, perché il nome utente e la password forniti dall'utente non sono corretti, oppure perché l'autenticazione è fallita per qualche altro motivo. |
|
|
Questo messaggio viene prodotto quando un utente tenta di eseguire un'azione o di accedere ad una pagina che richiede un controllo di sicurezza e l'utente non è al momento autenticato. |
|
|
Questo messaggio viene prodotto quando un utente che è già autenticato tenta di eseguire di nuovo il login. |
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.
Un ruolo è un gruppo, o un tipo, di utente al quale possono essere concessi certi privilegi per eseguire una o più azioni specifiche nell'ambito dell'applicazione. Essi sono dei semplici costrutti consistenti solo di un nome che "amministratore", "utente", "cliente", ecc. Possono sia essere concessi ad un utente (o in alcuni casi ad altri ruoli) che essere usati per creare gruppi logici di utenti per facilitare l'assegnazione di determinati privilegi dell'applicazione.

Un permesso è un privilegio (a volte una-tantum) per eseguire una singola, specifica azione. E' del tutto possibile costruire un'applicazione usando nient'altro che i privilegi, comunque i ruoli offrono un livello di facilitazione più alto quando si tratta di concedere dei privilegi a gruppi di utenti. Essi sono leggermente più complessi nella struttura rispetto ai ruoli ed essenzialmente consistono di tre "aspetti": un obiettivo , un'azione e un destinatario. L'obiettivo di un permesso è l'oggetto (o un nome arbitrario o una classe) per il quale è consentito di eseguire una determinata azione da parte di uno specifico destinatario (o utente). Ad esempio, l'utente "Roberto" può avere il permesso di cancellare gli oggetti cliente. In questo caso l'obiettivo del permesso può essere "clienti", l'azione del permesso sarà "cancella" e il recipiente sarà "Roberto".

Nell'ambito di questa documentazione i permessi sono generalmente rappresentati nella forma obiettivo:azione (omettendo il destinatario, benché nella realtà sarà sempre richiesto).
Iniziamo ad esaminare la forma più semplice di autorizzazione, la sicurezza dei componenti, inziando con l'annotazione @Restrict.
Benché l'uso dell'annotazione @Restrict fornisca un metodo flessibile e potente per rendere sicuri i componenti grazie alla sua possibilità di gestire le espressione EL, è consigliabile usare l'equivalente tipizzato (descritto più avanti), se non altro per la sicurezza a livello di compilazione che fornisce.
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.
A volte può risultare desiderabile eseguire un controllo di sicurezza nel codice, senza usare l'annotazione @Restrict. In questa situazione basta usare semplicemente Identity.checkRestriction() per risolvere l'espressione di sicurezza, così:
public void deleteCustomer() {
Identity.instance().checkRestriction("#{s:hasPermission(selectedCustomer,'delete')}");
}
Se l'espressione specificata non risolve a true, allora
se l'utente non ha eseguito l'accesso, l'eccezione NotLoggedInException viene lanciata, oppure
se l'utente ha eseguito l'accesso, viene lanciata un'eccezione AuthorizationException.
E' anche possibile chiamare i metodi hasRole() e hasPermission() direttamente dal codice Java:
if (!Identity.instance().hasRole("amministratore"))
throw new AuthorizationException("Devi essere un amministratore per eseguire questa azione");
if (!Identity.instance().hasPermission("cliente", "crea"))
throw new AuthorizationException("Non puoi creare nuovi clienti");
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 delle pagine richiede che l'applicazione usi un file pages.xml. Comunque è molto semplice da configurare. Basta includere un elemento >restrict< all'interno degli elementi page che si vogliono rendere sicuri. Se tramite l'elemento restrict non viene indicata esplicitamente una restrizione, verrà controllato implicitamente il permesso /viewId.xhtml:render quando la richiesta della pagina avviene in modo non-faces (GET), e il permesso/viewId.xhtml:restore quando un JSF postback (il submit della form) viene originato dalla pagina. Altrimenti viene la restrizione specificata verrà valutata come una normale espressione di sicurezza. Ecco un paio di esempi:
<page view-id="/settings.xhtml">
<restrict/>
</page
>
Questa pagina richiede implicitamente un permesso /settings.xhtml:render per le richieste non-faces e un permesso /settings.xhtml:restore per le richieste faces.
<page view-id="/reports.xhtml">
<restrict
>#{s:hasRole('amministratore')}</restrict>
</page
>
Sia le richieste faces che quelle non-faces a questa pagina richiedono che l'utente sia membro del ruolo amministratore.
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):
@PostLoad - Chiamato dopo che l'istana di una entità viene caricata dal database. Usare questo metodo per configurare un permesso read.
@PrePersist - Chiamato prima che una nuova istanza dell'entità sia inserita. Usare questo metodo per configurare un permesso insert.
@PreUpdate - Chiamato prima che un'entità sia aggiornata. Usare questo metodo per configurare un permesso update.
@PreRemove - Chiamato prima che un'entità venga cancellata. Usare questo metodo per configuare un permesso delete.
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() {}
/META-INF/orm.xmlE' anche possibile specificare i metodi callback in /META-INF/orm.xml:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
version="1.0">
<entity class="Customer">
<pre-persist method-name="prePersist" />
</entity>
</entity-mappings
>
Ovviamente c'è sempre bisogno di annotare il metodo prePersist() in Customer con @Restrict.
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.
I controlli di sicurezza sugli entity bean EJB3 sono eseguiti con un EntityListener. E' possibile installare questo listener usando il seguente file META-INF/orm.xml:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
version="1.0">
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="org.jboss.seam.security.EntitySecurityListener"/>
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings
>
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:
@Insert
@Read
@Update
@Delete
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")In aggiunta alla gestione tipizzata delle annotazioni sui permessi, la sicurezza di Seam fornisce anche le annotazioni tipizzate per i ruoli che consentono di limitare l'accesso ai metodi dei componenti in base all'appartenenza ad un ruolo dell'utente attualmente autenticato. Seam fornisce una di queste annotazioni già fatta, org.jboss.seam.annotations.security.Admin, usata per limitare l'accesso ad un metodo agli utenti che sono membri del ruolo admin (purché l'applicazione gestisca un tale ruolo). Per creare le proprie annotazioni per i ruoli basta meta-annotarle con org.jboss.seam.annotations.security.RoleCheck, come nel seguente esempio:
@Target({METHOD})
@Documented
@Retention(RUNTIME)
@Inherited
@RoleCheck
public @interface User {
}Qualsiasi metodo successivamente annotato con l'annotazione @User come mostrata nell'esempio precedente, sarà automaticamente intercettato e sarà verificata l'appartenenza dell'utente al ruolo con il nome corrispondente (che è la versione a lettere minuscole del nome dell'annotazione, in questo caso user).
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:
RuleBasedPermissionResolver - Questo risolutore di permessi usa Drools per risolvere i controlli di permesso basati sulle regole.
PersistentPermissionResolver - Questo risolutore di permessi memorizza gli oggetti permesso in un dispositivo persistente, come un database relazionale.
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.
Tabella 15.7. L'interfaccia PermissionResolver
|
Tipo restituito |
Metodo |
Descrizione |
|---|---|---|
|
|
|
Questo metodo deve stabilire se l'utente attualmente autenticato (ottenuto tramite una chiamata a |
|
|
|
Questo metodo deve rimuovere dall'insieme specificato tutti gli oggetti per i quali si otterrebbe |
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:

1. Una verifica di permesso viene iniziata da qualche parte (dal codice o tramite un'espressione EL) provocando una chiamata a Identity.hasPermission().
1.1. Identity chiama PermissionMapper.resolvePermission(), passando il permesso che deve essere risolto.
1.1.1. PermissionMapper conserva una Map di istanze di ResolverChain, indicizzate per classe. Usa questa mappa per identificare la giusta ResolverChain per l'oggetto obiettivo del permesso. Una volta che ha la giusta ResolverChain, recupera l'elenco dei PermissionResolver che contiene tramite una chiamata a ResolverChain.getResolvers().
1.1.2. Per ciascun PermissionResolver nel ResolverChain, il PermissionMapper chiama il suo metodo hasPermission(), passando l'istanza del permesso da verificare. Se qualcuno dei PermissionResolver restituisce true, allora la verifica del permesso ha avuto successo e il PermissionMapper restituisce anch'esso true a Identity. Se nessuno dei PermissionResolver restituisce true, allora la verifica del permesso è fallita.
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.
Se si usa la funzione dei permessi basati sulle regole fornita dalla sicurezza di Seam, Drools richiede che i seguenti file jar siano distribuiti insieme al progetto:
drools-compiler.jar
drools-core.jar
janino.jar
antlr-runtime.jar
mvel14.jar
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();
endDividiamolo 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.
Finora abbiamo visto solo controlli di permesso per obiettivi di tipo stringa. E' naturalmente possibile scrivere regole di sicurezza anche per obiettivi del permesso di tipo più complesso. Ad esempio, supponiamo che si voglia scrivere una regola di sicurezza che consenta agli utenti di creare un commento in un blog. La seguente regola mostra come questo possa essere espresso, richiedendo che l'obiettivo del controllo di permesso sia un'istanza di MemberBlog e anche che l'utente correntemente autenticato sia un membro del ruolo user:
rule CanCreateBlogComment no-loop activation-group "permissions" when blog: MemberBlog() check: PermissionCheck(target == blog, action == "create", granted == false) Role(name == "user") then check.grant(); end
E' possibile realizzare dei controlli di permesso (che consentono l'accesso a tutte le funzioni per un determinato obiettivo) basati su wildcard omettendo il vincolo action per il PermissionCheck nella regola, in questo modo:
rule CanDoAnythingToCustomersIfYouAreAnAdmin
when
c: PermissionCheck(target == "customer")
Role(name == "admin")
then
c.grant();
end;
Questa regola consente agli utenti con il ruolo admin di eseguire qualsiasi azione per qualsiasi controllo di permesso su customer.
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).
Before it can be used, PersistentPermissionResolver must be configured with a valid PermissionStore in components.xml. If not configured, it will attempt to use the default permission store, JpaIdentityStore (see section further down for details). To use a permission store other than the default, configure the permission-store property as follows:
<security:persistent-permission-resolver permission-store="#{myCustomPermissionStore}"/>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 |
|---|---|---|
|
|
|
This method should return a |
|
|
|
This method should return a |
|
|
|
This method should return a |
|
|
|
This method should persist the specified |
|
|
|
This method should persist all of the |
|
|
|
This method should remove the specified |
|
|
|
This method should remove all of the |
|
|
|
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 |
|---|---|---|
|
|
|
This annotation identifies the property of the entity that will contain the permission target. The property should be of type |
|
|
|
This annotation identifies the property of the entity that will contain the permission action. The property should be of type |
|
|
|
This annotation identifies the property of the entity that will contain the recipient user for the permission. It should be of type |
|
|
|
This annotation identifies the property of the entity that will contain the recipient role for the permission. It should be of type |
|
|
|
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 @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.
A further set of class-specific annotations can be used to configure a specific set of allowable permissions for a target class. These permissions can be found in the org.jboss.seam.annotation.security.permission package:
Tabella 15.10. Class Permission Annotations
|
Annotazione |
Target |
Descrizione |
|---|---|---|
|
|
|
A container annotation, this annotation may contain an array of |
|
|
|
This annotation defines a single allowable permission action for the target class. Its |
Here's an example of the above annotations in action. The following class can also be found in the SeamSpace example:
@Permissions({
@Permission(action = "view"),
@Permission(action = "comment")
})
@Entity
public class MemberImage implements Serializable {
This example demonstrates how two allowable permission actions, view and comment can be declared for the entity class MemberImage.
By default, multiple permissions for the same target object and recipient will be persisted as a single database record, with the action property/column containing a comma-separated list of the granted actions. To reduce the amount of physical storage required to persist a large number of permissions, it is possible to use a bitmasked integer value (instead of a comma-separated list) to store the list of permission actions.
For example, if recipient "Bob" is granted both the view and comment permissions for a particular MemberImage (an entity bean) instance, then by default the action property of the permission entity will contain "view,comment", representing the two granted permission actions. Alternatively, if using bitmasked values for the permission actions, as defined like so:
@Permissions({
@Permission(action = "view", mask = 1),
@Permission(action = "comment", mask = 2)
})
@Entity
public class MemberImage implements Serializable {
The action property will instead simply contain "3" (with both the 1 bit and 2 bit switched on). Obviously for a large number of allowable actions for any particular target class, the storage required for the permission records is greatly reduced by using bitmasked actions.
Obviously, it is very important that the mask values specified are powers of 2.
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 classes, and will use the value of the name (if specified) in the @Identifier annotation. If there is no name property provided, then it will attempt to use the component name of the class (if the class is a Seam component), or as a last resort it will create an identifier based on the name of the class (excluding the package name). For example, the identifier for the following class will be "customer":
@Identifier(name = "customer")
public class Customer {
The identifier for the following class will be "customerAction":
@Name("customerAction")
public class CustomerAction {
Finally, the identifier for the following class will be "Customer":
public class Customer { 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:
Tabella 15.11. PermissionManager API methods
|
Tipo restituito |
Metodo |
Descrizione |
|---|---|---|
|
|
|
Returns a list of |
|
|
|
Returns a list of |
|
|
|
Persists (grants) the specified |
|
|
|
Persists (grants) the specified list of |
|
|
|
Removes (revokes) the specified |
|
|
|
Removes (revokes) the specified list of |
|
|
|
Returns a list of the available actions for the specified target object. The actions that this method returns are dependent on the |
Invoking the methods of PermissionManager requires that the currently-authenticated user has the appropriate authorization to perform that management operation. The following table lists the required permissions that the current user must have.
Tabella 15.12. Permission Management Security Permissions
|
Metodo |
Oggetto del permesso |
Azione del permesso |
|---|---|---|
|
|
The specified |
|
|
|
The target of the specified |
|
|
|
The target of the specified |
|
|
|
Each of the targets of the specified list of |
|
|
|
The target of the specified |
|
|
|
Each of the targets of the specified list of |
|
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.
If you wish to configure the HTTP and HTTPS ports manually, they may be configured in pages.xml by specifying the http-port and https-port attributes on the pages element:
<pages xmlns="http://jboss.com/products/seam/pages"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.1.xsd"
no-conversation-view-id="/home.xhtml"
login-view-id="/login.xhtml"
http-port="8080"
https-port="8443"
>
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.
To get up and running, it is necessary to configure the Seam Resource Servlet, which will provide the Captcha challenge images to your pages. This requires the following entry in web.xml:
<servlet>
<servlet-name
>Seam Resource Servlet</servlet-name>
<servlet-class
>org.jboss.seam.servlet.SeamResourceServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name
>Seam Resource Servlet</servlet-name>
<url-pattern
>/seam/resource/*</url-pattern>
</servlet-mapping
>
Adding a CAPTCHA challenge to a form is extremely easy. Here's an example:
<h:graphicImage value="/seam/resource/captcha"/>
<h:inputText id="verifyCaptcha" value="#{captcha.response}" required="true">
<s:validate />
</h:inputText>
<h:message for="verifyCaptcha"/>
That's all there is to it. The graphicImage control displays the CAPTCHA challenge, and the inputText receives the user's response. The response is automatically validated against the CAPTCHA when the form is submitted.
You may customize the CAPTCHA algorithm by overriding the built-in component:
@Name("org.jboss.seam.captcha.captcha")
@Scope(SESSION)
public class HitchhikersCaptcha extends Captcha
{
@Override @Create
public void init()
{
setChallenge("What is the answer to life, the universe and everything?");
setCorrectResponse("42");
}
@Override
public BufferedImage renderChallenge()
{
BufferedImage img = super.renderChallenge();
img.getGraphics().drawOval(5, 3, 60, 14); //add an obscuring decoration
return img;
}
}
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.
Tabella 15.13. Security Events
|
Event Key |
Descrizione |
|---|---|
|
|
Raised when a login attempt is successful. |
|
|
Raised when a login attempt fails. |
|
|
Raised when a user that is already authenticated attempts to log in again. |
|
|
Raised when a security check fails when the user is not logged in. |
|
|
Raised when a security check fails when the user is logged in however doesn't have sufficient privileges. |
|
|
Raised just prior to user authentication. |
|
|
Raised just after user authentication. |
|
|
Raised after the user has logged out. |
|
|
Raised when the user's credentials have been changed. |
|
|
Raised when the Identity's rememberMe property is changed. |
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();
}
}
Note that an Identity component must be marked @Startup, so that it is available immediately after the SESSION context begins. Failing to do this may render certain Seam functionality inoperable in your application.
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.
Seam uses the openid4java package and requires four additional JARs to make use of the Seam integration. These are: htmlparser.jar, openid4java.jar, openxri-client.jar and openxri-syntax.jar.
OpenID processing requires the use of the OpenIdPhaseListener, which should be added to your faces-config.xml file. The phase listener processes the callback from the OpenID provider, allowing re-entry into the local application.
<lifecycle>
<phase-listener>org.jboss.seam.security.openid.OpenIdPhaseListener</phase-listener>
</lifecycle>
With this configuration, OpenID support is available to your application. The OpenID support component, org.jboss.seam.security.openid.openid, is installed automatically if the openid4java classes are on the classpath.
To initiate an OpenID login, you can present a simply form to the user asking for the user's OpenID. The #{openid.id} value accepts the user's OpenID and the #{openid.login} action initiates an authentication request.
<h:form>
<h:inputText value="#{openid.id}" />
<h:commandButton action="#{openid.login}" value="OpenID Login"/>
</h:form>
When the user submits the login form, he will be redirected to his OpenID provider. The user will eventually return to your application through the Seam pseudo-view /openid.xhtml, which is provided by the OpenIdPhaseListener. Your application can handle the OpenID response by means of a pages.xml navigation from that view, just as if the user had never left your application.
The simplest strategy is to simply login the user immediately. The following navigation rule shows how to handle this using the #{openid.loginImmediately()} action.
<page view-id="/openid.xhtml">
<navigation evaluate="#{openid.loginImmediately()}">
<rule if-outcome="true">
<redirect view-id="/main.xhtml">
<message>OpenID login successful...</message>
</redirect>
</rule>
<rule if-outcome="false">
<redirect view-id="/main.xhtml">
<message>OpenID login rejected...</message>
</redirect>
</rule>
</navigation>
</page>
Thie loginImmediately() action checks to see if the OpenID is valid. If it is valid, it adds an OpenIDPrincipal to the identity component, marks the user as logged in (i.e. #{identity.loggedIn} will be true) and returns true. If the OpenID was not validated, the method returns false, and the user re-enters the application un-authenticated. If the user's OpenID is valid, it will be accessible using the expression #{openid.validatedId} and #{openid.valid} will be true.
You may not want the user to be immediately logged in to your application. In that case, your navigation should check the #{openid.valid} property and redirect the user to a local registration or processing page. Actions you might take would be asking for more information and creating a local user account or presenting a captcha to avoid programmatic registrations. When you are done processing, if you want to log the user in, you can call the loginImmediately method, either through EL as shown previously or by directly interaction with the org.jboss.seam.security.openid.OpenId component. Of course, nothing prevents you from writing custom code to interact with the Seam identity component on your own for even more customized behaviour.
Logging out (forgetting an OpenID association) is done by calling #{openid.logout}. If you are not using Seam security, you can call this method directly. If you are using Seam security, you should continue to use #{identity.logout} and install an event handler to capture the logout event, calling the OpenID logout method.
<event type="org.jboss.seam.security.loggedOut">
<action execute="#{openid.logout}" />
</event>
It's important that you do not leave this out or the user will not be able to login again in the same session.