We are evaluating Shiro for a custom Saas app that we are building. Seems like a great framework does does 90% of what we want, out of the box. My understanding of Shiro is basic, and here is what I am trying to accomplish.
Scenario 1
Authentication done via LDAP (MS Active Directory)
Create unique users in LDAP, make app aware of LDAP users, and have client admins provision them into whatever roles..
Scenario 2
Authentication also done via JDBC Relam in their database
Questions:
Common to Sc 1 & 2 How can I tell Shiro which database to use? I realize it has to be done via some sort of custom authentication filter, but can someone guide me to the most logical way ? Plan to use the virtual host url to tell shiro and mybatis which DB to use.
Do I create one realm per client?
Sc 1 (User names are unique across clients due to LDAP) If user jdoe is shared by client1 and client2, and he is authenticated via client1 and tries to access a resource of client2, will Shiro permit or have him login again?
Sc 2 (User names unique within database only) If both client 1 and client 2 create a user called jdoe, then will Shiro be able to distinguish between jdoe in Client 1 and jdoe in Client 2 ?
My Solution based on Les's input..
public class MultiTenantAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
TenantAuthenticationToken tat = null;
Realm tenantRealm = null;
if (!(authenticationToken instanceof TenantAuthenticationToken)) {
throw new AuthenticationException("Unrecognized token , not a typeof TenantAuthenticationToken ");
} else {
tat = (TenantAuthenticationToken) authenticationToken;
tenantRealm = lookupRealm(tat.getTenantId());
}
return doSingleRealmAuthentication(tenantRealm, tat);
}
protected Realm lookupRealm(String clientId) throws AuthenticationException {
Collection<Realm> realms = getRealms();
for (Realm realm : realms) {
if (realm.getName().equalsIgnoreCase(clientId)) {
return realm;
}
}
throw new AuthenticationException("No realm configured for Client " + clientId);
}
}
New Type of token..
public final class TenantAuthenticationToken extends UsernamePasswordToken {
public enum TENANT_LIST {
CLIENT1, CLIENT2, CLIENT3
}
private String tenantId = null;
public TenantAuthenticationToken(final String username, final char[] password, String tenantId) {
setUsername(username);
setPassword(password);
setTenantId(tenantId);
}
public TenantAuthenticationToken(final String username, final String password, String tenantId) {
setUsername(username);
setPassword(password != null ? password.toCharArray() : null);
setTenantId(tenantId);
}
public String getTenantId() {
return tenantId;
}
public void setTenantId(String tenantId) {
try {
TENANT_LIST.valueOf(tenantId);
} catch (IllegalArgumentException ae) {
throw new UnknownTenantException("Tenant " + tenantId + " is not configured " + ae.getMessage());
}
this.tenantId = tenantId;
}
}
Modify my inherited JDBC Realm
public class TenantSaltedJdbcRealm extends JdbcRealm {
public TenantSaltedJdbcRealm() {
// Cant seem to set this via beanutils/shiro.ini
this.saltStyle = SaltStyle.COLUMN;
}
@Override
public boolean supports(AuthenticationToken token) {
return super.supports(token) && (token instanceof TenantAuthenticationToken);
}
And finally use the new token when logging in
// This value is set via an Intercepting Servlet Filter
String client = (String)request.getAttribute("TENANT_ID");
if (!currentUser.isAuthenticated()) {
TenantAuthenticationToken token = new TenantAuthenticationToken(user,pwd,client);
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. "
+ "Please contact your administrator to unlock it.");
} // ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
ae.printStackTrace();
}
}
}
There are three multi-tenancy models: Database, Schema, and Table. In Database multi-tenancy, the application connects to a database and gets data while the tenancy logic is delegated to the ops layer.
In contrast to multi-tenancy in the cloud, there is a single tenancy or a single tenant and the adoption of single-tenancy architecture is also common. A single tenant architecture is one in which a single instance of a software application and infrastructure is offered to only one user.
In hierarchical multi-tenancy, there are defined hierarchies among tenants and sub-tenants for the purpose of setting business boundaries and functional isolation as shown in figure 2 below. The tenants and sub-tenants exhibit a tree-like structure, grouped in a logical way to represent organisation structures.
You will probably need a ServletFilter that sits in front of all requests and resolves a tenantId pertaining to the request. You can store that resolved tenantId as a request attribute or a threadlocal so it is available anywhere for the duration of the request.
The next step is to probably create a sub-interface of AuthenticationToken, e.g. TenantAuthenticationToken
that has a method: getTenantId()
, which is populated by your request attribute or threadlocal. (e.g. getTenantId() == 'client1' or 'client2', etc).
Then, your Realm implementations can inspect the Token and in their supports(AuthenticationToken)
implementation, and return true
only if the token is a TenantAuthenticationToken
instance and the Realm is communicating with the datastore for that particular tenant.
This implies one realm per client database. Beware though - if you do this in a cluster, and any cluster node can perform an authentication request, every client node will need to be able to connect to every client database. The same would be true for authorization if authorization data (roles, groups, permissions, etc) is also partitioned across databases.
Depending on your environment, this might not scale well depending on the number of clients - you'll need to judge accordingly.
As for JNDI resources, yes, you can reference them in Shiro INI via Shiro's JndiObjectFactory:
[main]
datasource = org.apache.shiro.jndi.JndiObjectFactory
datasource.resourceName = jdbc/mydatasource
# if the JNDI name is prefixed with java:comp/env (like a Java EE environment),
# uncomment this line:
#datasource.resourceRef = true
jdbcRealm = com.foo.my.JdbcRealm
jdbcRealm.datasource = $datasource
The factory will look up the datasource and make it available to other beans as if it were declared in the INI directly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With