Add a “forgot password” link to MicroStrategy.

Intro

One of the issues that I’ve had in some of the MicroStrategy environments I’ve worked on is the fact that there is no option to have your password reset if you forget it. I decided to add the option because it makes it easier for both the users and the administrators. For this we will leverage the MicroStrategy plugin infrastructure for web as well as the Library REST API.

UI

The UI will look like such:

Workflow

When a user clicks on the link, they should be redirected to a standard HTML page where they can input their email address and get a new password. The actual workflow is like this:

For this to work, there are three pieces we need to create:

  1. The MicroStrategy plugin to add the link to the login page.
  2. The HTML page to submit the request.
  3. The backend that will interact with the REST API to create a new password for the user.

Let’s tackle these individually.

The Plugin

If you want to read on the plugin architecture, you can look at it here: https://lw.microstrategy.com/msdz/MSDL/GARelease_Current/_GARelease_Archives/104/docs/projects/WebSDK/Content/topics/webcusteditor/WCE_Plug-in_Architecture.htm

However, for our scenario we just need to be aware of the structure.

  • plugin_folder
    • javascript
      • loginPage.js
    • jsp
    • style
      • images
      • loginPage.css
    • WEB-INF
      • classes
      • xml
        • layouts

Although we need to create all the folders mentioned above, let’s focus on the two files in purple. We need to create those so that the javascript is triggered on the login page and the css is applied to the login page as well.

loginPage.js

const addLinks = loginStyle => {
	const forgotPassword = document.createElement("div");
	forgotPassword.setAttribute("id","forgotPassword");
	forgotPassword.innerHTML = "<a href=\"/loginEnhancements/forgotPassword.html\">Forgot Password</a>";
	loginStyle.appendChild(forgotPassword);
}

const mutationObserver = new MutationObserver((mutations, me) => {
	const loginStyle = document.getElementById("lb_LoginStyle");
	if (loginStyle) {
		addLinks(loginStyle);
		me.disconnect();
		return;
	}
});

mutationObserver.observe(document, {
	childList: true,
	subtree: true
});

What this javascript is doing is monitoring the login page for the div that contains the login form to render. Once it does, it adds the forgot password link and stops monitoring. It is very simple.

loginPage.css

#forgotPassword > a {
	margin: 		15px 0 0 45px;
	font-size: 		1.3em;
	display: 		block;
}

The css just changes the margin and font size. It makes it more pleasing to the eye.

The HTML Page

The html contains a simple form that sends the user’s email to the python backend. It then waits for a response. If the response is good, it tells the user an email will be sent to them with the new password. If the response fails, it alters the user to contact their administrator.

<html>
  <head>
    <script>
      const createUser = async email => {
        const response = await fetch("cgi-bin/forgot_password.py", {
          method: "POST",
          body: JSON.stringify({
            email: email
          })
        })
          .then(response => {
            if (!response.ok) {
              throw Error(response.statusText);
            }
            return true;
          })
          .catch(error => {
            return false;
          });
        return response;
      };
      const handleSubmit = e => {
        e.preventDefault();
        document.getElementById("submit").disabled = true;
        const email = e.srcElement.elements.email.value;
        createUser(email).then(response => {
          if (response) {
            alert("success, you will get an email with your information.");
          } else {
            alert("request failed, please contact your administrator");
          }
        });
      };
    </script>
  </head>
  <body>
    <h2>
      Use this site to reset your password for the MicroStrategy demo
      environment.
    </h2>
    <form action="#" onsubmit="handleSubmit(event)">
      <label for="email">Enter your email:</label>
      <input type="email" id="email" name="email" /><br />
      <button type="submit" id="submit">Submit</button>
    </form>
  </body>
</html>

The page itself will look like this:

The Backend

If you look at line #5 above, you can see we’re calling a python script inside the cgi-bin folder. Let’s talk about that for a bit.

If you are not a masochist, then you have MicroStrategy on a tomcat server much like myself. Tomcat has the ability to run python scripts.

Here is a nice tutorial you can follow: http://easytech2018.blogspot.com/2017/09/how-to-set-up-tomcat-7-and-run-python.html

It basically consists of installing python and then configuring a servlet so that tomcat knows how to execute your python scripts.

Back to the javascript

So on line #5 you can see a call to the forgot_password.py file (script below). Lines 7 to 9 show the body, which is basically just the email. This is what our python script will receive.

The python script

import mstr_actions as mstr
import sys, random, json

def createPass():
    s = "abcdefghijklmnopqrstuvwxyz01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ!_?"
    passlen = 10
    p =  "".join(random.sample(s,passlen ))
    return p


base_url = "https://mysite.com:8443/MicroStrategyLibrary/api"
mstr_user = "Administrator"
mstr_password = "Super Secret Password"
web_url = "https://mysite.com:8443/MicroStrategy"

sys.stdin.reconfigure(encoding='utf-8')

post_data = json.load(sys.stdin)
email = post_data['email']

username = email.split("@")[0]
new_password = createPass()

token, cookies = mstr.getMSTRSession(base_url, mstr_user, mstr_password)
userInfo = mstr.get_users_info(base_url, token, cookies, abbreviationBegins = username)[0]

status_code = 500

if userInfo:
    ### if user creation succeeds, add email address to user.
    user_id = userInfo['id']
    body = {
      "operationList": [
        {
          "op": "replace",
          "path": "/password",
          "value": new_password
        },
        {
            "op": "replace",
            "path": "/requireNewPassword",
            "value": True
        }
      ]
    }
    password_change_response = mstr.update_user(base_url, token, cookies, user_id, body)

    if password_change_response:
        subject = 'New MicroStrategy password details'
        content = 'Your new password is: ' + new_password + \
        '\nLogin here to login and change your password: ' + web_url
        email_response = mstr.send_email(base_url, token, cookies, [user_id], subject, content)

        if email_response:
            status_code = 204

mstr.closeMSTRSession(base_url, token, cookies)

print('Status: '+ str(status_code))
print('Content-Type: application/json')
print()

What the script above does is:

  1. Create a session with the library api (line 24)
  2. Check and make sure the user exists (line 29)
  3. Change the password and force the user to change it at login (line46)
  4. Send an email to the user with the info (line 52)
  5. Close the session (line 57)

There are a few caveats with the script:

  • The user id has to match their email (sans the domain).
  • The user has to have email setup in MicroStrategy.
  • You depend on a series of functions from mstr_functions.py

mstr_functions.py

I’m sharing these functions with you. I hope you find this interesting and useful. If you expand upon them please let me know.

import requests, urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def getMSTRSession(url, user, password):
    headers = {'Content-Type': 'application/json'}
    payload = {
      "username": user,
      "password": password,
      "loginMode": 1,
    }
    r = requests.post(url + '/auth/login', json=payload, verify=False)
    responseHeaders = r.headers
    responseCookies = r.cookies
    if r.status_code >= 200 and r.status_code < 300:
        return responseHeaders['X-MSTR-AuthToken'], responseCookies
    return False

def closeMSTRSession(url, token, cookies):
    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-MSTR-AuthToken' : token
        }
    r = requests.post(url + '/auth/logout', headers=headers, cookies=cookies, verify=False)
    if r.status_code == 204:
        return True
    return False

def get_users_info(url, token, cookies, nameBegins = None, abbreviationBegins = None, fields = None):
    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-MSTR-AuthToken' : token
        }
    params = {
        'fields' : fields,
        'nameBegins' : nameBegins,
        'abbreviationBegins' : abbreviationBegins
    }
    r = requests.get(
        url + '/users',
        headers=headers,
        params=params,
        cookies=cookies,
        verify=False
        )
    if r.status_code >= 200 and r.status_code < 300 :
        return r.json()
    return False

def update_user(url, token, cookies, user_id, body):
    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'X-MSTR-AuthToken' : token
        }
    r = requests.patch(
        url + '/users/'+user_id,
        headers=headers,
        json=body,
        cookies=cookies,
        verify=False
        )
    if r.status_code >= 200 and r.status_code < 300:
        return r.json()
    return False

def send_email(url, token, cookies, user_ids, subject, content = 'no content', is_html = False):
    headers = {
        'Content-Type': 'application/json',
        'X-MSTR-AuthToken' : token
        }
    body = {
      "notificationType": "DOSSIER_COMMENT",
      "userIds": user_ids,
      "subject": subject,
      "content": content,
      "isHTML": is_html,
      "extraProperties": {}
    }
    r = requests.post(
        url + '/emails',
        headers=headers,
        cookies=cookies,
        json=body,
        verify=False
        )
    if r.status_code >= 200 and r.status_code < 300:
        return True
    return False

Conclusion

This is a fun addition to the MicroStrategy platform that goes through the whole process of modifying UI, adding functionality, and leveraging the REST API.

If you can improve this solution, drop me a note. If you need help creating a solution or solving a problem, let me know how I can help.