Showing posts with label ADF Security. Show all posts
Showing posts with label ADF Security. Show all posts

Tuesday, April 13, 2010

ADF Security: Playing with Weblogic APIs on Authentication Providers

As promised in my last post, in this post I will share with you how to play with the Weblogic API through JMX to handle tasks such as creating users, creating roles, assigning users to roles, and letting users change their own password. This post will clear out the gray areas in implementing user subscription or registration forms and the generation of salted-hashed passwords that can be recognized by weblogic. This post assumes that you have already properly set-up your SQLAuthenticator provider in weblogic.

Though this post is specific to SQLAuthenticator, the concept demonstrated herein is applicable to the Weblogic API as a whole. Just changing the MBEAN_INTERFACE constant to "weblogic.security.providers.authentication.DefaultAuthenticatorMBean", this post already applies to the DefaultAuthenticator.

Prerequisites

  • Setup the SQLAuthenticator required schema
  • Create a Datasource in weblogic console that points to the schema in step 1.
  • Setup SQLAuthenticator provider in weblogic console.

Implementation

The key to playing with Weblogic APIs is through JMX (Java Management Extensions). I cant tell through experience that understanding JMX is not for the faint-hearted, but if you got it, you will realize how simple it is. Below is a sample adapter class that I device so that I will deal with JMX only once, and the rest of my application that needs to access the weblogic API will just point to my adapter class.
package soadev.adapters;

import java.io.Serializable;
import java.io.IOException;
import java.util.Hashtable;
import javax.management.Descriptor;
import javax.management.MBeanServerConnection;
import javax.management.ObjectName;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import javax.naming.Context;

public class SQLAuthenticatorAdapter implements Serializable {
    private static final String MBEAN_INTERFACE = "weblogic.security.providers.authentication.SQLAuthenticatorMBean";
    private MBeanServerConnection connection;
    private JMXConnector connector;
    private ObjectName providerON;
    
    
    public void createUser(String username, String password,
                           String description) throws Exception {
        connection.invoke(providerON, "createUser",
                          new Object[] { username, password, description },
                          new String[] { "java.lang.String",
                                         "java.lang.String",
                                         "java.lang.String" });
    }
    
    public void createGroup(String groupName, String description) throws Exception {
        connection.invoke(providerON, "createGroup",
                          new Object[] { groupName, description },
                          new String[] { "java.lang.String",
                                         "java.lang.String" });
    }
    
    public void addMemberToGroup(String groupName, String username)throws Exception{
        connection.invoke(providerON, "addMemberToGroup",
                          new Object[] { groupName, username },
                          new String[] { "java.lang.String", "java.lang.String" });
    }

    public void changeUserPassword(String username, String oldPassword,
                                   String newPassword) throws Exception {
        connection.invoke(providerON, "changeUserPassword",
                          new Object[] { username, oldPassword, newPassword },
                          new String[] { "java.lang.String",
                                         "java.lang.String",
                                         "java.lang.String" });
    }
    
    public boolean isMember(String parentGroupName, String memberUserOrGroupName, boolean recursive)throws Exception{
        return (Boolean) connection.invoke(providerON, "isMember",
                          new Object[] { parentGroupName, memberUserOrGroupName, recursive },
                          new String []{"java.lang.String", "java.lang.String", "java.lang.Boolean"});
    }
    
    private ObjectName getAuthenticationProviderObjectName(String type)throws Exception{
       
            ObjectName defaultRealm = getDefaultRealm();
            ObjectName[] atnProviders =
                (ObjectName[])connection.getAttribute(defaultRealm,
                                                      "AuthenticationProviders");
            ObjectName MBTservice =
                new ObjectName("com.bea:Name=MBeanTypeService,Type=weblogic.management.mbeanservers.MBeanTypeService");
            for (int p = 0; atnProviders != null && p < atnProviders.length;
                 p++) {
                ObjectName provider = atnProviders[p];
                ModelMBeanInfo info =
                    (ModelMBeanInfo)connection.getMBeanInfo(provider);
                Descriptor desc = info.getMBeanDescriptor();
                String className =
                    (String)desc.getFieldValue("interfaceClassName");
                String[] mba =
                    (String[])connection.invoke(MBTservice, "getSubtypes",
                                                new Object[] { type },
                                                new String[] { "java.lang.String" });
                for (int i = 0; i < mba.length; i++) {
                    if (mba[i].equals(className)) {
                        return provider;
                    }
                }
            }
            return null;
    }
    private ObjectName getDefaultRealm() throws Exception {
        ObjectName service =
            new ObjectName("com.bea:Name=DomainRuntimeService,Type=weblogic.management.mbeanservers.domainruntime.DomainRuntimeServiceMBean");
        ObjectName domainMBean =
            (ObjectName)connection.getAttribute(service, "DomainConfiguration");
        ObjectName securityConfiguration =
            (ObjectName)connection.getAttribute(domainMBean,
                                                "SecurityConfiguration");
        ObjectName defaultRealm =
            (ObjectName)connection.getAttribute(securityConfiguration,
                                                "DefaultRealm");
        return defaultRealm;
    }
    public void connect(){
        String hostname = "localhost";
        String username = "weblogic";
        String password = "weblogic1";
        int port = 7101;
        connect(hostname, username, password, port);
    }
    
    public void connect(String hostname, String username, String password, int port){
        try {       
            String protocol = "t3";
            String jndi =
                "/jndi/weblogic.management.mbeanservers.domainruntime";
            JMXServiceURL serviceURL =
                new JMXServiceURL(protocol, hostname, port, jndi);
            Hashtable env = new Hashtable();
            env.put(Context.SECURITY_PRINCIPAL, username);
            env.put(Context.SECURITY_CREDENTIALS, password);
            env.put(JMXConnectorFactory.PROTOCOL_PROVIDER_PACKAGES,
                    "weblogic.management.remote");
            env.put("jmx.remote.x.request.waiting.timeout", new Long(10000));
            connector = JMXConnectorFactory.connect(serviceURL, env);
            connection = connector.getMBeanServerConnection();
            providerON = getAuthenticationProviderObjectName(MBEAN_INTERFACE);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
    public void close(){
        try {
            connector.close();
        } catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
    }
}

Please update the connect() method with your own credentials and port information if necessary.

Below is a sample java client that utilize this adapter:
package soadev.client;

import soadev.adapters.SQLAuthenticatorAdapter;

public class SQLAuthenticatorAdapterClient {
    private SQLAuthenticatorAdapter adapter = new SQLAuthenticatorAdapter();
    public static void main(String[] args) {
    SQLAuthenticatorAdapterClient client = new SQLAuthenticatorAdapterClient();
       try {
           client.connect();
            client.testCreateUser();
            client.testCreateGroup();
            client.testAddMemberTopGroup();
            client.close();
        } catch (Exception e) {
            // TODO: Add catch code
            client.close();
            e.printStackTrace();
        }
    }
    
    public void testCreateUser()throws Exception{
        String username = "pino";
        String password = "password1";
        String displayName = "Pino SOADEV";
        adapter.createUser(username, password, displayName);
    }
    
    public void testCreateGroup()throws Exception{
        String groupName = "SOADevGroup";
        String description = "This is a especial group created through SQLAuthenticatorAdapter";
        adapter.createGroup(groupName, description);
    }
    
    public void testAddMemberTopGroup()throws Exception{
        String username = "pino";
        String groupName = "SOADevGroup";
        adapter.addMemberToGroup(groupName, username);
    }
    
    public void close(){
        adapter.close();
    }
    public void connect(){
        adapter.connect();
    }
}

Be sure that "Weblogic 10.3 Thin-Client" is included in the libraries and classpath of your client project.

After running the client above, you will see that user "pino", group "SOADevGroup, and the assignment of user "pino" to group "SOADevGroup" were persisted respectively in the USERS, GROUPS, and GROUPMEMBERS tables.

Prototype

//TODO

Conclusion

This post demonstrates how easy it is to play with Weblogic API so that you have something as reference to jump-start your own implementation. More interfaces can be implemented by referring to the resources below. Be wary the examples above do not exemplify proper exception and concurrency handling.

Cheers!

Pino

Resources

Friday, April 9, 2010

ADF Security: SQLAuthenticator is Simply the Best!

When choosing authentication repositories, something that is more familiar can be the best choice. And what is that "something familiar" for us developers?- Of course, the relational database (unless, if you know LDAPs more :D). Relational databases (RDMS) has encryption capability while authentication providers supports cryptographic hashing, so what else could you ask?
Plus the recommendation below of an an expert, who is previously from Bea Weblogic Portal Team:
When it comes to Authentication repositories, my experience tells me that you are safest performance-wise with a database backed authentication store. While customers have certainly been successful with other types of authentication repositories, if you want to minimize risk, the database approach trumps all others.
-- Peter Laird, Architect for Tendril Networks
In our case, relational database is simply the choice. We need not only know - what roles our users have, but also what data they can access based on the organizations that were assigned to them (plus a lot more...).

In this post I will share the knowledge that I have acquired related to the best database-based authentication provider, the SQLAuthenticator.


Some tips on SQLAuthenticator to avoid being miserable :D :
  1. Stick as much as possible to the default schema. With the default schema, you need not worry tweaking the SQL select and insert statements defined on the SQLAuthenticator provider details. For your convenience, below is the script:
    CREATE TABLE USERS (
        U_NAME VARCHAR(200) NOT NULL,
        U_PASSWORD VARCHAR(50) NOT NULL,
        U_DESCRIPTION VARCHAR(1000))
    ;
    ALTER TABLE USERS
       ADD CONSTRAINT PK_USERS
       PRIMARY KEY (U_NAME)
    ;
    CREATE TABLE GROUPS (
        G_NAME VARCHAR(200) NOT NULL,
        G_DESCRIPTION VARCHAR(1000) NULL)
    ;
    ALTER TABLE GROUPS
       ADD CONSTRAINT PK_GROUPS
       PRIMARY KEY (G_NAME)
    ;
    CREATE TABLE GROUPMEMBERS (
        G_NAME VARCHAR(200) NOT NULL,
        G_MEMBER VARCHAR(200) NOT NULL)
    ;
    ALTER TABLE GROUPMEMBERS
       ADD CONSTRAINT PK_GROUPMEMS
       PRIMARY KEY (
          G_NAME, 
          G_MEMBER
       )
    ;
    ALTER TABLE GROUPMEMBERS
       ADD CONSTRAINT FK1_GROUPMEMBERS
       FOREIGN KEY ( G_NAME )
       REFERENCES GROUPS (G_NAME)
       ON DELETE CASCADE
    ;
    
  2. If tip# 1 is not possible, be wary that aside from Users, Groups can also be a member of a given Group. GROUPMEMBERS table is not simply a join table between Users and Groups but also can be a recursion between groups. I believe this is the reason why the default schema did not use surrogate keys (meaningless Ids). Imagine what will happen if you have a User with Long id 1 and Group with the same id 1, then what would the following record from the GROUPMEMBERS table mean?
    GROUP_ID          MEMBER_ID 
       2                 1
    
    Does above mean Group 1 is a member of Group 2?
    Or does it mean USer 1 is a member of Group2?

    Another thing to note - a Group Membership or Grants table implemented like the following script will NOT support membership of groups into other groups which defeats some aspects of weblogic authorization:

    CREATE TABLE JHS_USER_ROLE_GRANTS  
    (  
    ID NUMBER(*, 0) NOT NULL,  
    USR_ID NUMBER(*, 0) NOT NULL,  
    RLE_ID NUMBER(*, 0) NOT NULL  
    );
    

  3. Do not enable Plaintext Passwords. This is to ensure that users are created in the right process. You would not like to see your secret password. Do you?
  4. Be sure to set the "Group Membership Searching" to "limited" and set the "Max Group Membership Search Level" to a value like "5". This is to avoid infinite loop when the in situations where for example Group A is a member of Group B, while Group B is also a member of Group A.
  5. You do not need to change the identity store in Jdeveloper. jazn.xml is perfectly fine.
  6. Andrejus was right -you need not modify the role mapping in weblogic.xml. The default like below is perfectly fine:
    <?xml version = '1.0' encoding = 'windows-1252'?>
    <weblogic-web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                      xsi:schemaLocation="http://www.bea.com/ns/weblogic/weblogic-web-app http://www.bea.com/ns/weblogic/weblogic-web-app/1.0/weblogic-web-app.xsd"
                      xmlns="http://www.bea.com/ns/weblogic/weblogic-web-app">
      <security-role-assignment>
        <role-name>valid-users</role-name>
        <principal-name>users</principal-name>
      </security-role-assignment>
    </weblogic-web-app>
    
  7. For each application role you defined in jazn.xml, create an equivalent group in your SQLAuthenticator provider in weblogic console, and recreate those roles in the Enterprise Roles in jazn.xml. In jazn, make the corresponding enterprise role as member of the appropriate application role.

With these tips, I believe that you could already setup SQLAuthenticator easily. Given enough time, I am planning to consolidate the steps in other blogs to give a one-stop shop in configuring SQLAuthenticator.

In the next post, I will share how to play with weblogic APIs to access our security realm and to do tasks such adding user, letting user change password, listing users and roles, and more using Java (not the WLST):D

Kudus to Edwin Biemond for introducing to us the SQLAuthenticator!
Cheers!

Useful Resources

Sunday, February 14, 2010

ADF UI Shell: Dynamic Tree Menu based on User Roles (ADF Policies)

In this post, I will try to share with you how to create a dynamic tree menu based on the authorization defined in ADF Policies (jazn.xml) in an application built with ADF UI Shell. Menu Items will be added to the tree menu, if the user has a role that was granted view authorization on a certain task-flow or page.


To accomplish the creation of a dynamic menu based on user roles, we need to do at least the following:
  1. Create and populate a Menu Table in the database;
  2. Generate a Menu entity thru the "Entities from Table" wizard in JDeveloper;
  3. Create a session bean to act as facade of our entity;
  4. Create a managed bean to provide tree model of authorized menus in appropriate hierarchy;
  5. Create a .jspx page based on the Oracle Dynamic Tabs Shell Template.
  6. Create a Launcher backing bean that will support our page and provide method to launch new tabs.
  7. Grant view authorization to our taskflows defined in jazn.xml to certain roles.

1) Create and populate a Menu Table in the database;

To support a dynamic menu based on roles we only need to have a table of menu items. A table of roles is not necessary. The key to the solution is the java version of the "#{securityContext.taskFlowViewable['taskFlowId']}" EL expression. Below is the diagram of our table:
ColumnComments
MENU_ID Primary key
DESCRIPTIONThe label that will be displayed on the tree.
DEFINITIONThis will hold taskFlowIds like for example "/WEB-INF/taskflows/gl/natural-acct-list-task-flow.xml#natural-acct-list-task-flow".
PARENT_MENU_IDThe id of the containing menu. This table is recursive.
TYPEThis field will enable us to show different icons per type.
DISPLAY_SEQThe sorting or display sequence of the menus.
IS_MULTIPLE_INSTANCERepresents a boolean value to determine if multiple instance of this taskflow will be allowed on UI Shell tabs.

2) Generate a Menu entity thru the "Entities from Table" wizard in JDeveloper;

Below is a sample generated Menu Entity class (please note the annotations are on the properties instead of the fields - I encountered strange error in related to jpa sessions ("this session is not of the current object but of the parent blah blah...")when I attempted to put the annotations in the fields ).
package blogspot.soadev.model;

import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;

@Entity
@NamedQueries({
  @NamedQuery(name = "findAllMenus", query = "select o from Menu o"),
  @NamedQuery(name = "findRootMenus", query = "select o from Menu o where o.parentMenu IS NULL"),
  @NamedQuery(name = "findTargetRootMenu", query = "select o from Menu o where o.id = :menuId")
})
public class Menu implements Comparable<Menu>, Serializable {   
    private Long id;
    private String description;  
    private String definition;   
    private String type;   
    private Menu parentMenu;   
    private Long displaySeq;  
    private boolean multipleInstance;   
    private List<Menu> childrenMenuList;  
 
    @Column(name="DISPLAY_SEQ", nullable = false)
    public Long getDisplaySeq() {
        return displaySeq;
    }

    public void setDisplaySeq(Long displaySeq) {
        this.displaySeq = displaySeq;
    }
    @Id
    @Column(name="MENU_ID", nullable = false)
    public Long getId() {
        return id;
    }

    public void setId(Long menuId) {
        this.id = menuId;
    }
    @Column(length = 120)
    public String getDescription() {
        return description;
    }

    public void setDescription(String name) {
        this.description = name;
    }
    @Column(length = 120)
    public String getDefinition() {
        return definition;
    }

    public void setDefinition(String taskFlowId) {
        this.definition = taskFlowId;
    }
    @Column(length = 1)
    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
    @ManyToOne
    @JoinColumn(name = "PARENT_MENU_ID")
    public Menu getParentMenu() {
        return parentMenu;
    }

    public void setParentMenu(Menu menu) {
        this.parentMenu = menu;
    }
    
    public void setMultipleInstance(boolean multipleInstance) {
        this.multipleInstance = multipleInstance;
    }
    @Column(name ="IS_MULTIPLE_INSTANCE", length = 1)
    public boolean isMultipleInstance() {
        return multipleInstance;
    }
    @OneToMany(mappedBy = "parentMenu")
    public List<Menu> getChildrenMenuList() {
        Collections.sort(childrenMenuList);
        return childrenMenuList;
    }

    public void setChildrenMenuList(List<Menu> menuList) {
        this.childrenMenuList = menuList;
    }

    public Menu addMenu(Menu menu) {
        getChildrenMenuList().add(menu);
        menu.setParentMenu(this);
        return menu;
    }

    public Menu removeMenu(Menu menu) {
        getChildrenMenuList().remove(menu);
        menu.setParentMenu(null);
        return menu;
    }

    public int compareTo(Menu m) {
        if (m == null){
            return -1;
        }
        return this.displaySeq.compareTo(m.displaySeq);
    }
}
You could note above that I added two named queries: findRootMenus and findTargetMenu. I also implemented a Comparable interface to support sorting of menus based on the desired display sequence.

3) Create a session bean to act as facade of our entity;

Below is my generated session bean:
package blogspot.soadev.service;

import java.util.List;
import javax.ejb.Local;
import javax.ejb.Remote;
import javax.ejb.Stateless;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import blogspot.soadev.model.Menu;

@Stateless(name = "ApplicationManager", mappedName = "DynamicTreeMenuBasedOnRoles-Model-ApplicationManager")
@Remote
@Local
public class ApplicationManagerBean implements ApplicationManager,
                                               ApplicationManagerLocal {
    @PersistenceContext(unitName="Model")
    private EntityManager em;


    /** <code>select o from Menu o</code> */
    public List<Menu> findAllMenus() {
        return em.createNamedQuery("findAllMenus").getResultList();
    }

    /** <code>select o from Menu o where o.id = menuId</code> */
    //I intend it to return a list instead of just a menu.
    public List<Menu> findTargetRootMenu(Long menuId) {
        return em.createNamedQuery("findTargetRootMenu").setParameter("menuId", menuId).getResultList();
    }

    /** <code>select o from Menu o where o.parentMenu IS NULL</code> */
    public List<Menu> findRootMenus() {
        return em.createNamedQuery("findRootMenus").getResultList();
    }
}

4) Create a managed bean to provide tree model of authorized menus in appropriate hierarchy;

Below is the managed bean that we will declare in adfc-config.xml as view scope. This will supply a tree model to our tree component:
package blogspot.soadev.view.managed;

import blogspot.soadev.model.Menu;
import blogspot.soadev.view.util.JSFUtils;
import java.beans.IntrospectionException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import oracle.adf.controller.security.TaskFlowPermission;
import oracle.adf.share.ADFContext;
import oracle.adf.share.security.SecurityContext;
import oracle.adf.share.security.authorization.RegionPermission;
import oracle.binding.BindingContainer;
import oracle.binding.OperationBinding;
import org.apache.myfaces.trinidad.model.ChildPropertyTreeModel;
import org.apache.myfaces.trinidad.model.TreeModel;

public class TreeMenuNavigator implements Serializable {
    private TreeModel model;
    private List<Menu> menuList;
    private List<Menu> authorizedMenuList = new ArrayList<Menu>();
    private Long rootMenuId = 2L; //the root menu Id of my default page
    private Map<Long, Menu> menuMap;

    public void setModel(TreeModel model) {
        this.model = model;
    }

    public TreeModel getModel() throws IntrospectionException {
        if (model == null) {
            model =
                    new ChildPropertyTreeModel(getMenuList(), "childrenMenuList");
        }
        return model;
    }

    public void setMenuList(List<Menu> menuList) {
        this.menuList = menuList;
    }

    public List<Menu> getMenuList() {
        BindingContainer bindings =
            (BindingContainer)JSFUtils.resolveExpression("#{bindings}");
        //I used the findTargetRootMenu() method instead of the findRootMenus
        //because I am reusing the same model for menu of other global tabs as well.
        //A global tab will have a different menu based on the root menu id 
        //that I set on this class whenever I invoke a global tab.
        //for testing purposes you could use the findRootMenus()
        //Please ensure to add the appropriate methodAction in the page definition of the containing .jspx page
        OperationBinding oper =
            (OperationBinding)bindings.getOperationBinding("findTargetRootMenu");
        List<Menu> rootMenuList = (List<Menu>)oper.execute();
        //initialize attributes
        authorizedMenuList = new ArrayList<Menu>();
        menuMap = new HashMap<Long, Menu>();
        menuList = new ArrayList<Menu>();
        reinitializeAuthorizedMenuList(rootMenuList);
        reconstructHierarchicalMenuList(authorizedMenuList);
        List<Menu> resultList = menuList;
        //release attributes that was used in recursive methods
        authorizedMenuList = null;
        menuMap = null;
        menuList = null;
        if (!resultList.isEmpty()) {
            return resultList.get(0).getChildrenMenuList();// I prefer to return the immediate children 
             //rather than the root. But you could try returning resultList instead
            //return resultList;
        }
        return Collections.emptyList();
    }

    private void reinitializeAuthorizedMenuList(List<Menu> menuList) {
        if (menuList == null) {
            return;
        }
        for (Menu menu : menuList) {
            if (isAccessible(menu.getDefinition())) {
                authorizedMenuList.add(menu);
            } else {
                reinitializeAuthorizedMenuList(menu.getChildrenMenuList());
            }
        }
    }


    public void reconstructHierarchy(Menu menu) {
        if (menu == null) {
            return;
        }
        if (menuMap.containsKey(menu.getId())) { //menu already loaded
            return;
        }
        menuMap.put(menu.getId(), menu);
        Menu m = menu.getParentMenu();
        if (m == null) {
            menuList.add(menu);
            return;
        }
        Menu parentCopy = null;
        if (menuMap.containsKey(m.getId())) {
            parentCopy = menuMap.get(m.getId());
        } else {
            parentCopy = copyAttributes(m);
            reconstructHierarchy(parentCopy);
        }
        parentCopy.addMenu(menu);
    }


    public void reconstructHierarchicalMenuList(List<Menu> authorizedMenuList) {
        for (Menu menu : authorizedMenuList) {
            Menu copy = copyAttributes(menu);
            reconstructHierarchy(copy);
        }
    }

    private Menu copyAttributes(Menu menu) {
        if (menu == null) {
            return null;
        }
        Menu copy = new Menu();
        copy.setId(menu.getId());
        copy.setDescription(menu.getDescription());
        copy.setDefinition(menu.getDefinition());
        copy.setParentMenu(menu.getParentMenu());
        copy.setDisplaySeq(menu.getDisplaySeq());
        copy.setType(menu.getType());
        copy.setMultipleInstance(menu.isMultipleInstance());
        return copy;
    }

    //this is the Java version of the "#{securityContext.regionViewable['pageDef']}" EL Expression
    public boolean isRegionViewable(String pageDef) {
        if (pageDef == null) {
            return false;
        }
        RegionPermission permission =
            new RegionPermission(pageDef, RegionPermission.VIEW_ACTION);
        SecurityContext ctx = ADFContext.getCurrent().getSecurityContext();
        return ctx.hasPermission(permission);
    }
    //this is the Java version of the "#{securityContext.taskFlowViewable['taskFlowId']}" EL Expression
    public boolean isTaskFlowViewable(String taskflowId) {
        if (taskflowId == null) {
            return false;
        }
        TaskFlowPermission permission =
            new TaskFlowPermission(taskflowId, TaskFlowPermission.VIEW_ACTION);
        SecurityContext ctx = ADFContext.getCurrent().getSecurityContext();
        return ctx.hasPermission(permission);
    }

    public boolean isAccessible(String definition) {
        return (isRegionViewable(definition) ||
                isTaskFlowViewable(definition));
    }

    public void setRootMenuId(Long rootMenuId) {
        this.rootMenuId = rootMenuId;
        model = new ChildPropertyTreeModel(getMenuList(), "childrenMenuList");
    }

    public Long getRootMenuId() {
        return rootMenuId;
    }
}

5) Create a .jspx page based on the Oracle Dynamic Tabs Shell Template.

Below is a snippet of my jspx page based on an extended UI Shell that shows the source of our tree component:
<af:tree var="node" rowSelection="single" id="menuTree"
                         value="#{viewScope.treeMenuNavigator.model}"
                         initiallyExpanded="true" fetchSize="-1"
                         contentDelivery="immediate">
                  <f:facet name="nodeStamp">
                    <af:panelGroupLayout id="pgl10">
                      <af:outputText value="#{node.description}" id="ot2"
                                     rendered="#{node.definition eq null}"
                                     inlineStyle="font-size:larger; font-weight:bold;"/>
                      <af:commandImageLink text="#{node['description']}" id="pt_cil5"
                                           rendered="#{node.definition  ne null}"
                                           icon="#{node.type eq 'L' ? '/images/List16.png': node.type eq 'C' ? '/images/Maintain16.png': node.type eq 'P' ? '/images/Process16.png': node.type eq 'R' ? '/images/Report16.png':  '/images/Transaction16.png'}"
                                           actionListener="#{backingBeanScope.launcher.launchMenu}"
                                           partialSubmit="true"
                                           immediate="true">
                        <f:attribute name="node" value="#{node}"/>
                      </af:commandImageLink>
                    </af:panelGroupLayout>
                  </f:facet>
                </af:tree>

6) Create a Launcher backing bean that will support our page and provide method to launch new tabs.

Below is our Launcher class declared in backingBean scope in adfc-config.xml:
package blogspot.soadev.view.backing;

import blogspot.soadev.model.Menu;
import javax.faces.component.UIComponent;
import javax.faces.event.ActionEvent;
import oracle.ui.pattern.dynamicShell.TabContext;

public class Launcher {
    
    public void launchMenu(ActionEvent event) {
        UIComponent component = (UIComponent)event.getSource();
        Menu menu = (Menu)component.getAttributes().get("node");
        _launchActivity(menu.getDescription(), menu.getDefinition(),
                       menu.isMultipleInstance());
    }
    private void _launchActivity(String title, String taskflowId, boolean newTab)
    {
      try
      {
        if (newTab)
        {
          TabContext.getCurrentInstance().addTab(
            title,
            taskflowId);
        }
        else
        {
          TabContext.getCurrentInstance().addOrSelectTab(
            title,
            taskflowId);
        }
      }
      catch (TabContext.TabOverflowException toe)
      {
        // causes a dialog to be displayed to the user saying that there are
        // too many tabs open - the new tab will not be opened...
        toe.handleDefault(); 
      }
    }
}
Whew! This is rather a long post. To be continued... Continuation...

7) Grant view authorization to our taskflows defined in jazn.xml to certain roles.

Please refer to this post by Chris Muir for detailed info about the minimum setting to run UI Shell with ADF Security. Be sure to check my comments on the bottom :)

Cheers!

Friday, December 25, 2009

ADF Security: The Two Most Useful Security Expression - #{securityContext.regionViewable[''] and #{securityContext.taskFlowViewable['']

If you have a requirement to conditionally display a tab, a link, or a toolbar button to prevent navigation to a protected page allowed only for some specific roles then I bet that the following security expression will satisfy your need:
1)#{securityContext.regionViewable['your.page.targetPageDef']}

Chris Muir has a nice blog about conditionally displaying the global tabs of the ADF UI Shell in which I was able to comment: ADF UI Shell + ADF Security.

But if your requirement is to conditionally display a menu or a toolbar that will launch a bounded task flow (just like the in the dynamic UI Shell) then the following security expression is what you'll need:
2) #{securityContext.taskFlowViewable['/WEB-INF/yourTaskFlow.xml#yourTaskFlow']}

The expressions above will return true if you have the applicable permission, so you can use it in the rendered property of your button, tab, link, or menu. But if your requirement is to disable a component then use "!" the negation operator.

It is also likely that you will need to check the permission programmatically just like when you are a creating a dynamic tree menu based on user roles. Please see the methods below for the translation of the EL security expressions above to pure Java:
import oracle.adf.controller.security.TaskFlowPermission;
import oracle.adf.share.ADFContext;
import oracle.adf.share.security.SecurityContext;
import oracle.adf.share.security.authorization.RegionPermission;
//class declaration
...
    public boolean isRegionViewable(String pageDef) {
        if (pageDef == null) {
            return false;
        }
        RegionPermission permission =
            new RegionPermission(pageDef, RegionPermission.VIEW_ACTION);
        SecurityContext ctx = ADFContext.getCurrent().getSecurityContext();
        return ctx.hasPermission(permission);
    }

    public boolean isTaskFlowViewable(String taskflowId) {
        if (taskflowId == null) {
            return false;
        }
        TaskFlowPermission permission =
            new TaskFlowPermission(taskflowId, TaskFlowPermission.VIEW_ACTION);
        SecurityContext ctx = ADFContext.getCurrent().getSecurityContext();
        return ctx.hasPermission(permission);
    }
...
Many thanks to John Stegeman for helping me figure this out.