The below bean class only allows the MANAGER role access to use the services exposed. In this case, findCustomerByAccountNumber. (The AccessRoles.MANAGER resolves to a string so if the string changes the CustomerServiceBean doesn't have to change.)
@RolesAllowed(AccessRoles.MANAGER) @Stateless(mappedName = JndiResourceName.CUSTOMER_SERVICE) @Remote(CustomerService.class) public class CustomerServiceBean implements CustomerService { @Override public Customer findCustomerByAccountNumber(String accountNumber) { Customer customer = null; // do stuff to find customer return customer; } }
To call the findCustomerByAccountNumber, the code can use the @RunAs annotation as described below:
@RunAs(AccessRoles.MANAGER) public void verifyCustomer(String accountNumber) { // do stuff Customer customer = customerService.findCustomerByAccountNumber(String accountNumber); // do more stuff }
But what if the roles calling the verify method can vary ie MANAGERS, OPERATORS, ADMIN. In this scenario, we would want to authenticate the 'caller' before accessing the findCustomerByAccountNumber method. A solution to this would be to use JAAS (Java Authentication and Authorization Service.)
The principal of this is to create realms and have users and groups in the realm. There are a few steps involved which are described as follows:
Firstly, the app server (in this example Glassfish) needs to create a realm and the below describes how to do this using the command line. It assumes connection pools, user and group database tables have been created and populated, and that the flexiblejdbcrealm-0.4.jar is in the Glassfish lib dir:
asadmin --host delete-auth-realm customer-realm
asadmin --host create-auth-realm --classname=org.wamblee.glassfish.auth.FlexibleJdbcRealm --property="jaas.context=customerJdbcRealm:datasource.jndi=
jdbc/Customer:sql.password=select password from customeruser where username\=?:sql.groups=select g.groupname from customergroup g inner join user_group ug on g.id\=ug.group_id inner join customeruser u on ug.user_id\=u.id where u.username\=?:password.digest=MD5:password.encoding=BASE64" customer-realm
jdbc/Customer:sql.password=select password from customeruser where username\=?:sql.groups=select g.groupname from customergroup g inner join user_group ug on g.id\=ug.group_id inner join customeruser u on ug.user_id\=u.id where u.username\=?:password.digest=MD5:password.encoding=BASE64" customer-realm
asadmin --host --user admin set server-config.security-service.activate-default-principal-to-role-mapping=true
asadmin --host set-log-level javax.enterprise.system.core.security=INFO
asadmin --host set-log-level org.wamblee.glassfish.auth=INFO
The login.conf needs to have the below added to it:
customerJdbcRealm {com.mypackage.auth.CustomerLoginModule required;}
The CustomerLoginModule class extends FlexibleJdbcLoginModule and gives us the ability to intercept the login/authentication calls if we so wish. In this case, any login exceptions are being logged:
public class CustomerLoginModule extends FlexibleJdbcLoginModule implements LoginModule { private static final Logger SECURITY_LOGGER = Logger.getLogger("com.mypackage"); @Override protected void authenticate() throws javax.security.auth.login.LoginException { try { super.authenticate(); } catch(LoginException le){ SECURITY_LOGGER.error("Authentication failed for " + _username + ". " + le.getMessage()); throw le; } } }
We can now change the verify method to authenticate before calling the 'secure' findCustomerByAccountNumber method:
private ProgrammaticLoginInterface programmaticLogin = new ProgrammaticLogin(); public void verifyCustomer(String accountNumber) { Customer customer = null; boolean loginSuccessful = programmaticLogin.login("manager", "password", "customer-realm", true); if (loginSuccessful) { customer = customerService.findCustomerByAccountNumber(String accountNumber); } else { // throw exception } }
The call to the ProgrammaticLogin instance attempts to use the supplied name and password directly to login to the current realm. If successful, a security context is created for that user and is used by the EJB when checking what roles are allowed to call it.
For example purposes, the above verifyCustomer method has the name and password hard coded but in reality these values could be obtained from a login web page or other such authentication mechanisms.
Hmm how do you get the RunAs annotation working with a remote method call? My IDE says "annotation type not applicable to this type of declaration".
ReplyDeleteUnknown. RunAs has to be done on the server side code, not client side. The client has to rely on the server to dictate who code get ran as. Hence why your IDE has an issue with this.
ReplyDelete