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'autenticazione degli utenti. Comunque, per requisiti di autenticazione meno complessi, Seam offre un metodo di autenticazione 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'autenticazione ad uno dei componenti dell'applicazione. Questo modulo di login è già configurato all'interno di Seam come parte dei criteri di gestione di default e in quanto tale non richiede alcun file di configurazione aggiuntivo. Esso consente di scrivere un metodo di autenticazione usando le classi entità che sono fornite dall'applicazione o, in alternativa, di esegure l'autenticazione con qualche altro fornitore di terze parti. Per configurare questa forma semplificata di autenticazione è richiesto di configurare il componente Identity
in components.xml
:
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:security="http://jboss.com/products/seam/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.2.xsd
http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.2.xsd">
<security:identity authenticate-method="#{authenticator.authenticate}"/>
</components
>
L'espressione EL #{authenticator.authenticate}
è la definizione di un metodo tramite la quale si indica che il metodo authenticate
del componente authenticator
verrà usato per autenticare l'utente.
La proprietà authenticate-method
specificata per identity
in components.xml
specifica quale metodo sarà usato dal SeamLoginModule
per autenticare l'utente. Questo metodo non ha parametri ed è previsto che restituisca un boolean, il quale indica se l'autenticazione ha avuto successo o no. Il nome utente e la password possono essere ottenuti da Credentials.getUsername()
e Credentials.getPassword()
rispettivamente (è possibile avere un riferimento al componente credentials
tramite Identity.instance().getCredentials()
). Tutti i ruoli di cui l'utente è membro devono essere assegnati usando Identity.addRole()
. Ecco un esempio completo di un metodo di autenticazione all'interno di un componente POJO:
@Name("authenticator")
public class Authenticator {
@In EntityManager entityManager;
@In Credentials credentials;
@In Identity identity;
public boolean authenticate() {
try {
User user = (User) entityManager.createQuery(
"from User where username = :username and password = :password")
.setParameter("username", credentials.getUsername())
.setParameter("password", credentials.getPassword())
.getSingleResult();
if (user.getRoles() != null) {
for (UserRole mr : user.getRoles())
identity.addRole(mr.getName());
}
return true;
}
catch (NoResultException ex) {
return false;
}
}
}
Nell'esempio precedente sia User
che UserRole
sono entity bean specifici dell'applicazione. Il parametro roles
è popolato con i ruoli di cui l'utente è membro, che devono essere aggiunti alla Set
come valori stringa, ad esempio "amministratore", "utente". In questo caso, se il record dell'utente non viene trovato e una NoResultException
viene lanciata, il metodo di autenticazione restituisce false
per indicare che l'autenticazione è fallita.
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 autenticazione 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 autenticazione 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'autenticazione è 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 autenticazione.
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'autenticazione 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 autenticazione, il solo cookie che un malintenzionato può prelevare tramite un attacco XSS è il cookie della sessione corrente dell'utente. Ciò significa che l'attacco funziona solo quando l'utente ha una sessione aperta, ovvero per un intervallo di tempo limitato. Al contrario è molto più allettante e pericoloso se un malintenzionato ha la possibilità di prelevare il cookie relativo alla funzione "Ricordami su questo computer", il quale gli consentirebbe di accedere senza autenticazione ogni volta che vuole. Notare che questo dipende anche da quanto è efficace la protezione del sito dagli attacchi XSS. Sta a chi scrive l'applicazione fare in modo che il sito sia sicuro al 100% dagli attacchi XSS, un obiettivo non banale per qualsiasi sito che consente di rappresentare sulle pagine un contenuto scritto dagli utenti.
I produttori di browser hanno riconosciuto questo problema e hanno introdotto la funzione "Ricorda la password", oggi disponibile su quasi tutti i browser. In questo caso il browser ricorda il nome utente e la password per un certo sito e dominio, e riempie la form di accesso automaticamente quando non è attiva una sessione con il sito. Se poi il progettista del sito offre una scorciatoia da tastiera conveniente, questo approccio è quasi altrettanto immediato come il cookie "Ricordami su questo computer", ma molto più sicuro. Alcuni browser (ad esempio Safari su OS X) memorizzano addirittura i dati delle form di accesso nel portachiavi cifrato di sistema. Oppure, in un ambiente di rete, il portachiavi può essere trasportato dall'utente (tra il portatile e il desktop, ad esempio), mentre i cookie del browser di solito non sono sincronizzati.
In definitiva: benché tutti lo stiano facendo, il cookie "Ricordami su questo computer" con l'autenticazione automatica è una cattiva pratica e non dovrebbe essere usata. I cookie che "ricordano" solo il nome dell'utente e riempiono la form di accesso con quel nome utente per praticità, non comportano rischi.
Per abilitare la funzione "Ricordami su questo computer" nella modalità di default (quella sicura, con il solo nome utente) non è richiesta alcuna speciale configurazione. Basta collegare un checkbox "Ricordami su questo computer" a rememberMe.enabled
nella form di accesso, come nel seguente esempio:
<div>
<h:outputLabel for="name" value="Nome utente"/>
<h:inputText id="name" value="#{credentials.username}"/>
</div>
<div>
<h:outputLabel for="password" value="Password"/>
<h:inputSecret id="password" value="#{credentials.password}" redisplay="true"/>
</div
>
<div class="loginRow">
<h:outputLabel for="rememberMe" value="Ricordami su questo computer"/>
<h:selectBooleanCheckbox id="rememberMe" value="#{rememberMe.enabled}"/>
</div
>
Per usare la modalità automatica, attraverso il token, della funzione "Ricordami su questo computer", occorre prima configurare la memorizzazione del token. Nello scenario più comune (gestito da Seam) questi token di autenticazione vengono memorizzati nel database, comunque è possibile implementare la propria memorizzazione dei token implementando l'interfaccia org.jboss.seam.security.TokenStore
. In questo paragrafo si suppone che per la memorizzazione dei token in una tabella del database si stia usando l'implementazione fornita con Seam JpaTokenStore
.
Il primo passo consiste nel creare una nuova entità che conterrà i token. Il seguente esempio mostra una possibile struttura che può essere usata:
@Entity
public class AuthenticationToken implements Serializable {
private Integer tokenId;
private String username;
private String value;
@Id @GeneratedValue
public Integer getTokenId() {
return tokenId;
}
public void setTokenId(Integer tokenId) {
this.tokenId = tokenId;
}
@TokenUsername
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@TokenValue
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
Come si può vedere dal listato, vengono usate un paio di annotazioni speciali, @TokenUsername
e @TokenValue
, per configurare le proprietà token e nome utente dell'entità. Queste annotazioni sono richieste per l'entità che conterrà i token di autenticazione.
Il passo successivo consiste nel configurare il JpaTokenStore
per usare questo entity bean per memorizzare e recuperare i token di autenticazione. Ciò viene fatto in components.xml
specificando l'attributo token-class
.
<security:jpa-token-store token-class="org.jboss.seam.example.seamspace.AuthenticationToken"/>
Una volta fatto questo, l'ultima cosa da fare è configurare anche il componente RememberMe
in components.xml
. La sua proprietà mode
dovrà essere impostata a autoLogin
:
<security:remember-me mode="autoLogin"/>
Questo è tutto ciò che è necessario. L'autenticazione automatica degli utenti avverrà quando torneranno a visitare il sito (purché abbiano impostato il checkbox "Ricordami su questo computer").
Per essere sicuri che gli utenti siano autenticati automaticamente quando tornano sul sito, il seguente codice deve essere posizionato in components.xml
:
<event type="org.jboss.seam.security.notLoggedIn">
<action execute="#{redirect.captureCurrentView}"/>
<action execute="#{identity.tryLogin()}"/>
</event>
<event type="org.jboss.seam.security.loginSuccessful">
<action execute="#{redirect.returnToCapturedView}"/>
</event
>
Per prevenire il fatto che gli utenti ricevano la pagina di errore di default in risposta ad un errore di sicurezza, si raccomanda che in pages.xml
sia configurata una redirezione degli errori di sicurezza ad una pagina più "carina". I due principali tipi di eccezione lanciati dalle API della sicurezza sono:
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 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 autenticazione, occorre abilitare il componente authentication-filter
in components.xml
:
<web:authentication-filter url-pattern="*.seam" auth-type="basic"/>
Per abilitare il filtro per l'autenticazione Basic impostare auth-type
a basic
, oppure per l'autenticazione Digest, impostarlo a digest
. Se si usa l'autenticazione Digest, occorre impostare anche un valore per key
e realm
:
<web:authentication-filter url-pattern="*.seam" auth-type="digest" key="AA3JK34aSDlkj" realm="La mia Applicazione"/>
key
può essere un qualunque valore stringa. realm
è il nome del dominio di autenticazione che viene presentato all'utente quando si autentica.
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.
If you are using the Identity Management features in your Seam application, then it is not required to provide an authenticator component (see previous Authentication section) to enable authentication. Simply omit the authenticate-method
from the identity
configuration in components.xml
, and the SeamLoginModule
will by default use IdentityManager
to authenticate your application's users, without any special configuration required.
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 relativi alla sicurezza. La seguente tabella elenca le chiavi dei messaggi che possono essere usate per sovrascrivere questi messaggi specificandoli in un file message.properties
. Per sopprimere un messaggio basta mettere nel file la chiave con un valore vuoto.
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 quale "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 destinatario 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
. Qualora sia un metodo sia la classe in cui questo è dichiarato sono annotati con @Restrict
, la restrizione sul metodo ha la precedenza (e la restrizione sulla classe non si applica). Se nell'invocazione di un metodo fallisce il controllo di sicurezza, viene lanciata un'eccezione come definito nel contratto di Identity.checkRestriction()
(vedi Restrizioni in linea). Una @Restrict
solo sulla classe del componente stesso è equivalente ad aggiungere @Restrict
a ciascuno dei suoi metodi.
Una @Restrict
vuota implica un controllo di permesso per nomeComponente:nomeMetodo
. Prendiamo ad esempio il seguente metodo di un componente:
@Name("account")
public class AccountAction {
@Restrict public void delete() {
...
}
}
In questo esempio il permesso richiesto per chiamare il metodo delete()
è account:delete
. L'equivalente di ciò sarebbe stato scrivere @Restrict("#{s:hasPermission('account','delete')}")
. Ora vediamo un altro esempio:
@Restrict @Name("account")
public class AccountAction {
public void insert() {
...
}
@Restrict("#{s:hasRole('admin')}")
public void delete() {
...
}
}
Questa volta la classe stessa del componente è annotata con @Restrict
. Ciò significa che tutti i metodi senza una annotazione @Restrict
a sovrascrivere, richiedono un controllo implicito di permesso. Nel caso di questo esempio il metodo insert()
richiede un permesso per account:insert
, mentre il metodo delete()
richiede che l'utente sia membro del ruolo admin
.
Prima di andare avanti, esaminiamo l'espressione #{s:hasRole()}
vista nell'esempio precedente. Sia s:hasRole()
che s:hasPermission
sono funzioni EL, le quali delegano ai metodi con i nomi corrispondenti nella classe Identity
. Queste funzioni possono essere usate all'interno di una espressione EL in tutte le API di sicurezza.
Essendo un'espressione EL, il valore dell'annotazione @Restrict
può fare riferimento a qualunque oggetto che sia presente in un contesto Seam. Ciò è estremamente utile quando si eseguono i controlli sui permessi per una specifica istanza di un oggetto. Ad esempio:
@Name("account")
public class AccountAction {
@In Account selectedAccount;
@Restrict("#{s:hasPermission(selectedAccount,'modifica')}")
public void modify() {
selectedAccount.modify();
}
}
La cosa interessante da notare in questo esempio è il riferimento a selectedAccount
che si vede all'interno della chiamata alla funzione hasPermission
. Il valore di questa variabile verrà ricercato all'interno del contesto Seam e passato al metodo hasPermission()
di Identity
, il quale in questo caso può determinare se l'utente ha il permesso richiesto per modificare l'oggetto Account
specificato.
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");
Uno degli indicatori di interfaccia utente ben progettata è quando agli utenti non vengono presentate opzioni per le quali essi non hanno i permessi necessari. La sicurezza di Seam consente la visualizzazione condizionale sia di sezioni di una pagina che di singoli controlli, basata sui privilegi dell'utente, usando esattamente le stesse espressioni EL usate nella sicurezza dei componenti.
Diamo un'occhiata ad alcuni esempi della sicurezza nell'interfaccia. Prima di tutto prentendiamo di avere una form di accesso che debba essere visualizzata solo se l'utente non ha già fatto l'accesso. Usando la proprietà identity.isLoggedIn()
possiamo scrivere questo:
<h:form class="loginForm" rendered="#{not identity.loggedIn}"
>
Se l'utente non ha eseguito l'accesso, allora la form di accesso verrà visualizzata. Fin qui tutto bene. Ora vogliamo che ci sia un menu sulla pagina che contenga alcune azioni speciali che devono essere accessibili solo agli utenti del ruolo dirigente
. Ecco un modo in cui ciò potrebbe essere scritto:
<h:outputLink action="#{reports.listManagerReports}" rendered="#{s:hasRole('dirigente')}">
Rapporti per i dirigenti
</h:outputLink
>
Anche fin qui tutto bene. Se l'utente non è un membro del ruolo dirigente
, allora outputLink
non verrà visualizzato. L'attributo rendered
in generale può essere usato per il controllo stesso oppure in un controllo <s:div>
o <s:span>
che ne comprende altri.
Ora andiamo su qualcosa di più complesso. Supponiamo di avere in una pagina un controllo h:dataTable
che elenca delle righe per le quali si può volere visualizzare o meno i link alle azioni in funzione dei permessi dell'utente. La funzione EL s:hasPermission
ci consente di passare un parametro oggetto che può essere usato per determinare se l'utente ha o meno il permesso richiesto per quell'oggetto. Ecco come può apparire una dataTable
con dei link controllati dalla sicurezza:
<h:dataTable value="#{clients}" var="cl">
<h:column>
<f:facet name="header"
>Name</f:facet>
#{cl.name}
</h:column>
<h:column>
<f:facet name="header"
>City</f:facet>
#{cl.city}
</h:column>
<h:column>
<f:facet name="header"
>Action</f:facet>
<s:link value="Modify Client" action="#{clientAction.modify}"
rendered="#{s:hasPermission(cl,'modify')}"/>
<s:link value="Delete Client" action="#{clientAction.delete}"
rendered="#{s:hasPermission(cl,'delete')}"/>
</h:column>
</h:dataTable
>
La sicurezza 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 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'istanza 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 configurare 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 questo viene annotato:
@PrePersist @Restrict
public void prePersist() {}
/META-INF/orm.xml
E' 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 fornite nel pacchetto 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 con maggiore dettaglio nel seguente paragrafo.
Questa è in realtà un'interfaccia che fornisce i metodi per risolvere i singoli permessi sugli oggetti. Seam fornisce le seguenti implementazioni già fatte di PermissionResolver
, che sono descritte in maggiore dettaglio più avanti in questo capitolo:
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 |
Essendo conservati nella sessione dell'utente, ogni implementazione di PermissionResolver
deve aderire ad un paio di restrizioni. In primo luogo, non possono contenere alcuna informazione di stato che abbia una visibilità inferiore a session (e la visibilità del componente stesso deve essere o application oppure session). In secondo luogo, non devono usare la dependency injection poiché ci potrebbero essere accessi da più thread contemporaneamente. Infatti, per ragioni di prestazioni, è raccomandabile annotare con @BypassInterceptors
per evitare del tutto l'insieme degli intercettori Seam.
Un ResolverChain
contiene un elenco ordinato di PermissionResolver
, con lo scopo di risolvere i permessi sugli oggetti di una determinata classe oppure i permessi obiettivo.
La ResolverChain
di default consiste di tutti i risolutori di permessi rilevati durante l'avvio dell'applicazione. L'evento org.jboss.seam.security.defaultResolverChainCreated
viene lanciato (e l'istanza di ResolverChain
viene passata come parametro dell'evento) quando il ResolverChain
di default viene creato. Questo consente di aggiungere ulteriori risolutori che per qualche ragione non erano stati rilevati durante l'avvio, oppure di riordinare o rimuovere i risolutori che sono nell'elenco.
Il seguente diagramma di sequenza mostra l'interazione tra i componenti del framework dei permessi durante la verifica di un permesso (segue la spiegazione). Una verifica di permesso può essere originata da una serie di possibili fonti, ad esempio gli intercettori di sicurezza, la funzione EL s:hasPermission
, oppure tramite una chiamata alla API Identity.checkPermission
:
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-api.jar
drools-compiler.jar
drools-core.jar
drools-decisiontables.jar
drools-templates.jar
janino.jar
antlr-runtime.jar
mvel2.jar
La configurazione per RuleBasedPermissionResolver
richiede che una base di regole venga prima configurata in components.xml
. Di default questa base di regole viene chiamata securityRules
, come nel seguente esempio:
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core"
xmlns:security="http://jboss.com/products/seam/security"
xmlns:drools="http://jboss.com/products/seam/drools"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.2.xsd
http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.2.xsd
http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-2.2.xsd
http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.2.xsd">
<drools:rule-base name="securityRules">
<drools:rule-files>
<value
>/META-INF/security.drl</value>
</drools:rule-files>
</drools:rule-base>
</components
>
Il nome predefinito della base di regole può essere modificato specificando la proprietà security-rules
per RuleBasedPermissionResolver
:
<security:rule-based-permission-resolver security-rules="#{prodSecurityRules}"/>
Una volta che il componente RuleBase
è configurato, è il momento di scrivere le regole di sicurezza.
Il primo passo per scrivere delle regole di sicurezza è di creare un nuovo file di regole nella cartella /META-INF
del file jar dell'applicazione. Di solito questo file dovrebbe essere chiamato qualcosa come security.drl
, comunque lo si può chiamare nel modo che si preferisce purché sia configurato in maniera corrispondente in components.xml
.
Dunque, che cosa deve contenere il file delle regole di sicurezza? A questo punto potrebbe essere una buona idea almeno sbirciare nella documentazione Drools, comunque per partire ecco un esempio estremamente semplice:
package MyApplicationPermissions; import org.jboss.seam.security.permission.PermissionCheck; import org.jboss.seam.security.Role; rule CanUserDeleteCustomers when c: PermissionCheck(target == "customer", action == "delete") Role(name == "admin") then c.grant(); end
Dividiamolo passo per passo. La prima cosa che vediamo è la dichiarazione del pacchetto. Un pacchetto in Drools è essenzialmente una collezione di regole. Il nome del pacchetto può essere qualsiasi, non è in relazione con niente che sia al di fuori della visibilità della base di regole.
La cosa successiva che possiano notare è un paio di dichiarazioni import
per le classi PermissionCheck
e Role
. Questi import
informano il motore di regole che all'interno delle nostre regole faremo riferimento a queste classi.
Infine abbiamo il codice della regola. Ogni regola all'interno di un pacchetto deve avere un nome univoco (di solito descrive lo scopo della regola). In questo caso la nostra regola si chiama CanUserDeleteCustomers
e verrà usata per verificare se ad un utente è consentito di cancellare un record relativo ad un cliente.
Guardando il corpo della definizione della regola si possono notare due distinte sezioni. Le regole hanno quello che è noto come lato sinistro (LHS, left hand side) e un lato destro (RHS, right hand side). Il lato sinistro consiste nella parte condizionale della regola, cioè l'elenco delle condizioni che devono essere soddisfatte affinché si applichi la regola. Il lato sinistro è rappresentato dalla sezione when
. Il lato destro è la conseguenza, o la parte di azione della regola che si applica solo se tutte le condizioni del lato sinistro sono verificate. Il lato destro è rappresentato dalla sezione then
. La fine della regola è stabilita dalla linea end
.
Se guardiamo la parte sinistra della regola vediamo che ci sono due condizioni. Esaminiamo la prima condizione:
c: PermissionCheck(target == "customer", action == "delete")
Letta in inglese questa condizione dice che all'interno della working memory deve esistere un oggetto PermissionCheck
con una proprietà target
uguale a "customer" e una proprietà action
uguale a "delete".
Dunque cos'è la working memory? Nota anche come "stateful session" nella terminologia Drools, la working memory è un oggetto collegato alla sessione che contiene le informazioni contestuali che sono richieste dal motore di regole per prendere una decisione sul controllo di permesso. Ogni volta che il metodo hasPermission()
viene chiamato, viene creato un oggetto, o Fatto, temporaneo PermissionCheck
, e viene inserito nella working memory. Questo PermissionCheck
corrisponde esattamente al permesso che si sta controllando, così, ad esempio, se viene chiamato hasPermission("account", "create")
allora verrà inserito nella working memory un oggetto PermissionCheck
con target
uguale a "account" e action
uguale a "create", per la durata del controllo di permesso.
Accanto al fatto PermissionCheck
c'è anche un fatto org.jboss.seam.security.Role
per ogni ruolo di cui l'utente autenticato è membro. Questi fatti Role
sono sincronizzati con i ruoli dell'utente autenticato all'inizio di ogni controllo di permesso. Di conseguenza qualsiasi oggetto Role
che venisse inserito nella working memory nel corso del controllo di permesso sarebbe rimosso prima che avvenga il controllo di permesso successivo, a meno che l'utente autenticato non sia effettivamente membro di quel ruolo. Insieme ai fatti PermissionCheck
e Role
la working memory contiene anche l'oggetto java.security.Principal
che era stato creato come risultato del processo di autenticazione.
E' anche possibile inserire ulteriori fatti nella working memory chiamando RuleBasedPermissionResolver.instance().getSecurityContext().insert()
, passando l'oggetto come parametro. Fanno eccezione a questo gli oggetti Role
che, come già detto, sono sincronizzati all'inizio di ciascun controllo di permesso.
Tornando al nostro esempio, possiamo anche notare che la prima linea della nostra parte sinistra ha il prefisso c:
. Questa è una dichiarazione di variabile ed è usata per fare riferimento all'oggetto rilevato dalla condizione (in questo caso il PermissionCheck
). Passando alla seconda linea della nostra parte sinistra vediamo questo:
Role(name == "admin")
Questa condizione dichiara semplicemente che ci deve essere un oggetto Role
con un name
uguale ad "admin" nella working memory. Come già menzionato, i ruoli dell'utente sono inseriti nella working memory all'inizio di ogni controllo di permesso. Così, mettendo insieme entrambe le condizioni, questa regola in pratica dice "mi attiverò quando ci sarà un controllo per il permesso customer:delete
e l'utente è un membro del ruolo admin
".
Quindi qual è la conseguenza dell'attivazione della regola? Diamo un'occhiata alla parte destra della regola:
c.grant()
La parte destra è costituita da codice Java e, in questo caso, esso invoca il metodo grant()
dell'oggetto c
il quale, come già detto, è una variabile che rappresenta l'oggetto PermissionCheck
. Insieme alle proprietà name
e action
, nell'oggetto PermissionCheck
c'è anche una proprietà granted
che inizialmente è impostata a false
. Chiamando grant()
su un PermissionCheck
la proprietà granted
viene impostata a true
, il che significa che il controllo di permesso è andato a buon fine, consentendo all'utente di portare avanti qualsiasi azione per cui il controlo di permesso era stato inteso.
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 un database relazionale. Questo risolutore di permessi fornisce una sicurezza orientata alle istanze in stile ACL (Access Control List), permettendo di assegnare specifici permessi sull'oggetto a utenti e ruoli. Allo stesso modo permette inoltre di assegnare in modo persistente permessi con un nome arbitrario (non necessariamente basato sull'oggetto o la classe).
Prima di essere usato il PersistentPermissionResolver
deve essere configurato con un PermissionStore
valido in components.xml
. Se non è configurato, proverà ad usare il permission store di default, JpaIdentityStore
(vedi il paragrafo successivo per i dettagli). Per usare un permission store diverso da quello di default, occorre configurare la proprietà permission-store
in questo modo:
<security:persistent-permission-resolver permission-store="#{myCustomPermissionStore}"/>
Il PersistentPermissionManager
richiede un permission store per connettersi al dispositivo di memorizzazione dove sono registrati i permessi. Seam fornisce una implementazione di PermissionStore
già fatta, JpaPermissionStore
, che viene usata per memorizzare i permessi in un database relazionale. E' possibile scrivere il proprio permission store implementando l'interfaccia PermissionStore
, che definisce i seguenti metodi:
Tabella 15.8. Interfaccia PermissionStore
Tipo restituito |
Metodo |
Descrizione |
---|---|---|
|
|
Questo metodo deve restituire una |
|
|
Questo metodo deve restituire una |
|
|
Questo metodo deve restituire una |
|
|
Questo metodo deve rendere persistente l'oggetto |
|
|
Questo metodo deve rendere persisistenti tutti gli oggetti |
|
|
Questo metodo deve rimuove l'oggetto |
|
|
Questo metodo deve rimuovere dal dispositivo di memorizzazione tutti gli oggetti |
|
|
Questo metodo deve restituire una lista di tutte le azioni disponibili (sotto forma di String) per la classe dell'oggetto specificato. Viene usato dalla gestione dei permessi per costruire l'interfaccia utente con cui si concedono i permessi sulle varie classi (vedi il paragrafo più avanti). |
E' l'implementazione di default di PermissionStore
(e l'unica fornita da Seam), che usa un database relazionale per memorizzare i permessi. Prima di poter essere usata deve essere configurata con una o due classi per la memorizzazione dei permessi su utenti e ruoli. Queste classi entità devono essere annotate con uno speciale insieme di annotazioni relative alla sicurezza per configurare quali proprietà dell'entità corrispondono alle diverse caratteristiche dei permessi che devono essere memorizzati.
Se si vuole usare la stessa entità (cioè una sola tabella sul database) per memorizzare sia i permessi degli utenti che quelli dei ruoli, allora è necessario configurare solo la proprietà user-permission-class
. Se si vogliono usare due tabelle distinte per memorizzare i permessi degli utenti e quelli dei ruoli, allora in aggiunta alla proprietà user-permission-class
si dovrà configurare anche la proprietà role-permission-class
.
Ad esempio, per configurare una sola classe entità per memorizzare sia i permessi degli utenti che quelli dei ruoli:
<security:jpa-permission-store user-permission-class="com.acme.model.AccountPermission"/>
Per configurare classi entità separate per la memorizzazione dei permessi di utenti e ruoli:
<security:jpa-permission-store user-permission-class="com.acme.model.UserPermission"
role-permission-class="com.acme.model.RolePermission"/>
Come già detto, le classi entità che contengono i permessi degli utenti e dei ruoli devono essere configurate con uno speciale insieme di annotazioni contenute nel pacchetto org.jboss.seam.annotations.security.permission
. La seguente tabella elenca queste annotazioni insieme ad una descrizione su come sono usate:
Tabella 15.9. Annotazioni per le entità dei permessi
Annotazione |
Obiettivo |
Descrizione |
---|---|---|
|
|
Questa annotazione identifica la proprietà dell'entità che contiene l'obiettivo del permesso. La proprietà deve essere di tipo |
|
|
Questa annotazione identifica la proprietà dell'entità che contiene l'azione. La proprietà deve essere di tipo |
|
|
Questa annotazione identifica la proprietà dell'entità che contiene l'utente a cui viene concesso il permesso. Deve essere di tipo |
|
|
Questa annotazione identifica la proprietà dell'entità che contiene il ruolo a cui viene concesso il premesso. Deve essere di tipo |
|
|
Questa annotazione deve essere usata quando la stessa entità/tabella viene usata per memorizzare sia i permessi degli utenti che quelli dei ruoli. Essa identifica la proprietà dell'entità che è usata per discriminare tra i permessi degli utenti e quelli dei ruoli. Per default, se il valore della colonna contiene la stringa @PermissionDiscriminator(userValue = "u", roleValue = "r") |
Ecco un esempio di una classe entità che viene usata per memorizzare sia i permessi degli utenti che quelli dei ruoli. La seguente classe si trova nell'applicazione di esempio SeamSpace:
@Entity
public class AccountPermission implements Serializable {
private Integer permissionId;
private String recipient;
private String target;
private String action;
private String discriminator;
@Id @GeneratedValue
public Integer getPermissionId() {
return permissionId;
}
public void setPermissionId(Integer permissionId) {
this.permissionId = permissionId;
}
@PermissionUser @PermissionRole
public String getRecipient() {
return recipient;
}
public void setRecipient(String recipient) {
this.recipient = recipient;
}
@PermissionTarget
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
@PermissionAction
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
@PermissionDiscriminator
public String getDiscriminator() {
return discriminator;
}
public void setDiscriminator(String discriminator) {
this.discriminator = discriminator;
}
}
Come si vede dall'esempio precedente, il metodo getDiscriminator()
è stato annotato con l'annotazione @PermissionDiscriminator
per consentire a JpaPermissionStore
di determinare quali record rappresentano i permessi degli utenti e quali rappresentano i permessi dei ruoli. Inoltre si può vedere che il metodo getRecipient()
è annotato sia con l'annotazione @PermissionUser
che con @PermissionRole
. Ciò è perfettamente valido e significa semplicemente che la proprietà recipient
dell'entità conterrà sia il nome dell'utente che quello del ruolo, in funzione del valore della proprietà discriminator
.
Un ulteriore insieme di annotazioni specifiche per le classi può essere usato per specificare i permessi consentiti per una determinata classe obiettivo. Questi permessi si possono trovare nel pacchetto org.jboss.seam.annotation.security.permission
:
Tabella 15.10. Annotazioni per i permessi sulle classi
Annotazione |
Obiettivo |
Descrizione |
---|---|---|
|
|
E' una annotazione contenitore, che può contenere un elenco di annotazioni |
|
|
Questa annotazione definisce una singola azione regolata da un permesso per la classe obiettivo. La sua proprietà |
Ecco un esempio di queste annotazioni al lavoro. La seguente classe si trova anch'essa nell'applicazione di esempio SeamSpace:
@Permissions({
@Permission(action = "view"),
@Permission(action = "comment")
})
@Entity
public class MemberImage implements Serializable {
Questo esempio dimostra come due possibili azioni regolate da permesso, view
e comment
possono essere dichiarate per la classe entità MemberImage
.
Per default più permessi per lo stesso obiettivo e destinatario vengono memorizzati in un singolo record sul database, con la proprietà/colonna action
contenente un elenco delle azioni concesse separate da una virgola. Per ridurre la quantità di spazio fisico richiesto per memorizzare un numero elevato di permessi è possibile usare un valore intero come maschera di bit (al posto di un elenco di valori separati da virgole) per memorizzare l'elenco delle azioni consentite.
Ad esempio, se al destinatario "Pippo" è concesso sia il permesso view
che il permesso comment
per una particolare istanza di MemberImage
(un entity bean), allora per default la proprietà dell'entità permesso conterrà "view,comment
", che rappresenta la concessione di due azioni. In alternativa, usando i valori di una maschera di bit per le azioni, si può definire in questo modo:
@Permissions({
@Permission(action = "view", mask = 1),
@Permission(action = "comment", mask = 2)
})
@Entity
public class MemberImage implements Serializable {
La proprietà action
conterrà semplicemente "3" (con sia il bit 1 che il bit 2 alzati). Ovviamente per un numero elevato di azioni da consentire per una particolare classe obiettivo, lo spazio richiesto per memorizzare i record dei permessi si riduce grandemente usando le azioni con la maschera di bit.
E' molto importante che i valori assegnati a mask
siano specificati come potenze di 2.
Quando JpaPermissionStore
memorizza o cerca un permesso deve essere in grado di identificare univocamente le istanze degli oggetti sui cui permessi deve operare. Per ottenere questo occorre assegnare una strategia di risoluzione dell'identificatore per ciascuna classe obiettivo, in modo da generare i valori identificativi univoci. Ciascuna implementazione della strategia di risoluzione sa come generare gli identificativi univoci per un particolare tipo di classe ed è solo questione di creare nuove strategie di risoluzione.
L'interfaccia IdentifierStrategy
è molto semplice e dichiara solo due metodi:
public interface IdentifierStrategy {
boolean canIdentify(Class targetClass);
String getIdentifier(Object target);
}
Il primo metodo, canIdentify()
restituisce semplicemente true
se la strategia di risoluzione è in grado di generare un identificativo univoco per la classe obiettivo specificata. Il secondo metodo, getIdentifier()
restituisce il valore dell'identificativo univoco per l'oggetto obiettivo specificato.
Seam fornisce due implementazioni di IdentifierStrategy
, ClassIdentifierStrategy
e EntityIdentifierStrategy
(vedi i prossimi paragrafi per i dettagli).
Per configurare esplicitamente la strategia di risoluzione da usare per una particolare classe, essa deve essere annotata con org.jboss.seam.annotations.security.permission.Identifier
, e il valore deve essere impostato a una implementazione dell'interfaccia IdentifierStrategy
. Una proprietà facoltativa name
può essere specificata, e il suo effetto dipende dall'implementazione di IdentifierStrategy
usata.
Questa strategia di risoluzione degli identificatori è usata per generare gli identificatori univoci per le classi e userà il valore della proprietà name
(se indicato) nell'annotazione @Identifier
. Se la proprietà name
non è indicata, allora tenterà di usare il nome del componente della classe (se la classe è un componente Seam), oppure, come ultima risorsa creerà un identificatore basato sul nome della classe (escludendo il nome del pacchetto). Ad esempio, l'identificatore per la seguente classe sarà "customer
":
@Identifier(name = "customer")
public class Customer {
L'identificatore per la seguente classe sarà "customerAction
":
@Name("customerAction")
public class CustomerAction {
Infine, l'identificatore per la seguente classe sarà "Customer
":
public class Customer {
Questa strategia di risoluzione è usata per generare valori di identificatori univoci per gli entity bean. Quello che fa è concatenare il nome dell'entità (o un nome configurato in altro modo) con una stringa che rappresenta la chiave primaria dell'entità. Le regole per generare la parte nome dell'identificatore sono simili a ClassIdentifierStrategy
. La chiave primaria (cioè l'id dell'entità) viene ottenuto usando il componente PersistenceProvider
, che è in grado di determinarne il valore a prescindere dall'implementazione della persistenza utilizzata dall'applicazione Seam. Per le entità non annotate con @Entity
è necessario configurare esplicitamente la strategia di risoluzione dell'identificatore nella classe entità stessa, ad esempio:
@Identifier(value = EntityIdentifierStrategy.class)
public class Customer {
Per avere un esempio del tipo di valori di identificatore generati, supponiamo di avere la seguente classe entità:
@Entity
public class Customer {
private Integer id;
private String firstName;
private String lastName;
@Id
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
Per una istanza di Customer
con un valore di id
pari a 1
, il valore dell'identificatore sarà "Customer:1
". Se l'entità è annotata con un nome esplicito di identificatore, come questo:
@Entity
@Identifier(name = "cust")
public class Customer {
Allora un Customer
con un id
pari a 123
avrà come identificatore "cust:123
".
In modo del tutto simile a come la sicurezza di Seam fornisce una API per la gestione delle identità per gestire utenti e ruoli, essa fornisce anche una API per la gestione dei permessi, tramite il componente PermissionManager
.
Il componente PermissionManager
è un componente Seam registrato a livello application che fornisce una serie di metodi per gestire i permessi. Prima di poter essere usato deve essere configurato con un permission store (benché di default tenterà di usare il JpaPermissionStore
se disponibile). Per configurare esplicitamente un permission store personalizzato, occorre specificare la proprietà permission-store
in components.xml:
<security:permission-manager permission-store="#{ldapPermissionStore}"/>
La seguente tabella descrive ciascuno dei metodi disponibili forniti da PermissionManager
:
Tabella 15.11. Metodi della API PermissionManager
Tipo restituito |
Metodo |
Descrizione |
---|---|---|
|
|
Restituisce un elenco di oggetti |
|
|
Restituisce un elenco di oggetti |
|
|
Memorizza (concede) il |
|
|
Memorizza (concede) l'elenco di |
|
|
Rimuove (revoca) il |
|
|
Rimuove (revoca) l'elenco di |
|
|
Restituisce un elenco delle azioni disponibili per l'oggetto obiettivo specificato. Le azioni che questo metodo restituisce dipendono dall'annotazione |
Per chiamare un metodo di PermissionManager
è richiesto che l'utente correntemente autenticato abbia le autorizzazioni appropriate per eseguire quella operazione di gestione. La seguente tabella elenca i permessi richiesti che l'utente corrente deve avere.
Tabella 15.12. Permessi per la gestione dei permessi
Metodo |
Oggetto del permesso |
Azione del permesso |
---|---|---|
|
L' |
|
|
L'obiettivo del |
|
|
L'obiettivo del |
|
|
Ciascuno degli obiettivi dell'elenco di |
|
|
L'obiettivo del |
|
|
Ciascuno degli obiettivi dell'elenco di |
|
Seam include un supporto di base per servire le pagine sensibili tramite il protocollo HTTPS. Si configura facilmente specificando uno scheme
per la pagina in pages.xml
. Il seguente esempio mostra come la pagina /login.xhtml
è configurata per usare HTTPS:
<page view-id="/login.xhtml" scheme="https"/>
Questa configurazione viene automaticamente estesa ai controlli JSF s:link
e s:button
, i quali (quando viene specificata la view
) faranno in modo che venga prodotto il link usando il protocollo corretto. In base al precedente esempio, il seguente link userà il protocollo HTTPS, perché /login.xhtml
è configurata per usarlo:
<s:link view="/login.xhtml" value="Login"/>
Navigando direttamente sulla pagina mentre si usa il protocollo non corretto causerà una redirezione alla stessa pagina usando il protocollo corretto. Ad esempio, navigando su una pagina che ha scheme="https"
usando HTTP causerà una redirezione alla stessa pagina usando HTTPS.
E' anche possibile configurare un default scheme per tutte le pagine. Questo è utile se si vuole usare HTTPS solo per alcune pagine. Se non è indicato un default scheme, allora il comportamento normale è di continuare ad usare lo schema correntemente usato. Perciò una volta che l'utente ha fatto l'accesso ad una pagina che richiede HTTPS, allora HTTPS continuerà ad essere usato dopo che l'utente si sposta su altre pagine non HTTPS. (Mentre questo è buono per la sicurezza, non lo è per le prestazioni!). Per definire HTTP come default scheme
, aggiungere questa riga a pages.xml
:
<page view-id="*" scheme="http" />
Chiaramente, se nessuna delle pagine dell'applicazione usa HTTPS allora non è richiesto di specificare alcun default scheme.
E' possibile configurare Seam per invalidare automaticamente la sessione HTTP ogni volta che lo schema cambia. Basta aggiungere questa riga a components.xml
:
<web:session invalidate-on-scheme-change="true"/>
Questa opzione aiuta a rendere il sistema meno vulnerabile alle intromissioni che rilevano l'id di sessione o alla mancanza di protezione su dati sensibili dalle pagine che usano HTTPS ad altre che usano HTTP.
Se si vogliono configurare manualmente le porte HTTP e HTTPS, queste possono essere configurate in pages.xml
specificando gli attributi http-port
e https-port
nell'elemento pages
:
<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.2.xsd"
no-conversation-view-id="/home.xhtml"
login-view-id="/login.xhtml"
http-port="8080"
https-port="8443"
>
Sebbene non faccia strettamente parte delle API di sicurezza, Seam fornisce un algoritmo CAPTCHA (Completely Automated Public Turing test to tell Computer and Humans Apart, test di Turing publico completamente automatico per distinguere gli umani dalle macchine) già fatto per prevenire l'interazione con l'applicazione da parte di procedure automatiche.
Per partire è necessario configurare la Seam Resource Servlet, che fornirà l'immagine CAPTCHA da risolvere nella pagina. Questo richiede la seguente voce 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
>
Aggiungere una verifica CAPTCHA ad una form è estremamente facile. Ecco un esempio:
<h:graphicImage value="/seam/resource/captcha"/>
<h:inputText id="verifyCaptcha" value="#{captcha.response}" required="true">
<s:validate />
</h:inputText>
<h:message for="verifyCaptcha"/>
Questo è tutto. Il controllo graphicImage
mostra l'immagine CAPTCHA e inputText
riceve la risposta dell'utente. La risposta viene automaticamente validata con il CAPTCHA quando la form viene inviata.
E' possibile personalizzare l'algoritmo CAPTCHA sovrascrivendo il componente già fatto:
@Name("org.jboss.seam.captcha.captcha")
@Scope(SESSION)
public class HitchhikersCaptcha extends Captcha
{
@Override @Create
public void init()
{
setChallenge("Qual � la risposta alla vita, all'universo e a tutto il resto?");
setCorrectResponse("42");
}
@Override
public BufferedImage renderChallenge()
{
BufferedImage img = super.renderChallenge();
img.getGraphics().drawOval(5, 3, 60, 14); //aggiungi qualche oscura decorazione
return img;
}
}
La seguente tabella descrive una serie di eventi (vedi Capitolo 6, Eventi, interceptor e gestione delle eccezioni) lanciati dalla sicurezza di Seam in corrispondenza di determinati eventi relativi alla sicurezza.
Tabella 15.13. Eventi della sicurezza
Nome dell'evento |
Descrizione |
---|---|
|
Lanciato quando un tentativo di login è stato completato con esito positivo. |
|
Lanciato quando un tentativo di login è fallito. |
|
Lanciato quando un utente che ha già fatto il login, tenta di fare il login di nuovo. |
|
Lanciato quando una verifica di sicurezza fallisce in quanto l'utente non ha fatto il login. |
|
Lanciato quando una verifica di sicurezza fallisce in quanto l'utente ha fatto il login ma non ha i privilegi richiesti. |
|
Lanciato subito prima l'autenticazione dell'utente. |
|
Lanciato subito dopo l'autenticazione dell'utente. |
|
Lanciato dopo che l'utente ha fatto un logout. |
|
Lanciato quando le credenziali dell'utente vengono cambiate. |
|
Lanciato quando la proprietà RememberMe di Identity viene modificata. |
A volte può essere necessario eseguire determinate operazioni con dei privilegi più elevati, come creare un nuovo utente da parte di un utente non autenticato. La sicurezza di Seam gestisce un tale meccanismo tramite la classe RunAsOperation
. Questa classe consente sia al Principal
o al Subject
, che ai ruoli dell'utente di essere sovrascritti per un singolo insieme di operazioni.
Il seguente codice di esempio mostra come viene usato RunAsOperation
, chiamando il suo metodo addRole()
per fornire un insieme di ruoli fittizi solo per la durata dell'operazione. Il metodo execute()
contiene il codice che verrà eseguito con i privilegi aggiuntivi.
new RunAsOperation() {
public void execute() {
executePrivilegedOperation();
}
}.addRole("admin")
.run();
In modo analogo, i metodi getPrincipal()
o getSubject()
possono anch'essi essere sovrascritti per specificare le istanze di Principal
e Subject
da usare per la durata dell'operazione. Infine, il metodo run()
è usato per eseguire la RunAsOperation
.
A volte può essere necessario estendere il componente Identity se l'applicazione ha particolari requisiti di sicurezza. Il seguente esempio (fatto di proposito, dato che le credenziali sarebbero normalmente gestite dal componente Credentials
) mostra un componente Identity esteso con un campo companyCode
aggiuntivo. La precendenza di installazione impostata a APPLICATION
assicura che questo Identity esteso venga installato al posto dell'Identity originale.
@Name("org.jboss.seam.security.identity")
@Scope(SESSION)
@Install(precedence = APPLICATION)
@BypassInterceptors
@Startup
public class CustomIdentity extends Identity
{
private static final LogProvider log = Logging.getLogProvider(CustomIdentity.class);
private String companyCode;
public String getCompanyCode()
{
return companyCode;
}
public void setCompanyCode(String companyCode)
{
this.companyCode = companyCode;
}
@Override
public String login()
{
log.info("###### CUSTOM LOGIN CALLED ######");
return super.login();
}
}
Notare che un componente Identity
deve essere marcato @Startup
, in modo che sia disponibile immediatamente dopo l'inizio del contesto SESSION
. La mancanza di questo dettaglio renderebbe non utilizzabili determinate funzionalità di Seam nell'applicazione.
OpenID è un standard comune per l'autenticazione esterna sul web. L'idea fondamentale è che qualsiasi applicazione web possa integrare (o sostituire) la sua gestione locale dell'autenticazione delegandone la responsabilità ad un server OpenID esterno scelto dall'utente. Questo va a beneficio dell'utente che non deve più ricordare un nome e una password per ogni applicazione web che usa, e dello sviluppatore, che viene sollevato di un po' di problemi nel manutenere un complesso sistema di autenticazione.
Quando si usa OpenID, l'utente sceglie un fornitore OpenID, e il fornitore OpenID assegna all'utente un OpenID. Questo id prende la forma di un URL, ad esempio http://grandepizza.myopenid.com
, comunque è accettabile trascurare la parte http://
dell'identificativo quando si fa il login ad un sito. L'applicazione web (detta relying party in termini OpenID) determina quale server OpenID deve contattare e redirige l'utente a quel sito per l'autenticazione. Dopo essersi autenticato con esito positivo, all'utente viene fornito un codice (crittograficamente sicuro) che prova la sua identità e viene rediretto di nuovo all'applicazione web originale. L'applicazione web locale può essere quindi sicura che l'utente che sta accedendo è il proprietario dell'OpenID che aveva fornito.
E' importante rendersi conto, a questo punto, che l'autenticazione non implica l'autorizzazione. L'applicazione web ha ancora bisogno di fare delle considerazioni su come usare quell'informazione. L'applicazione web potrebbe trattare l'utente come immediatamente autenticato e dargli/le pieno accesso al sistema, oppure potrebbe tentare di associare l'OpenID fornito ad un utente locale, chiedendo all'utente di registrarsi se non l'ha già fatto. La scelta su come gestire l'OpenID è lasciata ad una decisione progettuale dell'applicazione locale.
Seam usa il pacchetto openid4java e richiede quattro JAR aggiuntivi per usare l'integrazione Seam. Essi sono: htmlparser.jar
, openid4java.jar
, openxri-client.jar
e openxri-syntax.jar
.
L'elaborazione di OpenID richiede l'uso di OpenIdPhaseListener
, che deve essere aggiunto al file faces-config.xml
. Il phase listener elabora le chiamate dal fornitore OpenID, consentendo di rientrare nell'applicazione locale.
<lifecycle>
<phase-listener>org.jboss.seam.security.openid.OpenIdPhaseListener</phase-listener>
</lifecycle>
Con questa configurazione il supporto OpenID è disponibile nell'applicazione. Il componente per il supporto OpenID, org.jboss.seam.security.openid.openid
, viene installato automaticamente se le classi openid4java sono nel classpath.
Per iniziare un login con OpenID occorre presentare una semplice form che chieda all'utente il suo OpenID. Il valore #{openid.id}
accetta l'OpenID dell'utente e l'azione #{openid.login]
inizia la richiesta di autenticazione.
<h:form>
<h:inputText value="#{openid.id}" />
<h:commandButton action="#{openid.login}" value="OpenID Login"/>
</h:form>
Quando l'utente invia la form di login, viene rediretto al suo fornitore OpenID. L'utente torna infine all'applicazione tramite la pseudo pagina Seam /openid.xhtml
, che è fornita dal OpenIdPhaseListener
. L'applicazione può gestire la risposta OpenID per mezzo della navigazione indicata in pages.xml
per quella pagina, proprio come se l'utente non avesse mai lasciato l'applicazione.
La strategia più semplice è di eseguire immediatamente il login dell'utente. La seguente regola di navigazione mostra come gestire questa modalità usando l'azione #{openid.loginImmediately()}
.
<page view-id="/openid.xhtml">
<navigation evaluate="#{openid.loginImmediately()}">
<rule if-outcome="true">
<redirect view-id="/main.xhtml">
<message>OpenID login completato con esito positivo...</message>
</redirect>
</rule>
<rule if-outcome="false">
<redirect view-id="/main.xhtml">
<message>OpenID login rifiutato...</message>
</redirect>
</rule>
</navigation>
</page>
L'azione loginImmediately()
controlla per vedere se l'OpenID è valido. Se è valido, aggiunge un OpenIDPrincipal al componente identity, marca l'utente come loggato (cioè #{identity.loggedIn}
sarà true) e restituisce true. Se l'OpenID non è stato validato, il metodo restituisce false, e l'utente rientra nell'applicazione non autenticato. se l'OpenID dell'utente è valido, esso sarà accessibile usando l'espressione #{openid.validatedId}
e #{openid.valid}
sarà true.
Si può desiderare di non autenticare immediatamente l'utente nell'applicazione. In questo caso la navigazione dovrà controllare la proprietà #{openid.valid}
e redirigere l'utente ad una pagina per la registrazione o l'elaborazione dell'utente. Le azioni che si possono prendere sono di chiedere maggiori informazioni e creare un utente locale, oppure presentare un CAPTCHA per evitare registrazioni da programmi automatici. Quando questa elaborazione è terminata, se si vuole autenticare l'utente è possibile chiamare il metodo loginImmediately
, sia tramite EL come mostrato in precedenza, sia interagendo direttamento con il componente org.jboss.seam.security.openid.OpenId
. Ovviamente niente impedisce di scrivere da soli del codice personalizzato per interagire con il componente Seam Identity per avere un comportamento più personalizzato.
Il log out (dimenticando l'associazione OpenID) viene fatto chiamando #{openid.logout}
. Se non si sta usando la sicurezza Seam è possibile chiamare questo metodo direttamente. Se si sta usando la sicurezza Seam occorre continuare ad usare #{identity.logout}
e installare un gestore di eventi per catturare l'evento logout, chiamando il metodo logout di OpenID.
<event type="org.jboss.seam.security.loggedOut">
<action execute="#{openid.logout}" />
</event>
E' importante non trascurare questo punto altrimenti l'utente non sarà più in grado di eseguire nuovamente il login nella stessa sessione.