Developer Guidelines

This document explains the coding constructs specific to the banking work. It is meant to be of use to developers.

Database Considerations

Multi-tenant Database

To accommodate small banking entities that may have relatively smaller workload, the databases are designed to be multi-tenant. Every table in the database that could have data specific to a bank contains the bank's primary key, even when it would not be required in a normalized database. This is so that a simple query filter (in a where clause) can always to applied to the queries on those tables, eliminating the risk of a bank getting exposed to another bank's data.

Developers should always use these filters on all tables that have a bankid field.

Database Bifurcation

As covered in system architecture, there is a single main database and there can be several bank-specific databases.

Some tables exist only in the main database. Other tables exist in bank databases, and they can be (and generally are) present in the main database too.

One interesting consequence of this decision is the separation of the fields of a bank into two parts:

  • Bank Main (BANK_MAINDB) - the table residing in main database
  • Bank (BANK) - the table residing in the bank database

The Bank Main table contains two columns "data source" and "db type" and they point to the database in which the bank database tables for the given bank reside.

Authentication and Authorization

Once the login is verified, the login data is cached against a login token in the Redis cache. An expirable cookie is created containing the login token. It is called the session cookie.

The session cookie is the only mechanism to maintain a session in the banking application. Specifically, we are not using HTTP session. This will make our application scalable by creating instances against high load.

Thus, the login sequence is:

  1. Authenticate (2 FA)
  2. Save login data to Redis cache, against a unique login token
  3. Create cookie with the login token

Now, every time a request comes to the server, here is how the authentication and authorization happens (in entry points):

  1. Retrieve the login token from the cookie
  2. Retrieve the login data from the cache
  3. Check if the user is authorized to carry out the specific request

Just to clarify, this covers the server side authentication / authorization.

For Agent or Superuser

Both the superusers (Foretech users) and agents (third party, appointed by Foretech) are internally represented by Agent objects. Unless otherwise stated, the agents have a view-only access, and the superusers have a create-view-edit accesses over the banks and branches.

The login data that is cached is the Agent object itself. The object has a method that indicates whether the object represents a Superuser or is an Agent.

Using that, the Agent object can be authorized the functionality that needs only a superuser. If the logged in user is not a superuser, then such an entry point exits through an "unauthorized" exit path. That exit path is connected to an "Unauthorized access" page instance, which is an instance of the "HTTP Error" Page component. It throws a 403 error in that case.

For example, consider the Entry point instance "[banking.agents] Active banks entry". It is based on "[banking.core] Invoker For Agent Functions" entry point component. This component has the code to retrieve the login data from the cache, and check authorization based on the value of a parameter. Following single line achieves it:

outargAgent = UserUtil.authorizeAgent(isSuperuserNeeded, request);

While designing an entry point component, if you know which agent functionality the entry point governs access to, then you may do it without having a parameter setting.

For Banker

The users that belong to a bank (we call them as bankers or simply users) follow a slightly different model. Each user has a role (ex. Bank Manager), and each role has responsibilities mapped to these roles. The responsibilities translate to authorities. This responsibility mapping is specific to a bank. This means that the Bank managers from two different banks may have different authorities.

The login data that is cached is a combination of:

  • The user object
  • The bank main object
  • The bank object
  • Set of user responsibilities

The responsibilities enable one to authorize the user against a given functionality. Similar to the Agent/Superuser case, the authorization can be done at the entry point level, with the "unauthorized" exit path leading to an "Unauthorized access" page instance.

For example, consider the entry point instance "[banking.agents] Task invoker" that is an instance of "[banking.core] Invoker for Bank Functions" entry point. The component has the code to retrieve the login data from the cache, and then do the authorization checks based on the value of a parameter. All these are achieved by a single line of code:

UserLoginData logindata = UserUtil.authorizeBanker(reqdResponsibility, request);

While designing an entry point component, if you know which banker functionality the entry point governs access to, then you may do it without having a parameter setting.

UI Filtering

For Banker Functionality Pages

The same responsibilities are used for filtering out the UI parts of a UI page that are not relevant to the logged in user.

The Page components should take in the set of responsibilities with an inarg. (They would be available from the previous entry point instance.) Then the page can check whether the responsibility needed for a link / button / section is contained in the set, and show it only if it is.

<% if (responsibilities.contains(reqdResponsibility) { %>
...
<% } %>

For Agent or Superuser Functionality Pages

The same approach will be followed for agent functionalities too. Only here, instead of responsibilities, you would check the superuser privileges that an agent object has, while showing them a link to any superuser functionality.

Media Resources

We store images in filesystem and not in database. The database contains columns with name ending with PATH where the relative paths of these resources is stored. For example: LOGOPATH in BANK_MAINDB table (accessible from a BankMain object).

The base directories are stored in properties file banking.properties. The file PropUtil has a public final field named "properties" from where these properties can be accessed.

A developer needs to concatenate the base path with the relative path to get the complete path of the resource file. Check the banklogo.jsp file for an example of how it is done.

UI Themes

The banking application is themable. It contains various themes and different themes can be applied to different banks. One may use resources from readimade Bootstrap5 themes, or compile custom Bootstrap themes with the Bank-specific colors.

All the banker pages (starting right from the banker login page) have a knowledge of which bank they serve, and consequently are expected to show the bank specific logo and UI theme.

To logo path (see how media resources are stored) and the UI theme can be found in the BANK_MAINDB table, and obtained from BankMain object.

For convenience, two files bankerheader.txt and bankertopnav.txt are separated out that can be included in the bank related jsp pages which do this. (Note how they are included in the BankerLogin.jsp page).

There are quite a few places where updation to a data structure needs someone else's approval. For example, a GL Code master update.

The steps are:

  1. A user (with the create or update responsibility) makes the update
  2. A task is automatically created for approval
  3. The task is displayed to the users who have the approval responsibility
  4. The user (with the approval responsibility) examines and then approves or rejects the update. They can even make some changes while approving.

What happens if someone queries the information before the changes are approved? They get the previous information and not the changes.

The mechanism to achieve it is as follows:

  1. When the updater makes changes to some data (or creates new data), do not update it into the database. Instead, keep it in the APPROVALWAITING table in JSON format.
  2. Use this data to show the information to the approver user.
  3. Update the database only once the changes are approved.

However, in case the updater user themselves have the approval responsibility, then the changes can be persisted to the database after showing a confirmation box. This bypasses the tasks functionailty and the APPROVALWAITING table entry.