Applying JASS to a Sample Application

We will begin with describing a sample application, identify its authentication and authorization requirements and then use JAAS to add these features. We will be using the same application in subsequent chapters also to illustrate other security concepts. So let us spend some time understanding it.

The Sample Application

The sample application creates a highly simplified banking scenario. It consists of a number of files in %JSTK_HOME%srcorgjstkexampleank directory. Table 5-3 lists all the files and has a brief description of each file.

Table 5-3. Sample Application Files
FilenameBrief description
BankIntf.javaInterface to expose bank operations.
AccountIntf.javaInterface to expose operations on a bank account.
Exceptions.javaContainer class for all the exceptions thrown by BankIntf and AccountIntf methods.
ServerBank.javaBank is a concrete implementation of BankIntf.
serverAccount.javaAccount is a concrete implementation of AccountIntf.
serverDefaultBankPersistenceManager.javaConcrete class for storing serialized bank object on a file and loading it from file.
clientBankClient.javaA generalized client that takes a string command as argument, parses it and performs the corresponding operation on BankIntf or AccountIntf object.
clientBankClientShell.javaMain program to read user input from console in a loop and invoke BankClient methods to perform bank operations.
Note: Filenames are relative to %JSTK_HOME%srcorgjstkexampleank directory

If you look into the directory %JSTK_HOME%srcorgjstkexampleank, you will find some more files there. These are permission classes for various operations. We talk about these later when we enable the sample application for access control.

An important thing to notice is that BankClient always works with BankIntf and AccountIntf references and deals with inner class exceptions defined within the Exceptions class. It is initialized by main program BankClientShell that gets a BankIntf reference by calling load() method on DefaultBankPersistenceManager class. This kind of structure allows a good degree of separation among different classes and lets us add newer capabilities with a minimum amount of change later on.

Let us look at the source code of the BankIntf interface.

Listing 5-9. Definition of the interface BankIntf
// file: %JSTK_HOME%srcorgjstkexampleankBankIntf
package org.jstk.example.bank;

public interface BankIntf {
  public AccountIntf openAccount(java.math.BigDecimal initialDeposit);
  public void closeAccount(String acctNo)
      throws Exceptions.AccountNotFound, Exceptions.AccountClosed;
  public AccountIntf getAccount(String acctNo)
      throws Exceptions.AccountNotFound;
  public java.util.Iterator accounts();
}

Once you have a BankIntf object, you can open an account, list all the accounts, get an account for a given account number, and close an account through this object. Additional operations on an account are exposed by the AccountIntf interface.

Listing 5-10. Definition of the interface AccountIntf
// file: %JSTK_HOME%srcorgjstkexampleankAccountIntf
package org.jstk.example.bank;

import java.math.BigDecimal;

public interface AccountIntf {
  public void deposit(BigDecimal amt)
      throws Exceptions.AccountClosed;
  public void withdraw(BigDecimal amt)
      throws Exceptions.AccountClosed, Exceptions.InsufficientAmount;
  public void close() throws Exceptions.AccountClosed;
  public BigDecimal getBalance() throws Exceptions.AccountClosed;
  public String getAcctNo();
  public String getStatement();
}

These methods allow a client program to deposit, withdraw, check balance, and get account statement.

As mentioned earlier, a client program gets reference to a BankIntf object by invoking the load() method on the DefaultBankPersistenceManager class. Constructor of this class takes a java.util.Properties object as an argument and if this object has "org.jstk.example.bank.file" property then it takes the value of this property to be the pathname for persisting the Bank object. If no such property exists or the property exists but the corresponding file doesn't exist then it creates a new Bank object and returns reference to this newly created object. In case the property has been set to a non-existing file, subsequent modifications to the Bank object are persisted to that file. Here is a code fragment from BankClientShell.java showing this sequence and initialization of BankClient object.

Listing 5-11a. Initialization of the BankClient in BankClientShell.java
BankPersistenceManagerIntf bpm =
new DefaultBankPersistenceManager(System.getProperties());
BankIntf bank = bpm.load();
BankClient bc = new BankClient();
bc.init(bank);

BankClientShell program works like a normal shell: waiting for user input at the shell prompt, reading the user input from a console, passing the input string to BankClient for execution, displaying the result to console, and going back to shell prompt. The code fragment shown in Listing 5-11b accomplishes this.

Listing 5-11b. User input loop in BankClientShell.java
while (true){
  System.out.print("bcsh>");
  System.out.flush();
  String cmdline = new BufferedReader(
        new InputStreamReader(System.in)).readLine();
  String[] cmdargs = cmdline.split("\s");

  String result = bc.execCommand(cmdargs);
  System.out.println(result);
}

The sample application source files are part of the main JSTK sources and are compiled by running the build program Apache Ant on main build.xml, found in the home directory of JSTK installation. Compiled .class files are placed in archive file buildjstk.jar.

Here is a sample execution of BankClientShell class, assuming that the environment variable CLASSPATH is set to include the JSTK jar file jstk.jar.

C:jstk>java –Dorg.jstk.example.bank.file=configankdb.ser 
							org.jstk.example.bank.client.BankClientShell
bcsh>open 10000.00
							-- Open an account with initial deposit of 10000.00
Account Opened: 1000 – 1000 is the account number.
bcsh>current
							-- Show the current account number.
Current Account: 1000
bcsh>withdraw 200.00
							-- Withdraw 200.00 from current account.
Withdrawn: 200.00
bcsh>balance
							-- Show the current balance.
Current Balance: 9800.00
bcsh>quit
c:jstk>

If you run the same program again, you can see the previous balance as everything is persisted in file configankdb.ser.

C:jstk> java –Dorg.jstk.example.bank.file=configankdb.ser 
							org.jstk.example.bank.client.BankClientShell
bcsh>get 1000
							-- get the account with account number 1000
Current Account: 1000
bcsh>balance
Current Balance: 9800.00
bcsh>quit

Figure 5-3 shows the main interfaces and classes of this sample application and interaction among objects for the previous commands.

Figure 5-3. Sample application interfaces and classes.


Although we have omitted a lot of details of the sample application, hopefully you get a fair idea of what it is all about. If you want more details, look at the sources in JSTK.

Authentication and Authorization Requirements

Now that we understand the sample application, we can talk about its authentication and authorization requirements.

The current shell client, BankClientShell, allows all operations to anyone who can run this program. This is perhaps not how a real bank application should function. We want only valid users to be able to run this application. Essentially, all users must be authenticated.

Not all authenticate users should be allowed the same level of privilege. A category of users, perhaps bank employees with appropriate job functions, should be allowed to open and close accounts. These users would also be able to list all accounts and look into any of the accounts, but not perform transactions like withdrawal or deposits. Another category of users, the account owners, should have complete access to their own accounts but no access whatsoever to other accounts.

More complex schemes are possible, but we stick to these and see how JAAS can be used to secure the sample application with these requirements.

JAAS Enabled Sample Application

Satisfying the authentication requirement is easy. We have already seen an example of using a login module and a user account system with the DisplayFileLaucher program. It used a keystore as a user account system and KeyStoreLoginModule for login validation. To enable user authentication for the sample application through JAAS, we need to modify the launcher, BankClientShell, to go through the login sequence and run the main loop as a privileged action in the context of logged-in user. This modified file, which we name BCSecureShell.java, is shown below.

Listing 5-12. Definition of the class BCSecureShell.java
// BCSecureShell.java
import java.io.InputStreamReader;
import java.io.BufferedReader;
import org.jstk.example.bank.BankIntf;
import org.jstk.example.bank.server.DefaultBankPersistenceManager;
import org.jstk.example.bank.client.BankClient;
import org.jstk.uam.DefaultCallbackHandler;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import java.security.PrivilegedExceptionAction;

public class BCSecureShell {
  public static void main(String[] args) throws Exception {
    // Initialize BankClient.
    final BankClient bc = new BankClient();
    DefaultBankPersistenceManager bpm =
        new DefaultBankPersistenceManager(System.getProperties());
    BankIntf bank = bpm.load();
    bc.init(bank);

    // Let the user login.
    LoginContext lc =
        new LoginContext("BCSecureShell", new DefaultCallbackHandler());
  try {
    lc.login();
    } catch (javax.security.auth.login.LoginException le){
      System.err.println("Login failed. Exception: " + le);
      return;
    }
    PrivilegedExceptionAction action= new PrivilegedExceptionAction(){
      public Object run() throws Exception{
        while (true){          // infinite loop.
          System.out.print("bcssh>");
          System.out.flush();
          String cmdline = new BufferedReader(
               new InputStreamReader(System.in)).readLine();
          String[] cmdargs = cmdline.split("\s");
          String result = bc.execCommand(cmdargs);
          System.out.println(result);
        }
      }
     };
     Subject.doAs(lc.getSubject(), action);
  }
}

The class DefaultCallbackHandler is similar to the DFCallbackHandler we have already encountered. It has been packaged within JSTK so that it becomes available to any application that uses JSTK.

You can find this and other related files in the srcjsbookch5ank subdirectory of JSTK installation directory. Note that it is not part of the main JSTK tree but appears along with other examples from this chapter. We use this subdirectory as the working directory for this example.

Compile BCSecureShell.java and archive it in the bcssh.jar file. Assuming that compiled classes of main JSTK tree are available in the %JSTK_HOME%uildjstk.jar, issue the following commands:

C:ank>set classpath=.;%JSTK_HOME%uildjstk.jar
C:ank>javac BCSecureShell.java
C:ank>jar cvf bcssh.jar *.class
						

This program needs a login configuration file with an entry identified by string "BCSecureShell". The file login.conf has this entry:

// login.conf
BCSecureShell {
    org.jstk.uam.JSTKLoginModule required uamfile="uamdb.ser";
};

This configuration requires the user account information to be stored in the file uamdb.ser of the current directory. Recall that we created one such file in %JSTK_HOME%config directory earlier with the uamsh tool. You can either move that file to the %JSTK_HOME%srcjsbookch5ank directory or run the uamsh tool again from this directory, specifying the uamdb.ser file as the place to store user account information.

The authentication problem is solved. Let us now turn our attention to the authorization problem. We should be able to specify appropriate authorizations or access privileges in a Java policy file for users and roles for the operations exposed through BankIntf and AccountIntf. This is best done by having separate permission classes for Bank and Account operations and letting the method implementations of classes Bank and Account perform permission check.

We create two permission classes, BankPermission and AccountPermission, both in the package org.jstk.example.bank.server, for this purpose. The target name for both of these permission classes is the account number, implying permission for that particular account. A "*" string is a valid target name and implies all accounts. The action string for BankPermission is a comma or space-separated string having one or more of the following: open, close, get and list. Similarly, the action string for AccountPermission can have one or more of the following: open, close, deposit, withdraw, and read. Read permission on an account translates into permission to get a reference to the account, inquire about the balance and get a statement of past transactions.

A concrete permission class is implemented by subclassing abstract class java.security.Permission or one of its subclasses. The derived class overrides the methods as per the desired semantics. During permission check, the AccessController invokes these methods to determine whether the caller has the appropriate privilege or not. For this reason, it is important that the derived class adheres to the general contract defined by Permission class. This contract is in the form of following four public abstract methods:

boolean equals(Object o) – returns true if the argument o is of the same type, has the same name and has same actions. It returns false otherwise.

int hashCode() – returns the same value for an object during its lifetime within a program execution. Equal permissions should return the same value.

String getActions() – returns string representation of actions.

boolean implies(Permission p) – returns true if the current permission is the same or more permissive than the argument permission. For example, a BankPermission with “*” as the target name implies any other BankPermission provided the actions are the same or are a subset of the actions of the former. This is the key function for the whole mechanism to work properly and efficiently.

There are other methods in the class Permission but they need not be overridden. One such method, newPermissionCollection(), deserves special mention as it can be used for more efficient handling of permissions. However, it requires implementing an extra class for almost every concrete permission class. The details can be found in Javadocs.

Listing 5-13 shows how the above-mentioned contract is implemented for the class BankPermission. The implementation of AccountPermission is similar, and is not shown here.

Listing 5-13. Source File BankPermission.java
// File: BankPermission.java
package org.jstk.example.bank.server;

import java.security.Permission;
import java.util.StringTokenizer;

public class BankPermission extends Permission {
  protected int mask = 0;
  private static int OPEN = 0x01;
  private static int CLOSE = 0x02;
  private static int GET = 0x04;
  private static int LIST = 0x08;
  private String actions = null;

  public BankPermission(String name){
   super(name);
  }

  public BankPermission(String name, String action){
    super(name);
    parse(action);
  }

  private void parse(String action){
    StringTokenizer st = new StringTokenizer(action, ",	 ");
    while (st.hasMoreTokens()){
      String tok = st.nextToken();
      if (tok.equals("open")) mask |= OPEN;
      else if (tok.equals("close")) mask |= CLOSE;
      else if (tok.equals("get")) mask |= GET;
      else if (tok.equals("list")) mask |= LIST;
      else
        throw new IllegalArgumentException("Unknown action: " + tok);
    }
  }

  public boolean implies(Permission p) {
    if ((p == null) || (p.getClass() != getClass()))
      return false;
    BankPermission that = (BankPermission) p;
    if (getName().equals(that.getName()) || getName().equals("*")){
      if ((mask & that.mask) == that.mask)
        return true;
    }
    return false;
  }

  public boolean equals(Object o){
    if (o == this) return true; // test against self. Return true

    if ((o == null) || (o.getClass() != getClass()))
      return false;
    BankPermission that = (BankPermission) o;
    if (getName().equals(that.getName()) && (mask == that.mask))
      return true;
    return false;
  }

  public int hashCode(){
    return (getName().hashCode() ^ mask);
  }

  public String getActions(){
    StringBuffer sb = new StringBuffer();
    if ((mask & OPEN) == OPEN) sb.append(" open");
    if ((mask & CLOSE) == CLOSE) sb.append(" close");
    if ((mask & GET) == GET) sb.append(" get");
    if ((mask & LIST) == LIST) sb.append(" list");
    return sb.toString();
  }
}

This code is self-explanatory and doesn't need further elaboration. The only thing worth mentioning is the way actions are converted into a bit mask. This makes the subset test really simple in the implies()method and also comes in handy while testing for equality and computing the hashcode.

How are these permissions used within Bank and Account classes? Let us look at the following code fragment from Bank.java to get an idea:

public java.util.Iterator accounts(){
  checkPermission("*", "list");
  return acctsTable.values().iterator();
}
private void checkPermission(String name, String action){
  SecurityManager security = System.getSecurityManager();
  if (security != null) {
    BankPermission bp = new BankPermission(name, action);
    Java.security.AccessController.checkPermission(bp);
  }
}

Recall that the accounts() method returns a list of all the accounts in the bank and must require permission for list action on all account numbers. Whether the current context has this permission or not is checked by static method checkPermission() of AccessController class. This method takes an object representing the requested permission as an object. As we have already discussed, checkPermission() goes through the current stack of the AccessControllerContext object and verifies that every element on the stack has permissions that imply the requested permission. If all the verifications succeed, this method returns quietly. If not, it throws AccessControlException, a runtime exception that need not be explicitly declared.

Now we are ready to look at the policy file bank.policy that would enable us to run the sample application with JAAS enabled security.

// bank.policy
grant codeBase "file:${user.dir}/bcssh.jar" {
  permission java.util.PropertyPermission "*", "read, write";
  permission javax.security.auth.AuthPermission "createLoginContext";
  permission javax.security.auth.AuthPermission "doAs";
  permission java.io.FilePermission "${user.dir}${/}*", "read";
  permission org.jstk.example.bank.server.BankPermission "*",
      "open, close, get, list";
  permission org.jstk.example.bank.server.AccountPermission "*",
      "open, close, withdraw, deposit, read";
};

grant {
  permission java.io.FilePermission "${user.dir}${/}*", "read, write";
  permission javax.security.auth.AuthPermission "modifyPrincipals";
};
grant
  Principal org.jstk.uam.JSTKRolePrincipal "admin" {
  permission org.jstk.example.bank.server.BankPermission "*",
      "open, close, get, list";
  permission org.jstk.example.bank.server.AccountPermission "*",
      "open, close, deposit, read";
};

grant
  Principal org.jstk.uam.JSTKUserPrincipal "pankaj" {
  permission org.jstk.example.bank.server.AccountPermission "1000",
      "withdraw, deposit, read";
  permission org.jstk.example.bank.server.BankPermission "1000", "get";
};

Here is a summary of the access rules in this file:

The code in bcssh.jar has been given wide-ranging privileges. This is required as this code will be in the calling context stack of every operation and its privileges must be a superset of all privileges.

JSTKLoginModule code needs to have the permission to add Principals to Subject and read from the current directory (where the user account file uamdb.ser resides). The sample application needs write permission in the current directory for persisting the bank data.

Role admin has permission to carry out any operation.

User pankaj, owner of account number 1000, has withdraw, deposit and read permission on that account. It also has BankPermission for get action.

Let us now invoke BCSecureShell, specifying all the system properties to enable security manager, login configuration file, policy file and the file to persist bank data. Remember that we have already populated the user account system with users pankaj and veena and user veena has been assigned admin role.

C:ank>java -Djava.security.manager
							-Djava.security.policy=bank.policy 
							-Djava.security.auth.login.config=login.conf 
							-Dorg.jstk.example.bank.file=bankdb.ser 
							-cp bcssh.jar;%JSTK_HOME%uildjstk.jar BCSecureShell
login:  veena
password:  veenapass
bcssh>open 1200.00
Account Opened: 1000
bcssh>open 2000.00
Account Opened: 1001
bcssh>quit
						

The sequence of command opens two accounts, one with an opening balance of $1,200.00 and other with $2,000.00. The first account is given 1000 as an account number and the second one is 1001.

The command to run BCSecureShell with all the options is available in script file bcssh.bat. We use this script to run it again, and this time we login as user pankaj:

C:ank>bcssh
login:  pankaj
password:  pankajpass
bcssh>get 1001
Access denied
bcssh>get 1000
Current Account: 1000
bcssh>deposit 200.00
Deposited: 200.00
bcssh>statement
----------------- BEGIN BANK STATEMENT -----------------
Statement Date : Sun Jan 26 01:51:33 PST 2003
Account#       : 1000
Account Status : OPEN
Transactions   :
Sun Jan 26 01:50:30 PST 2003   OPEN     0.00     0.00     account open
Sun Jan 26 01:50:30 PST 2003   CREDIT   1200.00  1200.00  cash deposit
Sun Jan 26 01:51:26 PST 2003   CREDIT   200.00   1400.00  cash deposit
------------------ END BANK STATEMENT ------------------

bcssh>quit
						

You can see the access control in action. User pankaj cannot get account 1001 because he has not been authorized to access this account in the bank.policy file. He can access account 1000 because he has the authorization.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.157.182