SeamFramework.orgCommunity Documentation

第15章 セキュリティ

15.1. 概要
15.2. セキュリティの無効化
15.3. 認証
15.3.1. 認証コンポーネントの設定
15.3.2. 認証メソッドの記述
15.3.3. ログインフォームの記述
15.3.4. 設定のまとめ
15.3.5. Remember Me (覚えておいて!)
15.3.6. セキュリティ例外の処理
15.3.7. ログインリダイレクト
15.3.8. HTTP認証
15.3.9. 高度な認証機能
15.4. IDの管理
15.4.1. IDマネージャの設定
15.4.2. JpaIdentityStore
15.4.3. LdapIdentityStore
15.4.4. 自分のIDストアを記述する
15.4.5. ID管理による認証
15.4.6. IDマネージャの使用
15.5. エラーメッセージ
15.6. 認可
15.6.1. 核となる概念
15.6.2. コンポーネントの安全性を確保する
15.6.3. ユーザーインタフェースのセキュリティ
15.6.4. ページ単位のセキュリティ
15.6.5. エンティティのセキュリティ
15.6.6. タイプセーフなアクセス権のアノテーション
15.6.7. タイプセーフなロールのアノテーション
15.6.8. パーミッション許可モデル
15.6.9. ルールベースのパーミッションリゾルバー
15.6.10. 永続的パーミッションリゾルバー
15.7. パーミッションの管理
15.7.1. パーミッションマネージャ
15.7.2. パーミッションマネージャ操作のためのパーミッションチェック
15.8. SSLによるセキュリティ
15.8.1. デフォルトのポートの上書き
15.9. キャプチャ
15.9.1. キャプチャ サーブレットの設定
15.9.2. キャプチャのフォームへの追加
15.9.3. キャプチャアルゴリズムのカスタム化
15.10. セキュリティ イベント
15.11. 自分とは別の権限での実行
15.12. IDコンポーネント(Identity component)の拡張
15.13. OpenID
15.13.1. Configuring OpenID
15.13.2. Presenting an OpenIdDLogin form
15.13.3. Logging in immediately
15.13.4. Deferring login
15.13.5. Logging out

SeamのセキュリティAPIはSeamベースのアプリケーションに種々のセキュリティ関連機能を提供します。 これらの機能には、以下のようなものがあります。

この章ではこれらの機能の詳細について説明します

In some situations it may be necessary to disable Seam Security, for instances during unit tests or because you are using a different approach to security, such as native JAAS. Simply call the static method Identity.setSecurityEnabled(false) to disable the security infrastructure. Of course, it's not very convenient to have to call a static method when you want to configure the application, so as an alternative you can control this setting in components.xml:

Assuming you are planning to take advantage of what Seam Security has to offer, the rest of this chapter documents the plethora of options you have for giving your user an identity in the eyes of the security model (authentication) and locking down the application by establishing constraints (authorization). Let's begin with the task of authentication since that's the foundation of any security model.

Seam セキュリティの提供する認証機構は JAAS (Java Authentication and Authorization Service) の上に構築されており、 ユーザー認証のための堅牢で設定の自由度の高い API を提供しています。 しかしながら、 Seamで は JAAS の複雑さを隠蔽したより単純化された認証機構も提供しています。

単純な認証機構ではSeamアプリケーションのコンポーネントに認証を委ねるSeamLoginModule(これは、Seamに内蔵されているJAASのログインモジュールです)を使います。 このログインモジュールはSeamのデフォルトのアプリケーションポリシーとして予め設定されていますので、新たに設定に追加する事なく使用することができます。 また、作成したアプリケーションのエンティティクラスを利用して、認証メソッドを記述したり、 サード―パーティのプロバイダを使った認証をする事ができます。 この「単純な認証機構」を利用するためにはcomponents.xmlに下記のようにidentityコンポーネントを設定する必要があります。


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

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

</components
>

EL式 #{authenticator.authenticate}authenticatorコンポーネントのauthenticateメソッドを使って、ユーザーの認証を行うことを示すメソッドバインディングです。

components.xml 中のidentityauthenticate-methodプロパティでSeamLoginModuleにユーザーの認証に使うメソッドを指定します。 このメソッドはパラメータを取らず、認証が成功したか否かのboolean型を返します。 ユーザーのusernameとpasswordはCredentials.getUsername()Credentials.getPassword()からそれぞれ取得します(また、Identity.instance().getCredentials()からcredentialsコンポーネントを参照する事もできます)。 ユーザーがメンバーとして参加するロールはIdentity.addRole()により指定される必要があります。 以下に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;
      }
   }
}

上記の例では、UserUserRoleはアプリケーション独自のエンティティBeanとなっています。 パラメータ roles は "admin", "user" の様に文字列として、Setに追加されてゆく必要があります。 この例の場合、userが見付からずにNoResultExceptionが投げられた場合には、認証メソッドはfalseを返して、認証が失敗したことを示します。

Seamのセキュリティ機能ではオンラインのWEBアプリケーションで一般的に提供されている"Remember me"(覚えておいてね)機能をサポートしています。 この機能は、二つの異なったモードをサポートしており、一つはusernameをユーザーのブラウザにクッキーとして保存し、ブラウザからpasswordの入力を促すものです(この場合でも、最近のほとんどのブラウザは、passwordを記憶しています)。

第2のモードはユニークなトークンをクッキーとして記憶しておいて、そのサイトに入ると、passwordの入力をする事なく自動的にユーザーの認証をする機能をサポートするものです。

警告

クライアント側の永続的なクッキーによる自動的な認証(第2のモード)は危険で、ユーザーへの利便性は向上しますが、クロスサイトスクリプティングに対するセキュリティホールの影響を通常より遥かに重大な物としてしまいます。 認証のためのクッキーでなければ、XSSにより攻撃者に盗まれるクッキーは、現在のセッションのユーザーのクッキーという事になります。 これは、ユーザーがセッションを開いていなければ攻撃が有効でないことを意味し、攻撃可能な時間が非常に短い事を意味します。 攻撃者が自動認証をサポートする永続的なRemember meクッキーを盗む可能性があるとすれば、それはたいへん危険なことです。 この機能の利用の危険性は、システムがXSS攻撃に対してどれだけ防御できているのかに依存し、XSS攻撃に対して100%の防御を保証している必要がありますが、入力内容をWEBページに表示するようなサイトにとって、これは簡単ではありません。

ブラウザのベンダーはこの問題を認識しており、最近のブラウザでは新たに導入されたRemember Passwordをサポートしています。 この場合は、ブラウザは特定のドメイン、ウェブサイトに対してのusernameとpasswordを記憶しており、ウェブサイトとのセッションがアクティブでない状態で、ログインフォームのusernameとpasswordを自動的に埋めてゆきます。 ウェブデザイナの立場であれば、ログインのためのショートカットキーを設定しておけば、Remember Meと同様にユーザーの利便性を向上させることができます。 OS-xのSafariなど一部のブラウザでは、OSのキーチェインに暗号化したログインのフォームを記憶させています。 また、ブラウザのクッキーは一般に同期化させることはできませんが、ネットワーク環境ではこのキーチェインはラップトップからデスクトップへと移動させることができます。

まとめ:自動認証をする永続的なRemember Meの使用は一般化してしまっていますが、セキュリティ上、不適切であり使用すべきではありません。 ログイン時のusernameのみを記憶するクッキーを使用することには問題はありません。

デフォルトのRemember me(usernameのみ)機能を使用するためには、特に設定は必要ありません。 下の例のように、ログインフォームにremember meチェックボックスを入れて、これをrememberMe.enabledとバインドするだけです。


  <div>
    <h:outputLabel for="name" value="User name"/>
    <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="Remember me"/>
    <h:selectBooleanCheckbox id="rememberMe" value="#{rememberMe.enabled}"/>
  </div
>

トークンベースの自動認証機能の remember me を使用するためには、まずトークンの記憶場所を設定する必要があります。 Seamでもサポートしていますが、このトークンの記憶場所としてはデータベースが一般的です。 しかし、org.jboss.seam.security.TokenStore インタフェースを実装して独自の記憶場所を設定することも可能です。 この章では標準で提供されているJpaTokenStore実装を使用して認証トークンをデータベーステーブルに記憶させることを前提としています。

まず最初に、トークンを保持する新たなエンティティを作ります。 以下に、一般的なエンティティの構造を示します。

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

このコードから分かるように、エンティティのusernameとトークンのプロパティには@TokenUsername@TokenValueという特別なアノテーションが使われており、これらは認証トークンを含むエンティティに必須です。

次に、このエンティティBeanに対して認証トークンの出し入れをするために、 JpaTokenStoreを設定します。 これは、 components.xmltoken-class属性を指定することにより行います。



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

ここまでが終了したら、最後はcomponents.xmlRememberMeコンポーネントの設定をする事です。 modeautoLoginに設定されていなければなりません。


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

これで、remember meをチェックしているユーザーがサイトを再訪した時に、自動的に認証されるようになります。

ユーザーがサイトを再訪した時に確実に自動的に認証される様にするために、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
>

セキュリティエラーでユーザーがデフォルトのエラーページを受け取らないようにするために、pages.xmlにセキュリティエラーに対応した、もう少し見栄えのするページにリダイレクトするよう設定する事が推奨されます。 セキュリティAPIの発生させる例外には主として二つのタイプがあります。

NotLoggedInExceptionの場合、ユーザーがログインできるよう、ユーザーをログインページかユーザー登録ページへ誘導する事が推奨されます。 一方、AuthorizationExceptionの場合にはユーザーをエラーページに誘導した方が良いでしょう。 以下の例では、この二つのセキュリティ例外によるリダイレクトを処理しているpages.xmlを示しています。


<pages>

    ...

    <exception class="org.jboss.seam.security.NotLoggedInException">
        <redirect view-id="/login.xhtml">
            <message
>You must be logged in to perform this action</message>
        </redirect>
    </exception>

    <exception class="org.jboss.seam.security.AuthorizationException">
        <end-conversation/>
        <redirect view-id="/security_error.xhtml">
            <message
>You do not have the necessary security privileges to perform this action.</message>
        </redirect>
    </exception>

</pages
>

ほとんどのwebアプリケーションでは、より洗練されたログインリダイレクトを必要としますが、Seamではこの様なケースに対応できるような機能も持たせています。

認証されていないユーザーが特定のビュー(或はワイルドカードで指定された複数のビュー)の閲覧をしようとした時に、Seamがユーザーをログイン画面にリダイレクトするようにするためには (pages.xmlに) 下のように記述します。


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

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

    ...

</pages
>

ユーザーがログインした後で、再度ログインし直したい場合に自動的に最初のページ(ユーザーが入ってきたページ)に戻したいような状況を考えてみましょう。 下の様にイベントリスナーをcomponents.xmlに記述すると、ログインせずに制限されたページの閲覧をした(閲覧に失敗した)ことを記憶させておいて、ユーザーが再ログインして成功したときに、当初の要求時のページパラメータをもと当該ページにリダイレクトさせることができます。


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

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

ログインリダイレクトは対話スコープで実装されていますので、authenticate()の中で対話を終了させてはいけません。

推奨されませんが、どうしても必要であれば、Seamは(RFC2617)のHTTPBasicあるいはHTTPDigestメソッドを認証に使用する事ができます。 これらの認証フォームを使用する場合にはcomponents.xmlで authentication-filterが使用可能に設定されている必要があります。



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

ベーシックな認証フィルタを使用する場合、auth-typebasicを設定し、ダイジェスト認証を使用する場合には、digestを設定します。 ダイジェスト認証を使用する場合には keyrealmも設定する必要があります。



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

keyは任意の文字列です。 realmはユーザーが認証される時にユーザーに提供される認証レルムです。

ID管理は、バックエンドのIDストアの種類(データベース、LDAP等)に依存しない、Seamのユーザーとロールの管理のための標準APIを提供します。 ID管理APIの中心はidentityManagerコンポーネントで、新規ユーザーの作成、変更、削除、ロールの追加、無効化、パスワードの変更、ユーザーアカウントの有効化、無効化、ユーザーの認証、ユーザーとロールの一覧等の機能のためのメソッドを提供します。

使用する前にidentityManagerに一つ以上のIdentityStoresを設定する必要があります。 これらのコンポーネントが実際にバックにあるデータベース、LDAP、その他のセキュリティプロバイダと協調して仕事をします。

identityManager コンポーネントに認証と許可について別々のIDストアを設定する事が可能で、例えば、LDAPディレクトリを使用してユーザーの認証をし、RDBからこのユーザーのロール情報を得て使用する事ができます。

SeamはIdentityStoreとして二つのIdentityStoreの実装を提供しています。 ひとつはRDBを使用してユーザーとロールの情報を保持するJpaIdentityStoreで、デフォルトのIDストアとして設定されており、identityManager コンポーネントの設定をすることなく使用する事ができます。 もう一つはLdapIdentityStoreで、LDAPディレクトリを使用してユーザーとロールを保持します。

identityManager コンポーネントにはidentityStoreroleIdentityStoreの二つの設定可能なプロパティがあります。これらの値は、IdentityStoreインタフェースを実装したSeamコンポーネントを参照するEL式である必要があります。 既に言及したように、設定がされていない場合にはデフォルトの JpaIdentityStoreが使用され、また、identityStoreのみが設定された場合にはroleIdentityStore に同じ値が設定されたものとして処理されます。 例えば、components.xmlLdapIdentityStoreidentityManagerに使用するように設定した場合には、ユーザーに関するものと、ロールに関するものの両方にidentityManagerが使用されます。


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

下記の例ではユーザーに関してはLdapIdentityStoreを、またロールに関する処理にはJpaIdentityStoreを使用するようidentityManager を設定しています。


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

以下の章ではこれらのIDストアのインプリメンテーションの詳細について説明します

このIDストアはユーザーおよびロールをリレーショナルデータベースに保存する事を可能としています。 また、データベースのスキーマ設計にはできる限り制約を作らないように設計されており、使用するテーブルの構造に大幅な自由度を認めています。 これはユーザーおよびロールのレコード用のエンティティBeanに特別のアノテーションを使用する事により実現しています。

先に述べたように、特定のアノテーションを使用してユーザーとロールを保持するエンティティBeanを設定します。 下の表に、これらのアノテーションとその詳細な説明について示します。

表 15.1. ユーザーエンティティアノテーション

アノテーション

状態

詳細

@UserPrincipal

要求条件

このアノテーションでユーザーのusernameを保持しているフィールドあるいはメソッドをマークします。

@UserPassword

要求条件

このアノテーションは、アノテートされたフィールドあるいはメソッドにユーザーのpasswordがある事を示しています。 passwordのハッシュアルゴリズムをhashで指定することが可能で、指定できる値にはmd5, shanoneがあります。

@UserPassword(hash = "md5")

public String getPasswordHash() { 
  return passwordHash; 
}

Seamが標準でサポートしていないハッシュアルゴリズムを使用する場合には、PasswordHashを拡張して必要なハッシュアルゴリズムを実装してください。

@UserFirstName

オプション

ユーザーのファーストネームを保持しているフィールドあるいはメソッドをマークします。

@UserLastName

オプション

ユーザーのラストネームを保持しているフィールドあるいはメソッドをマークします。

@UserEnabled

オプション

このアノテーションは、アノテートされたフィールドあるいはメソッドがユーザーが不活化されているか否かを示していることを示しています。 ここでアノテートされるフィールドあるいはメソッドの属性はbooleanでなければなりません。 また、もしこのアノテーションが無ければ、すべてのユーザーが不活化されていないことになります。

@UserRoles

要求条件

このアノテーションは、アノテートされたフィールドあるいはメソッドにユーザーのロールがある事を示しています。 この属性については、以下により詳細に記述します。



既に示したようにJpaIdentityStoreはデータベースのユーザーとロールに関するテーブルのスキーマのデザインができるだけ自由にできるように設計されています。 ここでは、ユーザーとロールを保持するいくつかのデータベースのスキーマについてみてゆきます。

この単純な例では、クロス参照テーブルUserRolesを通じてmany-to-many関連でリンクされているuserとroleのテーブルで構成されています。

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

この例では、前の最少機能の例にすべてのオプションフィールドと、ロールにグループメンバーを許可する機能を追加しています。

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

IdentityManagerのIDストアの実装としてJpaIdentityStoreを使用する場合、特定の IdentityManagerメソッドを起動するとイベントが発生します。

このIDストアの実装はLDAPディレクトリをユーザーレコードとして使用するよう設計されています。 この実装は、ユーザーとロールのディレクトリへの保存の方法の設定の自由度が高くなっています。 以下のセクションでは、このIIDストアの設定オプションについて説明し、いくつかのサンプル設定を示します。

以下の表にcomponents.xml で設定できるLdapIdentityStoreの属性について示します。

表 15.3. LdapIdentityStore設定可能属性

プロパティ

デフォルト値

詳細

server-address

localhost

LDAPサーバのアドレス

server-port

389

LDAPサーバが使用しているポートの番号

user-context-DN

ou=Person,dc=acme,dc=com

ユーザーレコードを含むコンテキストの識別名(DN)

user-DN-prefix

uid=

この値がユーザーレコードの位置指定するためにusernameの前に前置されます。

user-DN-suffix

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

この値がユーザーレコードの位置指定するためにusernameの後ろに追加されます。

role-context-DN

ou=Role,dc=acme,dc=com

ロールレコードを含むコンテキストの識別子(DN)

role-DN-prefix

cn=

この値がロール名の前に前置され、ロールレコードを位置指定するための識別子として使用されます。

role-DN-suffix

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

この値がロール名の後ろに追加され、ロールレコードを位置指定するための識別子として使用されます。

bind-DN

cn=Manager,dc=acme,dc=com

LDAPサーバとバインドするために使用するコンテキスト

bind-credentials

secret

LDAPサーバとバインドするときに使用されるクレデンシャル(パスワード)

user-role-attribute

roles

ユーザーがメンバーであるロールのリストを含んでいるユーザーレコード中の属性の名前

role-attribute-is-DN

true

このブール値はユーザーレコード中のロール属性が識別名か否かを示しています。

user-name-attribute

uid

ユーザーレコードのどの属性がusernameに該当するのかを示しています。

user-password-attribute

userPassword

ユーザーレコードのどの属性がpasswordに該当するのかを示しています。

first-name-attribute

null

ユーザーレコードのどの属性がfirst nameに該当するのかを示しています。

last-name-attribute

sn

ユーザーレコードのどの属性がlast nameに該当するのかを示しています。

full-name-attribute

cn

ユーザーレコードのどの属性がユーザーのフルネームに該当するのかを示しています。

enabled-attribute

null

ユーザーレコードのどの属性がユーザーが不活化されていないかを示しています。

role-name-attribute

cn

ロールレコードのどの属性がロール名に該当するのかを示しています。

object-class-attribute

objectClass

ディレクトリ中でオブジェクトのクラスを決定している属性を示しています。

role-object-classes

organizationalRole

新規のロールレコードの作成のためのオブジェクトクラスの配列

user-object-classes

person,uidObject

新規のユーザーレコード作成のためのオブジェクトクラスの配列


IdentityManagerにアクセスできるようにするには、下のようにSeamコンポーネントにインジェクトします。

  @In IdentityManager identityManager;

あるいは、静的なinstance()メソッド経由でアクセスします。

  IdentityManager identityManager = IdentityManager.instance();

下のテーブルにIdentityManagerのAPIのメソッドを示します。

表 15.4. ID管理のAPI

メソッド

戻り値

詳細

createUser(String name, String password)

ブール型

指定されたusernameとpasswordで新規ユーザーのアカウントを作成します。 もし作成が成功すればtrueを、さもなくばfalseを返します。

deleteUser(String name)

ブール型

指定された名前のユーザーを削除します。 もし成功すればtrueを、さもなくばfalseを返します。

createRole(String role)

ブール型

指定された名前で新規のロールを作成します。 もし作成が成功すればtrueを、さもなくばfalseを返します。

deleteRole(String name)

ブール型

指定された名前のロールを削除します。 もし作成が成功すればtrueを、さもなくばfalseを返します。

enableUser(String name)

ブール型

指定された名前のユーザーアカウントを活性化します。 活性化されていないアカウントは認証の対象とはなりません。もし成功すればtrueを、さもなくばfalseを返します

disableUser(String name)

ブール型

指定された名前のユーザーアカウントを不活化します。 もし成功すればtrueを、さもなくばfalseを返します。

changePassword(String name, String password)

ブール型

指定された名前のユーザーアカウントのpasswordの変更をします。 もし成功すればtrueを、さもなくばfalseを返します。

isUserEnabled(String name)

ブール型

もし、指定されたユーザーのアカウントが活性化されていれば trueを、さもなくばfalseを返します。

grantRole(String name, String role)

ブール型

特定のロールをユーザーやロールに権限を付与します。 ロールは既に存在していることが必要です。 ロールの付与が成功した場合にはtrueを返し、もしそのロールがユーザーに既に付与されていた場合にはfalseを返します。

revokeRole(String name, String role)

ブール型

特定のユーザーあるいはロールから指定したロールを取り消します。 ユーザーが当該のロールのメンバーであり、かつ取り消しが成功した場合にはtrueを返し、ユーザーが当該ロールのメンバーでなければfalseを返します。

userExists(String name)

ブール型

もし、当該のユーザーが存在すればtrue、さもなくばfalseを返します。

listUsers()

リスト

ABC順にソートされたすべてのユーザー名の一覧を返します。

listUsers(String filter)

リスト

指定されたパラメータでフィルタしたユーザー名のリストをABC順にソートして返します

listRoles()

リスト

すべてのロール名の一覧を返します

getGrantedRoles(String name)

リスト

指定されたユーザーに明示的に認められたロール名の一覧を返します

getImpliedRoles(String name)

リスト

指定されたユーザー名に対して暗示的に付与されているすべてのロール名のリストを返します。 暗示的に付与されているロールとは、ユーザーに直接付与されているロールではなく、ユーザーが所属するロールに対して付与されているロールを言います。 例えば、adminロールはuserロールのメンバーであり、ユーザーがadminロールのメンバーであれば、このユーザーには暗示的にadminロールと userロールが付与されている。

authenticate(String name, String password)

ブール型

設定されたIDストアを使ってusernameとpasswordを認証します。 認証が成功すればtrue、失敗すればfalseを返します。 認証が成功しても、このメソッドの戻り値以外は何も変化しませんし、Identityコンポーネントの状態も変化しません。 loginを適切に行うためにはIdentity.login()が使用されなければなりません。

addRoleToGroup(String role, String group)

ブール型

特定のロールを指定したグループのメンバーに追加します。 操作が成功した場合にtrueを返します。

removeRoleFromGroup(String role, String group)

ブール型

指定されたロールを指定されたグループから削除します。 もし成功すればtrueを返します。

listRoles()

リスト

すべてのロール名のリスト


ID管理APIを使うためには、ユーザーはそのメソッドを呼び出す適切な権限を持っている必要があります。 以下の表にIdentityManagerにある個々のメソッドの起動に必要な権限の一覧を示します。 権限はリテラル文字列で指定します。


下の例では、adminロールのメンバーすべてが、すべてのID管理関連のメソッドへのアクセス権を付与されているセキュリティルールを示しています。

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

セキュリティAPIはセキュリティ関連イベントに対応するいくつかのデフォルトのフェースメッセージを発生します。 以下の表には、リソースファイルmessage.propertiesで、これらを上書きするためのメッセージキーを一覧にしています。 もし、これらのメッセージを出さないようにするのであれば、リソースファイルで対応するキーの値をブランクにしてください。


SeamのセキュリティAPIは、コンポーネント、コンポーネントのメソッド、それにページに対して多くの認可機能を提供します。 ここでは、それぞれの機能について説明します。 ここで説明するような高度なセキュリティ機能(ルールベースの認可のような)を使用する場合にはcomponents.xmlに前述のような設定を記述しておかなければならない、ということに留意してください。

Seamのセキュリティは必要なセキュリティ権限を持たないユーザーの操作を行わせないように、ロールとパーミッションによりユーザーの操作を制限する事を前提として設計されています。 SeamセキュリティAPIの提供する認可メカニズムは、ロールとパーミッションによるユーザー管理の概念に基づいて作られており、複数のアプリケーションリソース保護の方法を提供する拡張可能なフレームワークを提供しています。

それでは、もっとも簡単な形式の認可、コンポーネントのセキュリティについて@Restrictアノテーションから見てゆきましょう。

@Restrictアノテーションにより、Seamのコンポーネントにはクラスあるいはメソッドレベルでのセキュリティを付与する事ができます。 もし、クラスとその中のメソッドの両方に@Restrictアノテーションがあった場合には、メソッドレベルの制限が優先され、クラスレベルの制限は結果として適用されません。 もし、メソッドの起動がセキュリティチェックで失敗した場合には、Identity.checkRestriction()単位で例外が発生します。 コンポーネントレベルでの@Restrictアノテーションは、そのコンポーネントのすべてのメソッドに@Restrictをアノテートしたのと同じことになります。

空の@Restrictcomponent:methodNameを意味します。 下のようなコンポーネントの例を見てみましょう。

@Name("account")

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

この例では、delete()を呼び出すためにはaccount:deleteという権限が必要な事を暗黙的に示しています。 同様の内容は@Restrict("#{s:hasPermission('account','delete')}")と記述する事もできます。他の例についても見てゆきましょう。

@Restrict @Name("account")

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

ここでは、コンポーネントクラスに@Restrictとアノテーションが付記されています。これは、`Restrictがオーバーライドされない限り、パーミッションのチェックが暗示的に要求されることを示しています。この例の場合、insert()account:insertのパーミッションを必要とし、delete()はユーザーがadminロールに属していることが必要な事を示しています。

先に進む前に、上の例で見た #{s:hasRole()} 式について見てみましょう。 s:hasRoles:hasPermissionもEL式であり、 Identityクラスの同様の名前のメソッドに対応します。 これらセキュリティAPIのすべてについてEL式の中で使う事ができます。

EL式とすることで、@Restrictアノテーションは、Seamコンテキスト中のどのようなオブジェクトの値でも参照することができるようになります。 これは、特定のオブジェクトのインスタンスをチェックしてパーミッションを決定する場合に非常に有効な方法です。下の例を見てみましょう。

@Name("account")

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

ここで興味深いのは、hasPermission()というファンクション中でselectedAccoutを参照している事です。 この変数の値はSeamのコンテキスト中で検索され、IdentityhasPermission()に渡され、この例の場合、特定のAccountのオブジェクトに対する変更許可を持っているかを決定しています。

適切なユーザーインタフェースのデザインの一つとして、ユーザーが使用する権限を有しないオプションの表示をしないようにすることがあります。 Seamのセキュリティはユーザーの権限に応じて、コンポーネントのセキュリティで使用したのと同様にEL式を使用する事により1)ページ単位 2)個々のコントロール単位 で描画を管理する事ができます。

インタフェースのセキュリティの例について見てゆきましょう。 まず最初に、ログインしていないユーザーの時だけ表示されるログインフォームについて考えてみましょう。 identity.isLoggedIn()属性を使えば下のように記述できます。


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

もしユーザーがログインしていなければ、ログインフォームが表示されます(実に単純ですね)。 次に、managerロールを持っている人達だけがアクセス可能なメニューが必要だと仮定しましょう。 このような場合の一つの方法として、下に例を示したあります。


<h:outputLink action="#{reports.listManagerReports}" rendered="#{s:hasRole('manager')}">
    Manager Reports
</h:outputLink
>

これも、たいへんシンプルで、ユーザーがmanagerロールを持っていなければ、outputLinkは描画されません。rendered属性は一般に制御そのものに使われたり、<s:div><s:span>の中で制御の目的に使われます。

次にもう少し複雑な例: h:dataTableの制御用のアクションリンクの表示非表示をユーザーの権限により制御する事を考えます。 EL式s:hasPermissionにより、ユーザーが必要な権限を持っているか否かを決定するために必要なオブジェクトをパラメータとして渡すことができます。 以下に、セキュリティを向上させたリンクを持たせたh:dataTableの例を示します。


<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
>

Seamのセキュリティは、エンティティ単位でのread,insert,updateおよびdelete操作に対してのセキュリティ制約をかけることを可能にしています。

エンティティクラスのアクション全部に対してセキュリティをかけたいのであれば、下のようにクラスに@Restrictアノテーションを付記します。

@Entity

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

もし、@Restrictが評価式無しで付記されていれば、デフォルトとしてentity:actionのパーミッションがチェックされます。 ここで、パーミッションの対象はエンティティのインスタンスで、actionread, insert, update あるいは deleteのいずれかです。

また、下のようにエンティティのライフサイクルに@Restrict アノテーションを付記することにより、特定の操作だけに制約を課すことができます。

ここではinsert操作に対してのセキュリティチェックをするためのエンティティの設定方法を示しています。 ここで注意していただきたいのは、メソッドの内容はセキュリティと関係なく、アノテーションの仕方が重要な事です。



  @PrePersist @Restrict
  public void prePersist() {}
   

これは、認証されているユーザーが新規にMemberBlogレコードを追加する事ができるか否かをチェックする、エンティティ権限ルールの例(サンプルソースのseamspaceのコードから)です。 セキュリティチェックの対象となるエンティティは自動的にワーキングメモリー(この場合、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;

このルールはPrincipalファクトで示される現在の認証ユーザーがブログのエントリを作成したメンバーと同じ名前であればmemberBlog:insertパーミッションを付与します。 例示したコード中にある、構造体 "principal: Principal()" は認証の過程で挿入された ワーキングメモリ中のPrincipalオブジェクトへの変数結合で、変数principalと命名されています。 変数結合にする事により、他の場所で値が参照可能となり、下のようにPrincipal名とメンバーのユーザー名を比較する事ができます。 詳細は、JBoss Rules ドキュメントを参照してください。

最後に、JPAプロバイダをSeamセキュリティと統合するために、リスナークラスをインストールします。

Seamは@Restrictに代わるアノテーションをいくつか持っており、これらを使う事により@Restrictとは異なりEL式の評価を行わないので、コンパイル時の安全性を提供します。

Seamには標準のCRUD操作に関するパーミッション用のアノテーションが提供されていますが、独自のアノテーションを作成する事もできます。 以下はorg.jboss.seam.annotations.securityパッケージで配布されているアノテーションです。

これらのアノテーションを使うには、単にセキュリティチェックを行いたいメソッドやパラメータをアノテートするだけです。 メソッドがアノテートされた場合には、アクセス権のチェックの対象となるターゲットクラスも指定する必要があります。 以下の例を見てください。

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

この例ではユーザーが新規のCustomerオブジェクトを作成する権限があるか否かパーミッションチェックを行います。 パーミッションチェックの対象はCustomer.classjava.lang.Classインスタンスそのもの)で、アクションはアノテーション名の小文字に変換されたもの、ここではinsert、となります。

同様にコンポーネントのメソッドのパラメータに対してもアノテートする事ができます。 これを行った場合には、パラメータの値がアクセス権チェックの対象ですから、アクセス権のターゲットを指定する必要はありません。

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

下のように、独自のセキュリティアノテーションを作る場合には、単に@PermissionCheckとアノテートするだけです。

@Target({METHOD, PARAMETER})

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

もしデフォルトのアクセス権アクション名(アノテーション名の小文字版)を他の値で上書きする必要があれば、@PermissionCheckアノテーション内にその値を指定することができます。

@PermissionCheck("upgrade")

Seamセキュリティはアプリケーションに対するパーミッションの決定に対して拡張可能なフレームワークを提供します。 下のクラスダイアグラム図にはSeamの提供するパーミッションフレームワークの主要コンポーネントについて示しています。

関連するクラスについての詳細を以下のセクションに示します

実際には、これは個々のオブジェクトのアクセス権を決定するメソッドを提供するインタフェースです。 Seamは以下のPermissionResolverを内蔵しています。 それぞれの詳細はこの章の後半で説明します。

独自のパーミッションリゾルバを作成するためには、下の表にあるPermissionResolverインタフェースに定義されている二つのメソッドを実装します。 独自のPermissionResolver実装をSeamプロジェクトにデプロイする事により、プロジェクトがデプロイされる時(立ち上がり時)に自動的にスキャンされResolverChainに組み込まれます。


注意

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

ResolverChainPermissionResolversを順番に並べたリストを持っており、このリストに従い、特定のオブジェクトクラス、あるいはパーミッション対象についてのパーミッションを解決します。

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

下のシークエンス図にパーミッションチェック時のパーミッションフレームワーク内のコンポーネント相互の作用を示します。 パーミッションチェックは、例えば、セキュリティインタセプタ、EL式s:hasPermission、あるいはAPIIdentity.checkPermissionを呼び出す等、複数の方法により呼び出されます。

Seamに内蔵されているパーミッションリゾルバーの一つRuleBasedPermissionResolverは、Drools(JBoss Rules)によるセキュリティルールに基づいたパーミッションの評価を受け付けます。 ルールエンジンを使う事の利点は; 1)ユーザーパーミッションの評価に使用されるビジネスロジックを一か所にまとめることができる 2)スピードーDroolは効率の良いアルゴリズムを使用し、多くの条件の元に多くの複雑なルールを評価することが可能になっています。

RuleBasedPermissionResolverを設定するためには、components.xmlにDroolのルールベースが設定されている必要があります。 このルールベースは下の例のように、デフォルトでsecurityRulesと命名されていることを仮定しています。


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

デフォルトのルールベースの名前はRuleBasedPermissionResolversecurity-rules属性で上書きする事ができます。

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

RuleBaseコンポーネントを設定したら、次にセキュリティルールの記述をします。

セキュリティルールを作成するためには、まずアプリケーションのjarファイルの/META-INFディレクトリ下に新規のルールファイルを作ります。 通常このファイルはsecurity.drlのように命名されますが、components.xmlに別途指定しておけば、どのようにでも命名する事ができます。

セキュリティルールの内容は? この段階ではDroolsのドキュメントから適当に拝借してくるのが良いかもしれませんが、ここでは非常に単純な例から始めてみる事にしましょう

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

では、一つづつ見てゆきましょう。 最初はパッケージ宣言です。 Droolのパッケージはルールの集まりで、ルールベースの範囲外とは何ら関わりが無いので、パッケージの名前は任意で構いません。

つぎに、PermissionCheckクラスとRoleクラスに関するいくつかのインポート文があります。 これらのインポート文は、これから使うルールでこれらのクラスを参照する事をルールエンジンに対して伝えています。

そして、ルールの記述コード。それぞれのルールは、ルールごとにユニークな名前が与えられている必要があります (通常は、ルールの目的をルールの名前にします。) この例の場合、CanUserDeleteCustomersがルールの名前で、読んで字の如く、顧客レコードの削除をできるかできないかのチェックに使用します。

ルールの記述が二つの部分から成っている事がわかります。ルールは左部分 (LHS) と右部分 (RHS) として知られている部分から成り立っています。LHSはルールの条件部分 (即ち、ルールが実行されるために満たさなければならない条件のリスト) を規定しています。 LHSはwhenで表されるセクションにあり、また、RHSはLHSが満たされた場合に実行されるアクション、あるいは結果を記述しています。 RHSはthen以降の部分に記述します。 また、ルールの最後はendで終了します。

ルールについてLHSを見ると、二つの条件がある事が分かります。 まず、最初の条件を見てみましょう。

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

この条件は、簡単な英語で「ワーキングメモリ中に、target属性として"customer"を持ち、target属性として"delete"を持つPermissionCheckオブジェクトが存在しなければならない」と示しています。

ワーキングメモリって何? Droolsの技術用語で、ルールエンジンがパーミッションチェックをするために必要なコンテキスト情報を保持しているセッションスコープのオブジェクトの事を「ワーキングメモリ」と呼びます。 hasPermission()メソッドが呼ばれる都度、仮のPermissionCheckオブジェクト、あるいはファクト(Fact)がワーキングメモリに挿入されます。 このPermissionCheckは今からチェックされるパーミッションに対応しており、例えばhasPermission("account", "create")を呼び出すと、target属性が "account"でaction属性が"create"であるPermissionCheckオブジェクトがワーキングメモリに挿入され、パーミッションチェックが終了するまで存在します。

PermissionCheckファクトの他に、認証されたユーザーが所属するロールのorg.jboss.seam.security.Roleファクトがあります。 これらの Roleファクトはパーミッションチェックの開始の都度ユーザーの認証されたロールと同期されます。 従って、パーミッションチェックに使用されたワーキングメモリ中のRoleオブジェクトは、もし認証されたユーザーがそのロールに所属していなければ、次回のパーミッションチェックの前に削除されます。 ワーキングメモリにはPermissionCheckRoleファクトの他に認証の過程で作成されたjava.security.Principalオブジェクトが保持されています。

これ以外にパラメータとしてオブジェクトを渡しRuleBasedPermissionResolver.instance().getSecurityContext().insert()を呼び出すことにより、追加でワーキングメモリ中に長期に生存するファクトを挿入する事ができます。 例外として、先に説明したようにRoleオブジェクトはパーミッションチェックの都度同期されるために、はワーキングメモリ中に長期に生存するファクトとする事はできません。

先の例に戻り、LHSがc:で始まっていることに気がつくと思います。 これは、変数結合を表しており、条件のマッチングに利用されるオブジェクトへの参照を意味しています(この例の場合はPermissionCheck)。 LHSの2行目には下の記述があります。

Role(name == "admin")

この条件はワーキングメモリ中に"admin"というnameRoleオブジェクトが存在しなければならない事を示しています。 先述したように、ユーザーのロールはパーミッションチェックの開始の都度ワーキングメモリに挿入されますので、この条件は結果として「adminロールに所属するユーザーでcustomer:deleteの許可を求めているのであれば、これを認めます」という事を示しています。

ルールが適用されると、何が起こるのでしょうか? 次にルールのRHS側を見てみましょう。

c.grant()

RHSはJavaコードから成っており、この例の場合はcというオブジェクト (既に述べたように、PermissionCheckオブジェクトへの変数結合) のgrant()メソッドが起動されます。PermissionCheckオブジェクトのnameactionプロパティ以外にfalseに初期設定されたgrantedプロパティが存在します。PermissionCheckgrant()を呼ぶことにより、grantedプロパティはtrueにセットされ、パーミッションのチェックが成功し、ユーザーはパーミッションで決められたアクションについて実行することができるようになります。

Seamに内蔵されているパーミッションリゾルバーにはこれ以外にPersistentPermissionResolverがあり、これはリレーショナルデータベースのような永続的保存場所からパーミッションを読み込むことが可能で、ACLスタイルのインスタンスベースのセキュリティを提供しており、個別のユーザーとロールに対して特定のパーミッションを指定する事ができます。 また、任意の名前のパーミッションターゲットを指定して保存する事が可能です。

PersistentPermissionResolverは、パーミッションを保存しているバックエンドの保存場所との接続のためにパーミッションストアを必要とします。 Seamは標準で一つのPermissionStore実装JpaPermissionStoreを提供しており、リレーショナルデータベースにパーミッションを保存します。 下記のメソッドを定義しているPermissionStoreインタフェースを実装することにより、独自のパーミッションストアを作成する事も可能です。

表 15.8. パーミッションストアのインタフェース

戻り値の型

メソッド

詳細

List<Permission>

listPermissions(Object target)

このメソッドは対象のオブジェクトに付与されているすべての権限を表すPermissionオブジェクトのListを返します。

List<Permission>

listPermissions(Object target, String action)

このメソッドは対象のオブジェクトに付与されている特定のアクションに対するすべての権限を表すPermissionオブジェクトのListを返します。

List<Permission>

listPermissions(Set<Object> targets, String action)

このメソッドは対象の一連のオブジェクトに付与されている特定のアクションに対するすべての権限を表すPermissionオブジェクトのListを返します

ブール型

grantPermission(Permission)

このメソッドは特定のPermissionオブジェクトをバックエンドのストレージに保持し、成功すればtrueを返します。

ブール型

grantPermissions(List<Permission> permissions)

このメソッドは指定された Listにある、すべてのPermissionオブジェクトをバックエンドのストレージに保持し、成功すればtrueを返します。

ブール型

revokePermission(Permission permission)

このメソッドは指定されたPermissionオブジェクトをストレージから削除します。

ブール型

revokePermissions(List<Permission> permissions)

このメソッドは指定されたリストにあるすべてのPermissionオブジェクトをストレージから削除します。

List<String>

listAvailableActions(Object target)

このメソッドは指定された対象オブジェクトクラスに対して可能なアクション(文字列型)のリストを返します。 特定のクラスのパーミッションを付与するためのユーザーインタフェースを作成するためにパーミッション管理と共に使用されます。


これはデフォルトの(また、Seamが提供する唯一の)PermissionStoreの実装で、パーミッションの保存にリレーショナルデータベースを使用しています。 使用するためにはユーザーとロールのパーミッションの保存に係る一つないし二つのエンティティクラスの設定が必要になります。 これらのエンティティクラスは保存されているレコードとエンティティの属性がパーミッションのどれに対応するのかを設定するための特別なセキュリティに関するアノテーションでアノテートされている必要があります。

もしユーザーパーミッションとロールパーミッションに同一のエンティティ(一つのDBテーブル)を使うのであれば、user-permission-class属性を設定します。 ユーザーパーミッションとロールパーミッションを別々のテーブルに保持するのであれば、user-permission-class属性に加えてrole-permission-class属性を設定する必要があります。

例えば、ユーザーとロールのパーミッションを一つのエンティティクラスに保存するよう設定する場合は次のようになります。


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

ユーザーパーミッションとロールパーミッションを別のエンティティクラスに保存する場合の設定は次のようになります。


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

先述のように、ユーザーとロールのパーミッションを保持するエンティティクラスはorg.jboss.seam.annotations.security.permissionパッケージにある特別なアノテーションを設定されている必要があります。 下の表にこれらのアノテーションと、その使用方法の説明を示します。

表 15.9. エンティティ パーミッション アノテーション

アノテーション

ターゲット

詳細

@PermissionTarget

フィールド、メソッド

このアノテーションはパーミッションの対象を含んでいるエンティティの属性を示します。 この属性はjava.lang.Stringでなければなりません。

@PermissionAction

フィールド、メソッド

このアノテーションはパーミッションアクションを含んでいるエンティティの属性を示します。 この属性はjava.lang.Stringでなければなりません。

@PermissionUser

フィールド、メソッド

このアノテーションはパーミッションの受益ユーザーを含んでいるエンティティの属性を示します。 この属性はjava.lang.Stringで、かつユーザーのusernameを含んでいなければなりません。

@PermissionRole

フィールド、メソッド

このアノテーションはパーミッションの受益ロールを含んでいるエンティティの属性を示します。 この属性はjava.lang.Stringで、かつロール名を含んでいなければなりません。

@PermissionDiscriminator

フィールド、メソッド

このアノテーションはユーザーとロールパーミッションを同じエンティティ(テーブル)に保存する場合に使用します。 エンティティのユーザーとロールパーミッション属性の識別のために使用します。 デフォルトで、userストリングリテラルを含むカラムをユーザーパーミッションのカラムと見なし、roleストリングリテラルを含むカラムをロールパーミッションのカラムと見なします。 アノテーション中でuserValueroleValue属性を指定する事により、これらのデフォルト値は上書きする事ができます。 例えば、userの代わりにuを、roleの代わりにrを使うとすると、アノテーションは下のようになります。

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

この例ではユーザーとロールパーミッションが一つのエンティティクラスに保持されています。 下に示したクラスはサンプルのSeamSpaceからのものです。

@Entity

@Name("message")
@Scope(EVENT)
public class Message implements Serializable
{
   private Long id;
   private String title;
   private String text;
   private boolean read;
   private Date datetime;
   
   @Id @GeneratedValue
   public Long getId() {
      return id;
   }
   public void setId(Long id) {
      this.id = id;
   }
   
   @NotNull @Length(max=100)
   public String getTitle() {
      return title;
   }
   public void setTitle(String title) {
      this.title = title;
   }
   
   @NotNull @Lob
   public String getText() {
      return text;
   }
   public void setText(String text) {
      this.text = text;
   }
   
   @NotNull
   public boolean isRead() {
      return read;
   }
   public void setRead(boolean read) {
      this.read = read;
   }
   
   @NotNull 
   @Basic @Temporal(TemporalType.TIMESTAMP)
   public Date getDatetime() {
      return datetime;
   }
   public void setDatetime(Date datetime) {
      this.datetime = datetime;
   }
   
}

上の例に見るように、getDiscriminator()メソッドが @PermissionDiscriminatorでアノテートされ、どのレコードがユーザーのパーミッションを示し、どのレコードがロールパーミッションを示しているかをJpaPermissionStoreに示しています。 さらに、getRecipient()メソッドが@PermissionUser@PermissionRoleでアノテートされています。 これは、間違いではなく discriminator属性の値により、エンティティのrecipient属性の内容をユーザーの名前、あるいはロールの名前として処理する事を示しています。

デフォルトでは、一つの対象オブジェクトと受益者に対する複数のパーミッションは、一つのデータベースレコードとしてaction属性(DBではカラム)に複数のアクションをコンマで区切って記述され、保持されます。 大量のパーミッション情報を、物理的な保存領域を抑えてデータベースに保存するために、パーミッションアクションにコンマ区切りの文字列リストの代わりに、整数のビットマスク値を使用する事ができます。

例えば、受益者 "Bob"が特定のMemberImage(エンティティBean)インスタンスに対して viewcommentのパーミッションが付与されていた場合、パーミッションエンティティのaction属性は、二つのパーミッションアクションを付与されていることを示し"view,comment"を含んでいます。 代わりに、ビットマスクをパーミッションアクションに使用すると下のようになります。

@Permissions({

   @Permission(action = "view", mask = 1),
   @Permission(action = "comment", mask = 2)
})
@Entity
public class MemberImage implements Serializable {

action属性は、この場合単に"3"(bit 1 と 2 がonの状態)となります。 特定の対象クラスに対する大量のアクション許可を記述する場合には、アクションにビットマスクを使用する事により、明らかにパーミッションレコードの保存に必要な容量を圧縮する事ができます。

maskの値が2のn乗になっている事は明らかに重要です。

JpaPermissionStoreは、パーミッションを保存したり、参照したりするときに対象のインスタンスのパーミッションについて効果的に操作を行うために、対象インスタンスを一意に特定できる必要があります。 これを実現するために、ユニークなIDを生成するよう個々の対象となるクラスに対してidentifier strategyがアサインされます。 それぞれのID戦略実装により、クラスのタイプに応じてユニークなIDの生成が行われます。

IdentifierStrategyインタフェースはたいへんに単純で、二つのメソッドを宣言しているだけです。

public interface IdentifierStrategy {

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

最初のメソッドcanIdentify()は、識別子ストラテジーが指定されたターゲットクラスに対してユニークな識別子を生成可能な場合にtrueを返します。 第2のメソッドgetIdentifier()は指定されたターゲットオブジェクトに対してユニークな識別子の値を返します。

Seamは二つのIdentifierStrategy実装、ClassIdentifierStrategyEntityIdentifierStrategyを提供しています(詳細は次のセクション)。

特定のクラスに対して、特別のID戦略を使用するよう明示的に設定するには、org.jboss.seam.annotations.security.permission.Identifierアノテーションがされ、IdentifierStrategy インタフェースの実装に値が設定されている必要があります。 オプションとしてname属性を指定する事も可能ですが、この指定が及ぼす効果は IdentifierStrategyの実際の実装に依存します。

このID戦略はエンティティBean毎にユニークなIDを割り当てる方法で、エンティティのプライマリキーを示す文字列とエンティティの名前をつなぎ合わせて、IDを生成しています。 IDの名前セクションの生成ルールはClassIdentifierStrategyと同様です。 プライマリキー値 (即ち、エンティティのid ) は PersistenceProviderコンポーネントを使って取得する事ができ、アプリケーションでどの永続性実装を使用しているかに依存せずに値を決める事ができます。 @Entityでアノテートされていないエンティティについては, エンティティクラス自身に下のように明示的にID戦略を設定する事が必要です。

@Identifier(value = EntityIdentifierStrategy.class)

public class Customer { 

生成される識別子の例として、下のようなエンティティクラスを考えてみましょう。

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

id1Customerのインスタンスに対する識別子は"Customer:1"となります。 もし、エンティティクラスに明示的な識別子名のアノテーションがあれば、

@Entity

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

結果として、id123Customerは "cust:123"という識別子を持つことになります。

Seamセキュリティでユーザーとロールの管理のために提供しているID管理APIと同様に、永続的なユーザーのアクセス権の管理のためのパーミッションマネージメントAPIを PermissionManagerにより提供しています。

PermissionManagerコンポーネントはパーミッションを管理するための多くのメソッドを持つアプリケーションスコープのSeamのコンポーネントです。 使用する前にパーミッションストアを設定する必要があります(デフォルトで、JpaPermissionStoreが存在すれば、これを使うようになります)。 明示的に別のパーミッションストアを設定する場合には、components.xmlpermission-storeを設定します。



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

以下の表にPermissionManagerの提供するメソッドの詳細を示します。

表 15.11. パーミッションマネージャAPIのメソッド

戻り値の型

メソッド

詳細

List<Permission>

listPermissions(Object target, String action)

指定されたターゲットとアクションに対する承認されたすべてのパーミッションを示すPermissionオブジェクトを返します。

List<Permission>

listPermissions(Object target)

指定されたターゲットとアクションに対する承認されたすべてのパーミッションを示すPermissionオブジェクトを返します。

ブール型

grantPermission(Permission permission)

バックエンドのパーミッションストアに指定したPermissionsをセーブする。 操作に成功した場合trueを返す。

ブール型

grantPermissions(List<Permission> permissions)

バックエンドのパーミッションストアに指定した複数のPermissionsをセーブする。 操作に成功した場合trueを返す。

ブール型

revokePermission(Permission permission)

バックエンドのパーミッションストアから指定したPermissionsを取り除く(削除する)。操作に成功した場合trueを返す。

ブール型

revokePermissions(List<Permission> permissions)

バックエンドのパーミッションストアから指定した複数のPermissionsを取り除く(削除する)。 操作に成功した場合trueを返す。

List<String>

listAvailableActions(Object target)

対象ターゲットに対する適用可能なアクションのリストを返す。 返されるアクションはターゲットオブジェクトクラスに設定されている@Permission アノテーションに依存する。


SeamはHTTPSプロトコルによるpageのセキュリティを基本的な部分についてサポートしています。 この機能は、pages.xmlで必要なページについてschemeを指定することにより簡単に設定することができます。 下の例では/login.xhtmlでHTTPSを使う様に設定しています。


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

また、この設定は自動的にJSFのs:links:buttonにも引き継がれ (viewで指定した場合) 、リンクも正しいプロトコルで描画されます。前述の例の場合、下のようなリンクも/login.xhtmlがHTTPSを使うように設定されているために、s:link先のlogin.xhmtlにもHTTPSがプロトコルとして使用されます。


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

指定されたプロトコル以外 (正しくないプロトコル) を使って、ページを見ようとすると、正しいプロトコルを使って、指定のページへリダイレクトされます。 schema="https"が指定されているページにhttpでアクセスしようとすると、そのページにhttpsを使ってリダイレクトされます。

すべてのページに対してデフォルトのschemeを設定することも可能で、一部のページに対してHTTPSを使用したい場合などに有効です。 デフォルトスキーマが設定されていない場合には、現在のスキーマを継承します。 従って、ユーザーがHTTPSを必要とするページにアクセスすると、それ以降はHTTPSを必要としないページに対してもHTTPSを使ったアクセスとなります(これは、セキュリティ上は良いのですが、パフォーマンス上は問題があります)。 HTTPをデフォルトのschemeとして指定する場合には次の一行をpages.xmlに追加してください。


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

もちろん、HTTPSを使う必要がなければ、デフォルトのschemaを指定する必要もありません。

以下のように、components.xmlに設定することにより、スキーマが変さらになるたびに現在のHTTPセッションを自動的に無効にする事ができます。


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

このオプションはHTTPSのページから、HTTPのページへの重要データの漏れや、セッションIDの盗聴に対する脆弱性を減少させます。

厳密にはセキュリティAPIの一部ではありませんが、SeamはCAPCHA(Completely Automated Public Turing test to tell Computers and Humans Apart)アルゴリズムを内蔵しており、ウェブ上の自動処理プログラムによりアプリケーションが動作しないようにする事を可能にしています。

以下の表に、セキュリティ関連のイベントによりSeamのセキュリティで発生するイベント(章 6. イベント、インタセプタ、例外処理)を一覧にしました。


場合により、上位権限で処理することが必要な場合があります(たとえば、認証されていないユーザーが、新しいユーザーアカウントを作成する場合)。 Seamはこのような機能をRunAsOperationクラスで提供しています。 このクラスは、限定された一組の操作に対して PrincipalSubject、あるいはユーザーのロールを一時的に上書きする事を可能にします。

以下のコード例ではRunAsOperationの使われ方について、addRole() メソッドを呼び出して操作終了まで特定のロールを付与する方法を示します。 execute() メソッドはより上位の特権で実行するためのコードを持っています。

    new RunAsOperation() {       

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

同様にgetPrincipal()getSubject()メソッドはPrincipalインスタンスおよび Subjectインスタンスを実行中に上書きする事ができます。 最終的にRunAsOperationを実行するためにrun()メソッドを使用します。

アプリケーションが特別なセキュリティを要求する場合には、Identityコンポーネントを拡張する必要がある場合があります。以下の例(説明のためのもので、通常、クレデンシャルはCredentialsコンポーネントにより処理されます)ではcompanyCode フィールドを追加した拡張Identityコンポーネントを示しています。APPLICATIONにより、拡張Identityが内蔵Identityよりも優先されてインストールされることを保証しています。

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

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

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

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

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