Showing posts with label Apex. Show all posts
Showing posts with label Apex. Show all posts

Friday, 30 June 2017

OHS URL Rewrite

First half of this year I did two SAML2 implementations on Weblogic. One of those was to implement Single Sign On against ADFS for Apex applications.

In short, we installed an adapted version of ORDS on a Weblogic server and configured SAML2 for Service Provider initiated SSO, as can be read here.

We added an Oracle HTTP Server as reversed proxy to the story. For the other customer I found out how to create a separate saml2 routing, since we wanted a SP specific URI, but Weblogic only listen to the /saml2 URI. I solved that using the PathTrim and PathPrepend options in OHS, as can be read here.

But one thing that left was a 'Nice' URI. When we now want to get to the application we need to have an url like https://host.customer.nl/ords/f?p=ApexId  But we wanted to use something like https://apexappname.customer.nl. I thought to be smart and just do a PathPrepend of ords/f?p=ApexId. But this is considered to be a folder, so OHS/Apache adds a trailing slash '/', resulting in an url like: https://host.customer.nl/ords/f?p=ApexId/  .This routes to Apex nicely, but Apex considers the slash as part of the ApexId,  so you'll get a message indicating that Apex cannot find the 'ApexId/' application.

I was lost until I finaly found this blog. What we want is that if the user enters into the root of the server, the URL is rewritten to the Apex application. Actually not completely, because we rather have the ords extension left out the equation.

The trick is in the following three lines:
    RewriteEngine On
    RewriteCond %{REQUEST_URI} ^/$
    RewriteRule ^(.*)$ https://host.customer.nl/ords/f?p=ApexId [R,L]

When you use SSL,  and need to add this to the ssl.conf, you should add it to the virtual host configuration, just
    RewriteEngine On
    RewriteCond %{REQUEST_URI} ^/$
    RewriteRule ^(.*)$ https://host.customer.nl/ords/f?p=ApexId [R,L]

  </IfModule>
</VirtualHost>


We could fine tune this to do something like the following:
    <Location /f>
        SetHandler weblogic-handler
        WLSRequest ON
        WebLogicCluster weblogic.host1.customer.local:7003, weblogic.host2.customer.local:7003
        PathPrepend /ords
        KeepAliveEnabled on
        KeepAliveSecs 10
    </Location>
    RewriteEngine On
    RewriteCond %{REQUEST_URI} ^/$
    RewriteRule ^(.*)$ https://host.customer.nl/f?p=ApexId [R,L]
  </IfModule>
</VirtualHost>

Although I did not test this yet, that would at least remove the append of the /ords in the URI. It would only append the /f?p=150 part.Currently I don't know how to prevent that, since Apex does need these parameters. For Weblogic and OHS Apex is an application that works with parameters in the URI.


Tuesday, 2 May 2017

Single Sign On for Apex with ADFS? With Weblogic 12c and ORDS: Yes, you can!

Lately we implemented a Single Sign On solution for Apex, based on Weblogic 12cR2, ORDS 3.0.9, and ADFS as a federated Identity Provider. This combination turns out to be a marriage of 3 different worlds. So we ran in to a several issues that were not described in one simple how-to document. So in this document I try to assemble the information needed to do the end 2 end configuration (apart from the OHS configuration).

For most of the SAML2 configuration on Weblogic, we could have my earlier article  on SAML2.0 on Weblogic 11g, as a guide: Service Provider initiated SSO on WLS11g using SAML2.0 .

This helped a great deal with regards to ADFS and 12c. The rest of the issues I'd like to cover here, for future reference.

ORDS

ORDS can be installed in the regular way. I downloaded ORDS and unzipped it in the weblogic domain home. Then I did the setup using:
[oracle@darlin-vce-db ords309]$ java -jar ords.war setup

Then create the i.war containing the Apex and ORDS images. First copy the Apex images from the Apex home to the ORDS images folder. And then create the i.war:
[oracle@darlin-vce-db ords309]$ java -jar ords.war static /u01/data/oracle/config/domains/SP_domain/ords309/images

It is important to provide a complete/absolute path to the this command. This command creates an i.war that contains a reference to the images folder. You can see this as a virtual directory configuration as you would do in Apache/Oracle HTTP Server. It in fact only contains a web.xml, sun-web.xml and a weblogic.xml that contain a reference to that folder. For instance, the weblogic.xml contains:
<weblogic-web-app xmlns="http://www.bea.com/ns/weblogic/weblogic-web-app">
 <!-- This element specifies the context path the static resources are served from -->
 <context-root>/i</context-root>
 <virtual-directory-mapping>
  <!-- This element specifies the location on disk where the static resources are located -->
  <local-path>/u01/data/oracle/config/domains/SP_domain/ords309/images</local-path>
  <url-pattern>/*</url-pattern>
 </virtual-directory-mapping>
</weblogic-web-app> 

Before deploying the ords.war and i.war files to Weblogic (with Custom Roles), you'll need to make a manual adjustment to the ords.war.
The thing is that ORDS must be instructed to hand over the authentication to Weblogic. To do so, you'll need to add the following to the WEB-INF/web.xml file in the ords.war:
    <!-- Security Constraint -->
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>SecurePages</web-resource-name>
            <description>These pages are only accessible by authorized users.</description>
            <url-pattern>/f/*</url-pattern>
            <!--  <http-method>GET</http-method> -->
        </web-resource-collection>
        <auth-constraint>
            <description>These are the roles who have access.</description>
            <role-name>Anonymous</role-name>
        </auth-constraint>
        <user-data-constraint>
            <description>This is how the user data must be transmitted.</description>
            <transport-guarantee>NONE</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
    <!-- Login Config -->
    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>myrealm</realm-name>
    </login-config>
    <security-role>
        <role-name>Anonymous</role-name>
    </security-role>

In WEB-INF/weblogic.xml add the security-role-assignment for the role Anonymous:
<weblogic-web-app  xmlns="http://xmlns.oracle.com/weblogic/weblogic-web-app" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
                   xsi:schemaLocation="http://xmlns.oracle.com/weblogic/weblogic-web-app 
                   http://xmlns.oracle.com/weblogic/weblogic-web-app/1.6/weblogic-web-app.xsd">  <!-- Weblogic 12c -->
  <container-descriptor>
    <prefer-web-inf-classes>true</prefer-web-inf-classes>
  </container-descriptor>
  <session-descriptor>
    <persistent-store-type>replicated_if_clustered</persistent-store-type>
  </session-descriptor>
  <security-role-assignment>
    <!--<role-name>valid-users</role-name> -->
    <role-name>Anonymous</role-name>
    <principal-name>users</principal-name>
  </security-role-assignment>
  <context-root>/ords</context-root>
</weblogic-web-app>
This is necessary, because after the Authentication using SAML2, the Rolemapper kicks in. And that one has to find a valid role.

APEX-Schema's

APEX uses REST-calls to fetch the images. It is important that all the database users relating to ORDS and APEX are unlocked, that passwords are known. Check for each of the following schema's if you can logon with the known password.

  • ORDS_PUBLIC_USER
  • APEX_PUBLIC_USER
  • APEX_LISTENER
  • APEX_REST_PUBLIC_USER
  • ORDS_METADATA
Then perform the following steps from doc 2075837.1:
  1. Go into the directory where the extracted the full installation of APEX resides
  2. Login to SQLPlus as the SYS user (as sysdba)
  3. Rerun the @apex_rest_config.sql script to recreate the "connect through" privileges for the installation:
    @apex_rest_config
  4. Hit enter for the APEX_LISTENER and APEX_REST_PUBLIC_USER password prompt. (passwords apparently not used for this script)
  5. Verify with the following statement that the connect through grant is present:
    select * from dba_proxies
    where proxy='APEX_REST_PUBLIC_USER'
    and client='APEX_PUBLIC_USER';
  6. If the grant is still missing the customer should verify that the file apex_rest_config_core.sql contains the following statement and run the script again.
    -- Allow REST user to proxy into APEX_PUBLIC_USER for built-in RESTful Services
    alter user APEX_PUBLIC_USER grant connect through ^RESTUN.;
    

ORDS Validation

From ORDS 3.0.5 onwards ORDS expects a column in the table ords_metadata.apex_pool_config called pool_name. So you could end up with an ORDS installation/configuration that seems to work, but you might not get the Apex application images shown. This is described in this community question. If the applications images don't show up:
  1. Validate that this column exists, through a describe of the table, and check if the table contains any rows.
  2. If necessary, perform the following to chreate the column and fill the table:
cd /u01/data/oracle/config/domains/SP_domain/ords309
java -jar ordsname.war validate

APEX Authentication Schema

To have the Apex application use the authenticated user from Weblogic/ADFS, the authentication scheme needs to be changed. 
This has to be done in a way that in the authentication scheme (shared components --> security --> authentication) APEX fetches the user from the  REMOTE_USER header variable: 


Weblogic 12 and ADFS

ADFS will use SHA-256 for signing by default. But WLS currently does not support that for SAML2. Although for many other services WLS does 'know' how to do SHA-256. I found articles how to update the policies for OWSM to use SHA-256. But I could not find how to do the same for the SAML2 configuration of Weblogic. So have ADFS do the signing with SHA-1. This might seem not secure, but when using TLS this is a minor thing. Although Weblogic 12c should solve this, in my opinion. See also this blog on Weblogic with ADFS, point 5 under the Takeway or Gotchas.

Another thing we found is that ADFS expects a valid 'https://' url to the ServiceProvider for the entity-id. A regular unique string does not suffice. ADFS apparently checks this URL to be valid. So I used the default TLS-url to the Oracle HTTP server that I used to reverse proxy to the SP Weblogic.

Lastly, in ADFS we needed to add an extra explicit claim had to fill the urn:mace:dir:attribute-def:uid saml2-attribute. This is needed for the identy mapper.

Identity Mapper Class

I updated the IdentityMapper class that I used in my earlier blog. I found a little bug in determining the actual identity/username, that apparently did not occur in 11g. But I also refactored the class a bit, to my latest Java knowledge, for as far as applicable.

package nl.darwinit.wls.saml;

import com.bea.security.saml2.providers.SAML2AttributeInfo;
import com.bea.security.saml2.providers.SAML2AttributeStatementInfo;
import com.bea.security.saml2.providers.SAML2IdentityAsserterAttributeMapper;
import com.bea.security.saml2.providers.SAML2IdentityAsserterNameMapper;
import com.bea.security.saml2.providers.SAML2NameMapperInfo;

import java.security.Principal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.logging.Logger;

import weblogic.logging.LoggingHelper;

import weblogic.security.service.ContextHandler;


public class WLSSaml2IdentityMapper implements SAML2IdentityAsserterNameMapper, SAML2IdentityAsserterAttributeMapper {
    public static final String ATTR_PRINCIPALS = "com.bea.contextelement.saml.AttributePrincipals";
    public static final String ATTR_USERNAME = "urn:mace:dir:attribute-def:uid";
    private Logger lgr = LoggingHelper.getServerLogger();
    private final String className = "nl.darwinit.wls.saml.WLSSaml2IdentityMapper";
    //

    /**
     * Map Name Info to String
     * @param saml2NameMapperInfo
     * @param contextHandler
     * @return
     */
    @Override
    public String mapNameInfo(SAML2NameMapperInfo saml2NameMapperInfo, ContextHandler contextHandler) {
        final String methodName = className + ".mapNameInfo";
        debugStart(methodName);
        String user = null;
        debug(methodName, "saml2NameMapperInfo: " + saml2NameMapperInfo.toString());
        debug(methodName, "contextHandler: " + contextHandler.toString());
        debug(methodName, "contextHandler number of elements: " + contextHandler.size());
        // getNames gets a list of ContextElement names that can be requested.
        String[] names = contextHandler.getNames();
        // For each possible element
        for (String element : names) {
            debug(methodName, "ContextHandler element: " + element);
            // If one of those possible elements has the AttributePrinciples
            if (element.equals(ATTR_PRINCIPALS)) {
                // Put the AttributesPrincipals into an ArrayList of CustomPrincipals
                ArrayList<CustomPrincipal> customPrincipals =
                    (ArrayList<CustomPrincipal>) contextHandler.getValue(ATTR_PRINCIPALS);
                int i = 0;
                String attr;
                if (customPrincipals != null) {
                    // For each AttributePrincipal in the ArrayList
                    for (CustomPrincipal customPrincipal : customPrincipals) {
                        // Get the Attribute Name and the Attribute Value
                        attr = customPrincipal.toString();
                        debug(methodName, "Attribute " + i + " Name: " + attr);
                        debug(methodName, "Attribute " + i + " Value: " + customPrincipal.getCollectionAsString());
                        // If the Attribute is "loginAccount"
                        if (attr.equals(ATTR_USERNAME)) {
                            user = customPrincipal.getCollectionAsString();
                            // Remove the "@DNS.DOMAIN.COM" (case insensitive) and set the username to that string
                            if (!user.equals("null")) {
                                user = user.replaceAll("(?i)\\@CLIENT\\.COMPANY\\.COM", "");
                                debug(methodName, "Username (from loginAccount): " + user);
                                break;
                            }
                        }
                        i++;
                    }
                }
                // For some reason the ArrayList of CustomPrincipals was blank - just set the username to the Subject
                if (user == null || "".equals(user)) {
                    user = saml2NameMapperInfo.getName(); // Subject = BRID
                    debug(methodName, "Username (from Subject): " + user);
                }
                return user;
            }
        }
        // Just in case AttributePrincipals does not exist
        user = saml2NameMapperInfo.getName(); // Subject = BRID
        debug(methodName, "Username (from Subject): " + user);
        debugEnd(methodName);
        // Set the username to the Subject
        return user;
        // debug(methodName,"com.bea.contextelement.saml.AttributePrincipals: " + arg1.getValue(ATTR_PRINCIPALS));
        // debug(methodName,"com.bea.contextelement.saml.AttributePrincipals CLASS: " + arg1.getValue(ATTR_PRINCIPALS).getClass().getName());
        // debug(methodName,"ArrayList toString: " + arr2.toString());
        // debug(methodName,"Initial size of arr2: " + arr2.size());
    }
    //

    /**
     * Map Attribute Info to Collection<Principal>
     * @param attrStmtInfos
     * @param contextHandler
     * @return
     */
    public Collection<Principal> mapAttributeInfo(Collection<SAML2AttributeStatementInfo> attrStmtInfos,
                                                  ContextHandler contextHandler) {
        final String methodName = className + ".mapAttributeInfo";
        Collection<Principal> principals = null;
        if (attrStmtInfos == null || attrStmtInfos.size() == 0) {
            debug(methodName, "AttrStmtInfos has no elements");
        } else {
            principals = new ArrayList<Principal>();
            for (SAML2AttributeStatementInfo stmtInfo : attrStmtInfos) {
                Collection<SAML2AttributeInfo> attrs = stmtInfo.getAttributeInfo();
                if (attrs == null || attrs.size() == 0) {
                    debug(methodName, "No attribute in statement: " + stmtInfo.toString());
                } else {
                    for (SAML2AttributeInfo attr : attrs) {
                        CustomPrincipal principal = null;
                        String principalName = "";
                        Collection<String> attrValues = attr.getAttributeValues();
                        if (!attrValues.isEmpty()) {
                            int attrValIdx = 0;
                            for (String attrValue : attrValues) {
                                debug(methodName,
                                      "Value " + ++attrValIdx + " of " + attr.getAttributeName() + "= " + attrValue);
                                if (attrValIdx == 1) {
                                    principalName = attrValue;
                                }
                            }
                        } else {
                            principalName = attr.getAttributeName();
                        }
                        principal = new CustomPrincipal(principalName, attr.getAttributeValues());
                        debug(methodName, "Add principal: " + principal.toString());
                        principals.add(principal);
                    }
                }
            }
        }
        return principals;
    }
    //
    private void debug(String methodName, String msg) {
        lgr.fine(methodName + ": " + msg);
    }
    //
    private void debugStart(String methodName) {
        debug(methodName, "Start");
    }
    //
    private void debugEnd(String methodName) {
        debug(methodName, "End");
    }

}

To be complete, here is the principal class:
package nl.darwinit.wls.saml;

import java.util.Collection;

import weblogic.security.principal.WLSAbstractPrincipal;
import weblogic.security.spi.WLSUser;


public class CustomPrincipal extends WLSAbstractPrincipal implements WLSUser {
    @SuppressWarnings("compatibility:1303497257301553869")
    private static final long serialVersionUID = 1L;

    private String commonName;
    private Collection<String> collection;

    public CustomPrincipal(String name, Collection<String> collection) {
        super();
        // Feed the WLSAbstractPrincipal.name. Mandatory
        this.setName(name);
        this.setCommonName(name);
        this.setCollection(collection);
    }

    public CustomPrincipal() {
        super();
    }

    public CustomPrincipal(String commonName) {
        super();
        this.setName(commonName);
        this.setCommonName(commonName);
    }

    public void setCommonName(String commonName) {
        // Feed the WLSAbstractPrincipal.name. Mandatory
        this.setName(commonName);
        this.commonName = commonName; 
        System.out.println("Attribute: " + this.getName());
        // System.out.println("Custom Principle commonName is " + this.commonName);
    }

    public Collection<String> getCollection() {
        return collection;
    }

    public String getCollectionAsString() {
        String collasstr =  "null";
        if (collection != null && collection.size() > 0) {
            for (String value : collection) {
                collasstr = value;
                break;
            }
            /*for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
                collasstr = (String) iterator.next();
                return collasstr;
            }*/
        }
        return collasstr;
    }

    public void setCollection(Collection<String> collection) {
        this.collection = collection;
        // System.out.println("set collection in CustomPrinciple!");
        if (collection != null && collection.size() > 0) {
            /*for (Iterator iterator = collection.iterator(); iterator.hasNext();) {
                final String value = (String) iterator.next();
                System.out.println("Attribute Value: " + value);
            }*/
            for (String value : collection) {
                System.out.println("Attribute Value: " + value);
            }
        }
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + ((collection == null) ? 0 : collection.hashCode());
        result = prime * result + ((commonName == null) ? 0 : commonName.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        if (getClass() != obj.getClass())
            return false;
        CustomPrincipal other = (CustomPrincipal) obj;
        if (collection == null) {
            if (other.collection != null)
                return false;
        } else if (!collection.equals(other.collection))
            return false;
        if (commonName == null) {
            if (other.commonName != null)
                return false;
        } else if (!commonName.equals(other.commonName))
            return false;
        return true;
    }

}

In my earlier article I described how to add a reference to the jar file containing these classes to the java classpath field on the Server Start tab in the console.
In 12c this apparently does not work, the class is not picked up. Add it to the class path by creating/editing the setUserOverrides.sh/.cmd file.

Conclusion

This must be about it. We had quite a bit of Trial & Error. But most of the gotchas are listed here I think. But feel free to hire us if you need help. (Because I think you'll need the A-team for different issues...)

Tuesday, 3 March 2009

Apex 3.2: My first Oracle Forms conversion

I developed Oracle Forms applications for many years. I also did some minor APEX projects. I read about the ability of Apex 3.2 to migrate Oracle Forms applications and I was wondering how this is working.

First I upgraded my Apex 3.1.2. to 3.2. This was quite easy. Download the zipfile from OTN, extract it, started the good old sqlplus against my local XE database, ran a script and everything was ok.
Then I started to read the manual about the migration process. The picture that I found there was very clear:


and there was a little roadmap as well.

First I had to create a little form. I decided to create an extremely simple form. I wondered how the migration works I'm not (yet) interested in migrating complex forms.
(when I look at the image above this should be possible as well. Pll, olb and mmb files can be migrated too).
Using the Oracle Forms wizards a simple form on dept was created. When I run the form it looks like this:



A simple CRUD (create,retrieve, update, delete) screen on Dept. The resulting fmb is darwin.fmb.

The first step in the migration process is creating an XML file of the fmb file using frmf2xml.bat.
This command does the job:
C:\oracle\10gR2\bin\frmf2xml.bat darwin.fmb

After this I started APEX (I allready had a workspace and a user) and on the righthand side of the screen there is a new option: Application Migration



After clicking this I had to create a new project and after that I had to import the previously created darwin_fmb.xml file:


After pressing [next], I get a summary of the imported XML file:



The original form consist of 1 block, 1 base table block (this is nice, the converter is able to dsitinguish beteen base table and non base table blocks!) and three items. In this form are no triggers, no list of values, no alerts and no program units. In this simple case I'm not interested in the last objects but I have to admit that I'm curious what will happen with these objects.

I have the option to upload another XML file but I choose for creating the application.

This results in the following steps:

[Next]

[Next] Note: By default Apex creates a Report followed by a Table layout


A tabular Form


With the first theme.



And after a while there is the application.
When I run it I see that the application is migrated very smoothly. This looks very promising for more complex forms!