Tuesday, December 3, 2019

Adding an Unauthenticated JSP to IBM Control Desk

Background

We have a customer that needed to allow unauthenticated users to open tickets within IBM Control Desk, and we only had access to IBM HTTP Server and our Maximo/ICD WebSphere server to make that happen.

Security Risks

Essentially, anyone with the link can get to the pages described in this article. They could even write a script to create a huge number of tickets, taking down your ICD installation. So in any pages you create using this article, you need to do *something* to guard against that behavior. Exactly what you do depends on your specific situation. 

Where to Create the Unprotected JSP

You just need to create the .jsp file under:
.../installedApps/<cell__name>/MAXIMO.ear/maximouiweb.war/webclient/login

For example, put the following in the file HelloWorld.jsp in the above directory:

<HTML>
 <HEAD>
  <TITLE>Hello World</TITLE>
 </HEAD>
 <BODY>
  <H1>Hello World</H1>
  Today is: <%= new java.util.Date().toString() %>
 </BODY>
</HTML>

Then access it with the url:


And this is what you'll see, with no login required:

image.png


That's it. Now you just need to write the JSP code to do what you need.

Caveats

A JSP file added in this way would need to be re-deployed after each time you deploy the application. These instructions do NOT tell you how to add the JSP file to the EAR build process.
You need to manually copy the JSP file(s) to the appropriate location for each MAXIMO UI JVM.

Monday, December 2, 2019

Creating incident ticket in IBM Control Desk using the new REST API

Background

Maximo 7.6.0.2 introduced a new REST API that can be accessed via .../maximo/oslc . Here's a link to the documentation on it:

https://developer.ibm.com/static/site-id/155/maximodev/restguide/Maximo_Nextgen_REST_API.html

This is an all-JSON API that makes things a ton easier than it was with the older (and deprecated) XML-based REST API.

The Problem

However, that documentation is aimed at Maximo Enterprise Asset Management users and not IBM Control Desk users. That means there aren't any examples for creating incidents or service requests, for example.

Why You're Here

You want an example of creating an INCIDENT in ICD, and that's what I'll provide. I'm using ICD 7.6.1.1 on WebSphere, DB2 and IBM HTTP Server, along with the sample data. That's how I have a classification ID and hierarchy structure in the example below.

Basically, the best way I've found is to use the MXOSINCIDENT object structure because it already has a bunch of relationships (including one to TICKETSPEC, so you can add specifications when creating an incident). Here are the details:


Additional header:
properties: *

BODY:
{
    "reportedby": "MXINTADM",
    "description": "second MXINCIDENT OS API",
    "externalsystem": "EVENTMANAGEMENT",
    "classstructureid": "21010405",
    "ticketspec": [{"assetattrid": "computersystem_serialnumber","alnvalue": "99999"}]
}

RESPONSE:

{
    "affecteddate": "2019-11-29T15:02:00-05:00",
    "template": false,
    "creationdate": "2019-11-29T15:02:00-05:00",
    "hierarchypath": "21 \\ 2101 \\ 210104 \\ 21010405",
    "historyflag": false,
    "actlabcost": 0.0,
    "createwomulti_description": "Create Multi Records",
    "selfservsolaccess": false,
    "outageduration": 0.0,
    "ticketuid": 46,
    "inheritstatus": false,
    "reportdate": "2019-11-29T15:02:00-05:00",
    "class_description": "Incident",
    "description": "second MXINCIDENT OS API",
    "reportedby": "MXINTADM",
    "classificationid": "21010405",
    "sitevisit": false,
    "_rowstamp": "10009026",
    "accumulatedholdtime": 0.0,
    "createdby": "MXINTADM",
    "isknownerror": false,
    "affectedperson": "MXINTADM",
    "class": "INCIDENT",
    "ticketid": "1040",
    "ticketspec": [
        {
            "classstructureid": "21010405",
            "changeby": "MXINTADM",
            "changedate": "2019-11-29T15:02:00-05:00",
            "alnvalue": "99999",
            "mandatory": false,
            "refobjectname": "INCIDENT",
            "ticketspecid": 7,
            "assetattrid": "COMPUTERSYSTEM_SERIALNUMBER",
            "_rowstamp": "10009029",
            "refobjectid": 46,
            "displaysequence": 1,
        }
    ],
    "status_description": "New",
    "externalsystem_description": "EVENT MANAGEMENT",
    "classstructureid": "21010405",
    "changeby": "MXINTADM",
    "changedate": "2019-11-29T15:02:00-05:00",
    "externalsystem": "EVENTMANAGEMENT",
    "actlabhrs": 0.0,
    "relatedtoglobal": false,
    "hasactivity": false,
    "statusdate": "2019-11-29T15:02:00-05:00",
    "createwomulti": "MULTI",
    "hassolution": false,
    "virtualenv": false,
    "pluspporeq": false,
    "isglobal": false,
    "oncallautoassign": false,
    "pmscinvalid": false,
    "status": "NEW"
}

Prerequisites

To successfully do the above, you do need to Configure Object Structure security: https://www.ibm.com/support/pages/using-object-structure-security-limit-access-security-groups , and the user MUST have a Default Insert Site, which apparently my MXINTADM user does. MAXADMIN in my system DOES NOT, so it fails if I use that user.

I'm using Postman for testing, which I highly recommend: https://www.getpostman.com/downloads/

Of course you'll use some specific language (or curl) when you're doing this in production, but for testing, you want to use Postman.

A helpful link

In addition to the API documentation, this link was very helpful to me:


Tuesday, November 12, 2019

Special characters in passwords initially configuring IBM Control Desk 7.6.1.1

Several of the screens displayed by ConfigUI tell you that some passwords don't allow special characters. The rule of thumb I found the REALLY hard way is:

ONLY use underscore as a special character in any password initially.

We ran into problems mainly with dollar sign, but other ones will certainly bite you, too. We even ran into the problem with the root user's password. Any password can be changed after the initial deployment, so save yourself some headache and make them initially very simple. And the painful part is that some of the errors will just be silent, such as "Unable to access host with these credentials", but no other error. So just take the advice above and you'll be much happier.

Monday, November 4, 2019

Moving to the cloud. Pick any two: Cheap, Fast, or Easy

The Cloud offers literally all of your current IT services, plus tons more, some of which most of your IT department has never heard of before. You can quickly and easily move all your existing workloads there, but you'll pay dearly, as many companies are finding out. You can take the time to train your IT staff and meticulously plan for the most efficient way to the cloud, but that's not quick. 

Moving to the cloud correctly truly requires rethinking how you do everything in IT. In all cases, the best route is to only move a subset of workloads or capabilities to the cloud, and different clouds may be better for different workloads. Some things are easier and cheaper to run on-prem. For example, in many cases it can be cost effective to arm your IT and development staff with laptops with 64GB of RAM. Doing so allows each one to run their own private multi-cloud in which they can test away. A brand new laptop with warranty with 64GB of RAM and 6 cores (12 threads) can be found for under $1700 on eBay and has a useful life of 4 years. Such a VM in the cloud (AWS EC2 r5.2xkarge) costs $.20 per hour, which is $5,300 for a three-year term, and doesn’t allow the flexibility of a local system running VMWare Workstation. That's a very specific example, but it illustrates why each and every workload needs to be analyzed or audited before simply moving it to the cloud.

 Some workloads are more suited to specific public clouds. WebSphere applications are a big example. If you want to “lift and shift” these workloads to the cloud, the IBM Cloud should be your first choice. If you have apps that run under Sharepoint, you should absolutely run those applications on Microsoft’s Azure cloud. There are many other workloads that may run equally well on any cloud, and for those, the analysis needs to take other factors into account.

 The point I want to get across is that moving to the cloud requires analysis by a qualified team of experts. I think the best approach is to hire one or more experts and simultaneously train your own team to help them get up to speed. The combination of those two is extremely important, because you don’t want newly-trained people responsible for your entire cloud migration. You want an expert who can guide the team, allowing them to take over responsibilities over time.

Friday, September 13, 2019

If you're scripting on Windows, use PowerShell

My last post on PowerShell was in 2008, so I thought I would write an update. If you're writing scripts on Windows, you should probably be using PowerShell. It seems to have 99 to 100% of the tools (especially parsers) that I ever need. I just recently needed to scrape a web page for some data, so I thought I would spend some time messing with PowerShell to get it going. Well, it only took about 30 minutes to develop the entire script that I needed, with absolutely no external dependencies.

Here's the whole script:

# get the web page. Yes, PowerShell has a 'curl' command/alias
$resp = curl -UseDefaultCredentials http://myhostname/mypagename

# Get all of the rows of the table with an ID of "serverTable"
$rows = $resp.ParsedHtml.getElementById("serverTable").getElementsByTagName("TR")

# Loop through the rows, skipping the header row: 
for ($i=1; $i -lt $rows.Length(); $i++) {

# get the hostname of this row
  $thehost = $rows[$i].getElementsByTagName("TD")[2]

# get the date this host was last rebooted
  $rebootDateString=$rows[$i].getElementsByTagName("TD")[6]

# if the host was rebooted over 20 days ago, print that date

  if ([datetime]::Now.AddDays(-20) -gt [datetime]::parseexact($rebootDateString.innerText,"G", $null))   {
    $rebootDateString.innerText } }


That's it, with no external references and nothing extra to install. It's got date parsing, date arithmetic, HTML parsing and HTTP request capabilities all built in. I realize that this then isn't portable... or is it? There's actually a PowerShell port for Linux available, with instructions here from Microsoft:


I know that Python is a hot language these days, but I don't like it as much as PowerShell. I tried to do the above with Python, and it took quite a bit longer, even though I've used Python more than PowerShell. You have to import some classes and then use XPath to find elements in the HTML. PowerShell was just straightforward and easy, at least for me, with my background and expectations. YMMV, but I like PowerShell.

Thursday, September 12, 2019

How to view an LtpaToken2 token

If you use any WebSphere-based applications (DASH, Impact, BPM, ODM, etc.), you're using LTPA Tokens. An LTPA Token is a browser cookie named LtpaToken2. You can see it if you turn on developer tools in your browser (F12) after you log into one of these applications. You'll see it in the "COOKIES" request header. The value of that header will look something like this:

s_vi=[CS]v1|2E7C0CDA8507BB19-4000010FA0013DFE[CE]; s_cc=true; s_sq=%5B%5BB%5D%5D; JSESSIONID=00004FX-3-uu2ZoYHx1t9p8fJIb:52e767f1-e67e-4220-8435-fa54d8776107; CSRFCookie=C9495874E4D5BA23D8E1330E4F76EA5C9495874E4D5BA23D8E1330E4F76EA5; LtpaToken2=i/InlYuq2tm3rPdd/3BEzA8m9BCc8WGNR3q6eu7OfeQ7s1ICiMvPv0QCNQar5cCQlyVH5GE0N0VNbJj1Z6sUGe2S3nb1kwwbzdzPWzCbNPPtN3uiPWnfLyXzi5T4p2Pz/URwCfP6zWW2NOob/yQoG5vYg/JAgJag9CWP5tqd9+6FgInahSj3VaYYvu69O4hY+h6e6D+v7mpLTYBRM33TlVugTxOkx64JTMAdwFAfH553Ob2T+sW4aqyiGc7arLodIMlWjiVbkBBEgYZ0PXMyCPKb7JPa+5lFxfMRBK0P1kMsC34OXnQ1jUaedx44U4I5

Notice that each cookie=value pair is separated by a semicolon (;). The LtpaToken2 cookie and value are in bold above. That cookie contains the "principal" (user) name, the "realm" (a named security policy domain), and the expiration date of the token. This token is used by a WebSphere server (whether it's WebSphere ND, WebSphere Liberty, or any other flavor of WebSphere) to make authorization decisions to determine which resources the user has access to. It's also used for authentication across WebSphere servers if the two servers share the same LTPA keys. This sharing of keys is how one server "trusts" an LTPA token created by another server.

For troubleshooting purposes, sometimes you want/need to see what's inside an LTPA token. I found the best article on the web that provides the code to allow you to do exactly that. The page is found here: http://tech.srij.it/2012/04/how-to-decrypt-ltpa-cookie-and-view.html , but it leaves out some basic steps that I'm including here. I'm also copying the Java code at the bottom of this blog post just in case the linked article disappears.

Once you've copied the code into a file named DecryptLTPA.java (the file MUST have this name to match the name of the class defined in the file), you then need to specify several values in the file. Specifically, you need to provide values for the following variables:


ltpaKey : the com.ibm.websphere.ltpa.3DESKey value from ltpa.keys file. This file already exists if you're using WebSphere Liberty. If you're using "full" WebSphere, you need to create this file (with whatever name you want) by exporting the LTPA keys from the WebSphere Administration Console. In the ltpa.keys file, this value will end with the characters "\=" (backslash equals). When you paste the value into the DecryptLTPA.java, remove that backslash.

ltpaPassword : WebAS  - This is the default password on WebSphere Liberty. If you're using "full" WebSphere, the password will be whatever you specify when you export the LTPA keys using the WebSphere Administration Console.

tokenCipher : the entire value of the ltpatoken2 header from the COOKIES header in the Request.


Once you've done the above, the beginning of the definition of the main() function in the DecryptLTPA.java file should look something like:

 public static void main(String[] args) {
  String ltpaKey = "ADbpPpqPf3bnkj0b34sNaNC2FYHYygub3/cGjIn+mR4=";
  String ltpaPassword = "WebAS";
  String tokenCipher = "i/InlYuq2tm3rPdd/3BEzA8m9BCc8WGNR3q6eu7OfeQ7s1ICiMvPv0QCNQar5cCQlyVH5GE0N0VNbJj1Z6sUGe2S3nb1kwwbzdzPWzCbNPPtN3uiPWnfLyXzi5T4p2Pz/URwCfP6zWW2NOob/yQoG5vYg/JAgJag9CWP5tqd9+6FgInahSj3VaYYvu69O4hY+h6e6D+v7mpLTYBRM33TlVugTxOkx64JTMAdwFAfH553Ob2T+sW4aqyiGc7arLodIMlWjiVbkBBEgYZ0PXMyCPKb7JPa+5lFxfMRBK0P1kMsC34OXnQ1jUaedx44U4I5";

You then need to compile the java file with a command similar to the following:

 javac -cp /opt/IBM/tivoli/impact/wlp/usr/servers/ImpactUI/apps/blaze.war/WEB-INF/lib/commons-codec-1.10.jar DecryptLTPA.java

That command points to the commons-codec.1.10.jar file, which contains the definition of the Base64 class referenced by the code. That command was run on a Netcool Impact version 7.1.0.16 server which uses WebSphere Liberty. If you were to compile the code on a DASH version 3.1.3.0 server, the command would look like this:

javac -cp /opt/IBM/tivoli/JazzSM/profile/installedApps/JazzSMNode01Cell/IBM Cognos.ear/p2pd.war/WEB-INF/lib/commons-codec-1.3.jar DecryptLTPA.java

In both cases, my path includes the Java 1.7 SDK binaries and my JAVA_HOME is set to the Java 1.7 SDK directory.

Once you have the file compiled, you can actually run it, which requires a similar command:

 java -cp /opt/IBM/tivoli/impact/wlp/usr/servers/ImpactUI/apps/blaze.war/WEB-INF/lib/commons-codec-1.10.jar:. DecryptLTPA

Notice that you must include "." (present working directory) in the classpath (-cp flag) in addition to the commons-codec jar file.

Once that runs, you should see output similar to:

Algorithm:[AES]
Full token string:[expire:1568250479184$u:user\:customRealm/impactadmin%1568250479184%zoQ7cb1BSWekvxdd3slUpxmCRlBCBmMO1nu8iztv73PKQN3MIybuCx/C9EdKGwoeoguJHKrj0BOOAeXxVLDgIQL5Jz2Tg6LQcIyTpAtRAVMsqWTPFzDBrs85Zxs9kP0zFEiOvEsDUmRXXm92dN6zxWooEyGz453x1VPmqGoZ0ww=]
Token is for:[expire:1568250479184$u:user\:customRealm/impactadmin]
Token expires at:[2019-09-11-21:07:59 EDT]
Token signature:[zoQ7cb1BSWekvxdd3slUpxmCRlBCBmMO1nu8iztv73PKQN3MIybuCx/C9EdKGwoeoguJHKrj0BOOAeXxVLDgIQL5Jz2Tg6LQcIyTpAtRAVMsqWTPFzDBrs85Zxs9kP0zFEiOvEsDUmRXXm92dN6zxWooEyGz453x1VPmqGoZ0ww=]

As you can see, the user (principal) ID and expiration timestamps are easily visible, which was my goal from the beginning.

Full Java code:

import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.StringTokenizer;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class DecryptLTPA {
 private static final String AES = "AES/CBC/PKCS5Padding";
 private static final String DES = "DESede/ECB/PKCS5Padding";

 public static void main(String[] args) {
  String ltpaKey = "<DES key from ltpa token>";
  String ltpaPassword = "<password used to export ltpa token>";
  String tokenCipher = "<the header text to dercypt>";

  try {
   Base64 b = new Base64();
   byte[] secretKey = null;
   MessageDigest md = MessageDigest.getInstance("SHA");
   md.update(ltpaPassword.getBytes());
   byte[] hash3DES = new byte[24];
   System.arraycopy(md.digest(), 0, hash3DES, 0, 20);
   Arrays.fill(hash3DES, 20, 24, (byte) 0);
   secretKey = decrypt(b.decode(ltpaKey), hash3DES, DES);
   byte[] ltpaByteArray = b.decode(tokenCipher);

   String algorithm, userInfo, expires, signature, ltpaPlaintext;
   try {
    algorithm="DES";
    ltpaPlaintext = new String(decrypt(ltpaByteArray, secretKey, DES));
   } catch (Exception e) {
    algorithm="AES";
    ltpaPlaintext = new String(decrypt(ltpaByteArray, secretKey, AES));
   }

   System.err.println("Algorithm:["+algorithm+"]");
   StringTokenizer st = new StringTokenizer(ltpaPlaintext, "%");
   userInfo = st.nextToken();
   expires = st.nextToken();
   signature = st.nextToken();
   System.err.println("Full token string:[" + ltpaPlaintext + "]");
   Date d = new Date(Long.parseLong(expires));
   SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss z");
   System.err.println("Token is for:[" + userInfo + "]");
   System.err.println("Token expires at:[" + sdf.format(d) + "]");
   System.err.println("Token signature:[" + signature + "]");

  } catch (Exception e) {
   e.printStackTrace();
  }
 }

 public static byte[] decrypt(byte[] ciphertext, byte[] key, String algorithm) throws Exception {
  SecretKey sKey = null;

  if (algorithm.indexOf("AES") != -1) {
   sKey = new SecretKeySpec(key, 0, 16, "AES");
  } else {
   DESedeKeySpec kSpec = new DESedeKeySpec(key);
   SecretKeyFactory kFact = SecretKeyFactory.getInstance("DESede");
   sKey = kFact.generateSecret(kSpec);
  }
  Cipher cipher = Cipher.getInstance(algorithm);

  if (algorithm.indexOf("ECB") == -1) {
   if (algorithm.indexOf("AES") != -1) {
    IvParameterSpec ivs16 = generateIvParameterSpec(key, 16);
    cipher.init(Cipher.DECRYPT_MODE, sKey, ivs16);
   } else {
    IvParameterSpec ivs8 = generateIvParameterSpec(key, 8);
    cipher.init(Cipher.DECRYPT_MODE, sKey, ivs8);
   }
  } else {
   cipher.init(Cipher.DECRYPT_MODE, sKey);
  }
  return cipher.doFinal(ciphertext);
 }

 private static IvParameterSpec generateIvParameterSpec(byte key[], int size) {
  byte[] row = new byte[size];

  for (int i = 0; i < size; i++) {
   row[i] = key[i];
  }

  return new IvParameterSpec(row);
 }
}