PicketLink その3

今回は、picketlink-authorization-idm-jpaの説明をします。
元のコードはここにあります。
この例では、JPAと組み合わせた場合について示されています。

これまでの記事
PicketLink
PicketLink その2

実行環境
PicketLink 2.7.0.Final
WildFly 8.2.0.Final

前提条件
CDI 1.0
JPA 2.0
JSF 2.0

著作権表示

JBoss, Home of Professional Open Source
Copyright 2013, Red Hat, Inc. and/or its affiliates, and individual
contributors by the @authors tag. See the copyright.txt in the 
distribution for a full listing of individual contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,  
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

picketlink-authorization-idm-jpa.war
warファイルの内容は次の通りです。
picketlink-jpa-war

/WEB-INF/beans.xml

<!-- Marker file indicating CDI should be enabled -->
<beans xmlns="http://java.sun.com/xml/ns/javaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="
      http://java.sun.com/xml/ns/javaee 
      http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
</beans>

beans.xmlは前回と同様です。

/WEB-INF/faces-config.xml

<?xml version="1.0"?>
<!-- Marker file indicating JSF should be enabled -->
<faces-config version="2.0" xmlns="http://java.sun.com/xml/ns/javaee"
 xmlns:xi="http://www.w3.org/2001/XInclude"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">

</faces-config>

faces-config.xmlは前回と同様です。

/WEB-INF/picketlink-quickstart-ds.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- This is an unmanaged datasource. It should be used for proofs of concept 
    or testing only. It uses H2, an in memory database that ships with JBoss 
    AS. -->
<datasources xmlns="http://www.jboss.org/ironjacamar/schema"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.jboss.org/ironjacamar/schema http://docs.jboss.org/ironjacamar/schema/datasources_1_0.xsd">
    <!-- The datasource is bound into JNDI at this location. We reference 
        this in META-INF/persistence.xml -->
    <datasource jndi-name="java:jboss/datasources/PicketLinkQuickstartDS"
        pool-name="picketlink-quickstart" enabled="true"
        use-java-context="true">
        <connection-url>jdbc:h2:mem:picketlink-quickstart;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1</connection-url>
        <driver>h2</driver>
        <security>
            <user-name>sa</user-name>
            <password>sa</password>
        </security>
    </datasource>
</datasources>

WildFlyのデータソース設定ファイルです。ここでは、WildFlyに内蔵されているH2データベースエンジンが使用されています。実運用では、他のデータベースを使うことをおすすめします。
WildFly、JBoss以外のアプリケーションサーバの場合は、別の方法での設定が必要です。

/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
   xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="
        http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
   <persistence-unit name="picketlink-default">
      <!-- If you are running in a production environment, add a managed 
         data source, this example data source is just for development and testing! -->
      <!-- The datasource is deployed as WEB-INF/picketlink-quickstart-ds.xml, you
         can find it in the source at src/main/webapp/WEB-INF/picketlink-quickstart-ds.xml -->
      <jta-data-source>java:jboss/datasources/PicketLinkQuickstartDS</jta-data-source>

      <class>org.picketlink.idm.jpa.model.sample.simple.AttributedTypeEntity</class>
      <class>org.picketlink.idm.jpa.model.sample.simple.AccountTypeEntity</class>
      <class>org.picketlink.idm.jpa.model.sample.simple.RoleTypeEntity</class>
      <class>org.picketlink.idm.jpa.model.sample.simple.GroupTypeEntity</class>
      <class>org.picketlink.idm.jpa.model.sample.simple.IdentityTypeEntity</class>
      <class>org.picketlink.idm.jpa.model.sample.simple.RelationshipTypeEntity</class>
      <class>org.picketlink.idm.jpa.model.sample.simple.RelationshipIdentityTypeEntity</class>
      <class>org.picketlink.idm.jpa.model.sample.simple.PartitionTypeEntity</class>
      <class>org.picketlink.idm.jpa.model.sample.simple.PasswordCredentialTypeEntity</class>
      <class>org.picketlink.idm.jpa.model.sample.simple.AttributeTypeEntity</class>

      <properties>
         <!-- Properties for Hibernate -->
         <property name="hibernate.hbm2ddl.auto" value="create-drop" />
         <property name="hibernate.show_sql" value="false" />
      </properties>
   </persistence-unit>
</persistence>

JPAの設定ファイルです。
7行目で、picketlink-defaultという名前を付けています。
12行目のjta-data-sourceは、picketlink-quickstart-ds.xmlの10行目datasourceの jndi-nameに対応しています。

Resources.java

package org.jboss.as.quickstarts.picketlink.authorization.idm.jpa;

import org.picketlink.annotations.PicketLink;

import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Produces;
import javax.faces.context.FacesContext;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

/**
 * This class uses CDI to alias Java EE resources, such as the {@link FacesContext}, to CDI beans
 * 
 * <p>
 * Example injection on a managed bean field:
 * </p>
 * 
 * <pre>
 * &#064;Inject
 * private FacesContext facesContext;
 * </pre>
 */
public class Resources {

    @PersistenceContext(unitName = "picketlink-default")
    private EntityManager em;

    /*
     * Since we are using JPAIdentityStore to store identity-related data, we must provide it with an EntityManager via a
     * producer method or field annotated with the @PicketLink qualifier.
     */
    @Produces
    @PicketLink
    public EntityManager getPicketLinkEntityManager() {
        return em;
    }

    @Produces
    @RequestScoped
    public FacesContext produceFacesContext() {
        return FacesContext.getCurrentInstance();
    }
}

25行目のunitNameは、persistence.xml7行目のpersistence-unitのnameに対応しています。
getPicketLinkEntityManagerメソッドは、PicketLinkでJPAを使用するために定義しています。
produceFacesContextメソッドは、FacesContextを返しています。

IdentityManagementConfiguration.java

package org.jboss.as.quickstarts.picketlink.authorization.idm.jpa;

import org.picketlink.idm.config.IdentityConfiguration;
import org.picketlink.idm.config.IdentityConfigurationBuilder;

import javax.enterprise.inject.Produces;

/**
 * This bean produces the configuration for PicketLink IDM
 * 
 * 
 * @author Shane Bryzak
 *
 */
public class IdentityManagementConfiguration {

    /**
     * This method uses the IdentityConfigurationBuilder to create an IdentityConfiguration, which
     * defines how PicketLink stores identity-related data.  In this particular example, a
     * JPAIdentityStore is configured to allow the identity data to be stored in a relational database
     * using JPA.
     */
    @Produces IdentityConfiguration produceIdentityManagementConfiguration() {
        IdentityConfigurationBuilder builder = new IdentityConfigurationBuilder();

        builder
            .named("default")
            .stores()
            .jpa()
                // Specify that this identity store configuration supports all features
            .supportAllFeatures();

        return builder.build();
    }

}

このクラスは、PicketLinkがidentityに関連するデータをどのように保存するか定義しています。
namedメソッドは、設定に名前を付けます。ここでは、defaultという名前にしています。
storesメソッドは、IdentityStoresConfigurationBuilderを取得します。IdentityStoresConfigurationBuilderは、identityの保存の設定に用いられます。
jpaメソッドは、JPAStoreConfigurationBuilderを取得します。JPAStoreConfigurationBuilderは、JPA向けのidentityの保存の設定に用いられます。
supportAllFeaturesメソッドは、デフォルトの機能が使用可能となるようにしています。

SecurityInitializer.java

package org.jboss.as.quickstarts.picketlink.authorization.idm.jpa;

import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.PartitionManager;
import org.picketlink.idm.RelationshipManager;
import org.picketlink.idm.credential.Password;
import org.picketlink.idm.model.basic.Group;
import org.picketlink.idm.model.basic.Role;
import org.picketlink.idm.model.basic.User;

import javax.annotation.PostConstruct;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.inject.Inject;

import static org.picketlink.idm.model.basic.BasicModel.addToGroup;
import static org.picketlink.idm.model.basic.BasicModel.grantGroupRole;
import static org.picketlink.idm.model.basic.BasicModel.grantRole;

/**
 * This startup bean creates a number of default users, groups and roles when the application is started.
 * 
 * @author Shane Bryzak
 */
@Singleton
@Startup
public class SecurityInitializer {

    @Inject
    private PartitionManager partitionManager;

    @PostConstruct
    public void create() {

        // Create user john
        User john = new User("john");
        john.setEmail("john@acme.com");
        john.setFirstName("John");
        john.setLastName("Smith");

        IdentityManager identityManager = this.partitionManager.createIdentityManager();

        identityManager.add(john);
        identityManager.updateCredential(john, new Password("demo"));

        // Create user mary
        User mary = new User("mary");
        mary.setEmail("mary@acme.com");
        mary.setFirstName("Mary");
        mary.setLastName("Jones");
        identityManager.add(mary);
        identityManager.updateCredential(mary, new Password("demo"));

        // Create user jane
        User jane = new User("jane");
        jane.setEmail("jane@acme.com");
        jane.setFirstName("Jane");
        jane.setLastName("Doe");
        identityManager.add(jane);
        identityManager.updateCredential(jane, new Password("demo"));

        // Create role "manager"
        Role manager = new Role("manager");
        identityManager.add(manager);

        // Create application role "superuser"
        Role superuser = new Role("superuser");
        identityManager.add(superuser);

        // Create group "sales"
        Group sales = new Group("sales");
        identityManager.add(sales);

        RelationshipManager relationshipManager = this.partitionManager.createRelationshipManager();

        // Make john a member of the "sales" group
        addToGroup(relationshipManager, john, sales);

        // Make mary a manager of the "sales" group
        grantGroupRole(relationshipManager, mary, manager, sales);

        // Grant the "superuser" application role to jane
        grantRole(relationshipManager, jane, superuser);
    }
}

このクラスは、ユーザ情報を設定しています。
35行目から60行目で、john、mary、janeの3人のユーザを設定しています。
また、RoleとGroupがあり、62行目から72行目で、managerロール、superuserロール、salesグループを作成しています。
77行目で、johnをsalesグループに入れています。
80行目で、maryをsalesグループのmanagerロールにしています。
83行目で、janeにsuperuserロールを割り当てています。

/index.html

<!-- Plain HTML page that kicks us into the app -->

<html>
<head>
<meta http-equiv="Refresh" content="0; URL=home.jsf">
</head>
</html>

/home.jsfへリダイレクトします(/home.xhtmlが呼び出されます)。

/home.xhtml

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:ui="http://java.sun.com/jsf/facelets">

  <p>
    This quickstart demonstrates how we can use PicketLink IDM's groups and roles to provide authorization checks within an application.
  </p>
  
  <p>
    The identity management configuration is based on PicketLink's JPAIdentityStore, which uses a database to store the application's
    users, groups and roles, and the relationships between them.
  </p>

<ui:fragment rendered="#{identity.loggedIn}">
    <div>Congratulations! You are currently logged in as: <b>#{identity.account.loginName}</b></div>

    <ui:fragment rendered="#{authorizationChecker.hasApplicationRole('superuser')}">
      <div>You have been granted the 'superuser' application role.</div>
    </ui:fragment>
    
    <ui:fragment rendered="#{authorizationChecker.isMember('sales')}">
      <div>You are a member of the 'sales' group.</div>
    </ui:fragment>
    
    <ui:fragment rendered="#{authorizationChecker.hasGroupRole('manager', 'sales')}">
      <div>You have been granted the 'manager' role in the 'sales' group.</div>
    </ui:fragment>

    <h:form>
        <h:commandButton id="logout" value="Log out" action="#{identity.logout}"/>
    </h:form>
</ui:fragment>

<h:form id="loginForm" rendered="#{not identity.loggedIn}">
    <h:messages globalOnly="true"/>

    <div class="loginRow">
        <h:outputLabel for="name" value="Username" styleClass="loginLabel"/>
        <h:inputText id="name" value="#{loginCredentials.userId}"/>
    </div>

    <div class="loginRow">
        <h:outputLabel for="password" value="Password" styleClass="loginLabel"/>
        <h:inputSecret id="password" value="#{loginCredentials.password}" redisplay="true"/>
    </div>

    <div class="loginRow">

    </div>

    <div class="buttons">
        <h:commandButton id="login" value="Login" action="#{loginController.login}" styleClass="loginButton"/>
    </div>

    <p>
      Tip: you can login with any of the following username/password combinations:
      <div>john/demo</div>
      <div>mary/demo</div>
      <div>jane/demo</div>
    </p>
    
    <p>
      Each of these accounts has different privileges assigned to them.
    </p>

</h:form>

<br style="clear:both"/>

</html>

35行目の#{not identity.loggedIn}は、ログイン状態を取得しています。identityは、PicketLinkで定義されています。
ログインしていない場合、35-67行目が表示されます。
picketlink-jpa-home
40行目と45行目のloginCredentialsは、PicketLinkで定義されているDefaultLoginCredentialsクラスです(DefaultLoginCredentialsクラスは、@Named(“loginCredentials”)が付加されています)。
入力されたユーザ名とパスワードは、それぞれloginCredentials.userIdとloginCredentials.passwordに設定されます。
53行目の[Login]ボタンを押すと、LoginController.java(後述)のloginメソッドが実行されます。ユーザ名とパスワードの組み合わせは、SecurityInitializer.javaで設定したjohn/demo、mary/demo、jane/demoの3つです。
認証に失敗すると、この画面に戻って、36行目のメッセージを表示します。
picketlink-jpa-home-failed
認証に成功した場合もこの画面に戻ります。15行目の#{identity.loggedIn}がtrueになるので、15行目から33行目を表示します。
picketlink-jpa-home-success
16行目の#{identity.account.loginName}はログイン名です。
18行目の#{authorizationChecker.hasApplicationRole(‘superuser’)}は、AuthorizationChecker.java(後述)のhasApplicationRoleメソッドを呼び出しています。
ログインユーザがsuperuserロールを持っている場合、このフラグメントが表示されます。
22-24行目は、ログインユーザがsalesグループに属している場合に表示されます。
26-28行目は、ログインユーザが、salesグループのmanagerロールを持っている場合に表示されます。
[Log out]ボタンを押すとログアウトします。

LoginController.java

package org.jboss.as.quickstarts.picketlink.authorization.idm.jpa;

import javax.ejb.Stateless;
import javax.enterprise.context.RequestScoped;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;

import org.picketlink.Identity;
import org.picketlink.Identity.AuthenticationResult;

/**
 * We control the authentication process from this action bean, so that in the event of a failed authentication we can add an
 * appropriate FacesMessage to the response.
 * 
 * @author Shane Bryzak
 * 
 */
@Stateless
@Named
public class LoginController {

    @Inject
    private Identity identity;

    @Inject
    private FacesContext facesContext;

    public void login() {
        AuthenticationResult result = identity.login();
        if (AuthenticationResult.FAILED.equals(result)) {
            facesContext.addMessage(
                    null,
                    new FacesMessage("Authentication was unsuccessful.  Please check your username and password "
                            + "before trying again."));
        }
    }
}

28行目のFacesContextは、Resources.javaのproduceFacesContextメソッドから注入されています。
home.xhtmlで[Login]ボタンが押されると、loginメソッドが実行されます。
31行目でログインが実行されます。入力されたユーザ名とパスワードが既に、それぞれloginCredentials.userIdとloginCredentials.passwordに設定されています。
認証に失敗した場合は、FacesMessageを作成します。

AuthorizationChecker.java

package org.jboss.as.quickstarts.picketlink.authorization.idm.jpa;

import org.picketlink.Identity;
import org.picketlink.idm.IdentityManager;
import org.picketlink.idm.RelationshipManager;
import org.picketlink.idm.model.basic.BasicModel;
import org.picketlink.idm.model.basic.Group;
import org.picketlink.idm.model.basic.Role;

import javax.inject.Inject;
import javax.inject.Named;

import static org.picketlink.idm.model.basic.BasicModel.*;

/**
 * This is a utility bean that may be used by the view layer to determine whether the
 * current user has specific privileges. 
 * 
 * @author Shane Bryzak
 *
 */
@Named 
public class AuthorizationChecker {
    
    @Inject
    private Identity identity;
    
    @Inject 
    private IdentityManager identityManager;

    @Inject
    private RelationshipManager relationshipManager;

    public boolean hasApplicationRole(String roleName) {
        Role role = getRole(this.identityManager, roleName);
        return hasRole(this.relationshipManager, this.identity.getAccount(), role);
    }

    public boolean isMember(String groupName) {
        Group group = getGroup(this.identityManager, groupName);
        return BasicModel.isMember(this.relationshipManager, this.identity.getAccount(), group);
    }

    public boolean hasGroupRole(String roleName, String groupName) {
        Group group = getGroup(this.identityManager, groupName);
        Role role = getRole(this.identityManager, roleName);
        return BasicModel.hasGroupRole(this.relationshipManager, this.identity.getAccount(), role, group);
    }
}

34行目のhasApplicationRoleメソッドは、home.xhtmlの#{authorizationChecker.hasApplicationRole(‘superuser’)}から呼ばれています。このメソッドは、ログインユーザがロールを持っているかどうか検証します。getRoleメソッドとhasRoleメソッドは、Picketlinkで定義されているメソッドです。ここでは、ログインユーザがsuperuserロールを持っている場合にtrueを返します。
39行目のisMemberメソッドは、home.xhtmlの#{authorizationChecker.isMember(‘sales’)}から呼ばれています。このメソッドは、ログインユーザがグループのメンバーかどうか検証します。getGroupメソッドとBasicModel.isMemberメソッドは、Picketlinkで定義されているメソッドです。ここでは、ログインユーザがsalesグループのメンバーの場合にtrueを返しています。
44行目のhasGroupRoleメソッドは、home.xhtmlの#{authorizationChecker.hasGroupRole(‘manager’, ‘sales’)}から呼ばれています。このメソッドは、ログインユーザがグループ内のロールを持っているかどうか検証します。BasicModel.hasGroupRoleメソッドは、Picketlinkで定義されているメソッドです。ここでは、ログインユーザがsalesグループのmanagerロールの場合にtrueを返しています。