User Access Control¶
You have seen some aspects of Krail’s User Access Control already, and are probably aware that it provides this by integrating Apache Shiro. This Tutorial will not attempt to cover the whole of Shiro’s capability - Shiro’s own documentation does a good job of that already.
What we will do, however, is demonstrate some of the features of Shiro, within a Krail context:
- Implementing a Realm. Implement a trivial Realm to provide authentication and authorisation
- Page Access Control. This is Krail specific use of Shiro features to determine whether a user has permission to access a page
- Coded access. Checking from code whether a user has permissions to do something
- Access Control annotations. This will demonstrate the use of Shiro’s annotations, as an alternative to using coded access
Krail does not yet provide any user management capability (the management of users, groups & roles etc) as this is often provided via LDAP, Active Directory or Identity Management systems. There is an open ticket for it, so it may be developed one day.
Example¶
We will take this opportunity to tidy up our site, and limit who can use different parts of the site. This is what we want to achieve:
- the ‘finance’ pages should be on their own branch
- public pages will remain available to any user
- private pages will be limited to just 2 users, “eq” and “fb”
- both users will have access to the ‘private’ branch
- both users will be able to change their own options
- ‘fb’ will be able to access the finance pages, but ‘eq’ will not.
- there will be an ‘admin’ user who can access all pages and change all options
At this point we must stress that this is going to be a trivial example of User Access Control, and to do it properly you need to consult the Shiro documentation. This Tutorial should give you some useful pointers, however.
Move the Pages¶
To move the ‘finance’ pages:
- change the line in the
BindingManager
to put theMyPages
root URI at finance instead of private/finance-department
baseModules.add(new MyPages().rootURI("finance"));
- In the
PurchasingView
change the uri parameter to be finance/purchasing
package com.example.tutorial.pages;
import com.google.inject.Inject;
import uk.q3c.krail.core.navigate.sitemap.View;
import uk.q3c.krail.core.shiro.PageAccessControl;
import uk.q3c.krail.core.view.Grid3x3ViewBase;
import uk.q3c.krail.i18n.Translate;
@View(uri = "finance/purchasing", pageAccessControl = PageAccessControl.PERMISSION, labelKeyName = "Purchasing")
public class PurchasingView extends Grid3x3ViewBase {
@Inject
protected PurchasingView(Translate translate) {
super(translate);
}
}
- In
NewsView.doBuild()
change the button event to point to the new page location ` navigateToPrivatePage.addClickListener(c -> navigator.navigateTo(“finance/accounts”)); `
User accounts¶
- create a new package, ‘com.example.tutorial.uac’
- in that package create a new class “TrivialUserAccount” - it is obvious what it does
package com.example.tutorial.uac;
import java.util.Arrays;
import java.util.List;
public class TrivialUserAccount {
[source]
----
private String password;
private List<String> permissions;
private String userId;
public TrivialUserAccount(String userId, String password, String... permissions) {
this.userId = userId;
this.password = password;
this.permissions = Arrays.asList(permissions);
}
public String getUserId() {
return userId;
}
public String getPassword() {
return password;
}
public List<String> getPermissions() {
return permissions;
}
----
}
<div class=”admonition note”> <p class=”first admonition-title”>Note</p> <p class=”last”>You may notice that there is no “role” in this user account. You can certainly use Shiro’s roles in Krail, but we prefer to use permissions for the <a href=”https://shiro.apache.org/authorization.html#Authorization-ElementsofAuthorization” target=”“>reasons given</a> by the Shiro team.</p> </div>
Credentials Store¶
- create a class “”TrivialCredentialsStore” as somewhere to keep the user accounts:
package com.example.tutorial.uac;
import com.google.inject.Inject;
import java.util.HashMap;
import java.util.Map;
public class TrivialCredentialsStore {
private Map<String, TrivialUserAccount> store = new HashMap<>();
@Inject
protected TrivialCredentialsStore() {
}
public TrivialCredentialsStore addAccount(String userId, String password, String... permissions) {
store.put(userId, new TrivialUserAccount(userId, password, permissions));
return this;
}
public TrivialUserAccount getAccount(String principal) {
return store.get(principal);
}
}
- define the users’ credentials to meet our requirements - we’ll just put them in the constructor
@Inject
protected TrivialCredentialsStore() {
addAccount("eq", "eq", "page:view:private:*","option:edit:SimpleUserHierarchy:eq:0:*:*");
addAccount("fb", "fb", "page:view:private:*","page:view:finance:*","option:edit:SimpleUserHierarchy:fb:0:*:*");
addAccount("admin", "password", "page:view:*","option:edit:*");
}
Permission Strings¶
What we have done here is give users specific credentials. The userId
and password are obvious. The permission strings use Shiro’s
WildcardPermission
.
This is a very flexible way of defining
permissions. Krail uses
the WildcardPermission
to define page and option approval.
Page Permission¶
So for example, a page with a url of:
private/apage/asubpage/id=1
is translated by Krail’s PagePermission
into a Shiro compatible
syntax of:
page:view:private:apage:asubpage
This represents:
- resource type (‘page’)
- action (‘view’)
- resource instance (the Url with the ‘/’ transposed to a ‘:’ to match the Shiro syntax)
- the url parameter is ignored, because it is not part of the page definition
This is then compared, by Shiro, with the permission a user has been given. Both ‘eq’ and ‘fb’ have been given a permission: ` page:view:private:* ` which translates to “for a resource type page, this user can view any with a url starting with private”
The ‘admin’ user has been given permission to view any page, simply by wildcarding all pages
page:view:*
Option permission¶
An Option follows a similar pattern, provided by OptionPermission
- resource type (‘option’)
- action (‘edit’)
- resource instance (an option) structured [hierarchy]:[user id]:[hierarchy level index]:[context]:[option name]:[qualifier]:[qualifier]
Thus the option permissions given to ‘eq’ and ‘fb’ only allow them to
edit their own options in the SimpleUserHierarchy
. This is set by
giving permission only at the user level, hierarchy level index = 0
Again the ‘admin’ user is all-powerful, with permission to edit any option:
option:edit:*
Authentication¶
Shiro has the concept of a Realm
, where the rules for Authentication
and Authorisation are defined - by you, as they will be application
specific. Shiro offers a number of ways to implement
Realm,
and here we will just provide a trivial example, combining
authentication and authorisation into one Realm
We will sub-class AuthorizingRealmBase
, as that provides a mechanism
for enabling the cache via Guice.
- in the package, ‘com.example.tutorial.uac’ create a class
“TutorialRealm”, extending
AuthorizingRealmBase
package com.example.tutorial.uac;
import uk.q3c.krail.core.shiro.AuthorizingRealmBase;
public class TutorialRealm extends AuthorizingRealmBase {
}
- We want to use our
TrivialCredentialsStore
, so we will inject that into the constructor - Caching obviously is not needed for this trivial case, but we will
pass
Optional<CacheManager>
toAuthorizingRealmBase
. This will allow us to demonstrate enabling the cache from Guice.
public class TutorialRealm extends AuthorizingRealmBase {
private TrivialCredentialsStore credentialsStore;
@Inject
protected TutorialRealm(Optional<CacheManager> cacheManagerOpt, TrivialCredentialsStore credentialsStore) {
super(cacheManagerOpt);
this.credentialsStore = credentialsStore;
}
}
- provide the authentication logic by overriding
doGetAuthenticationInfo()
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
TrivialUserAccount userAccount = credentialsStore.getAccount((String) token.getPrincipal());
if (userAccount == null) {
return null;
}
String tokenCredentials = new String((char[])token.getCredentials());
if(userAccount.getPassword().equals(tokenCredentials)) {
return new SimpleAuthenticationInfo(userAccount.getUserId(),token.getCredentials(),"TutorialRealm");
}else{
return null;
}
}
This logic returns null if the user account is not found, or the
password supplied by the token does not match the credentials. If
authentication is successful, a populated instance of
SimpleAuthenticationInfo
is returned
Authorisation¶
- override
doGetAuthorizationInfo()
to provide the authorisation logic
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
TrivialUserAccount userAccount = credentialsStore.getAccount((String) principals.getPrimaryPrincipal());
if (userAccount != null) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(new HashSet<>(userAccount.getPermissions()));
return info;
}
return null;
}
This logic returns a populated SimpleAuthorizationInfo
instance if
the user account is found, or null if not
Using the Realm¶
- override the
shiroModule()
method in theBindingManager
to use the newRealm
- enable the cache as shown
@Override
protected Module shiroModule() {
return new DefaultShiroModule().addRealm(TutorialRealm.class).enableCache();
}
*
run the application and check to see if we have met our requirements:
- log in as ‘eq’, with password ‘eq’
- private pages should be visible, but not the finance pages or system admin pages
- you should still be able to modify options on the “My News” page
- pressing the “system option” button on “My News” will result in a “You do not have permission” message
- log out
- log in as ‘fb’ - try a wrong password if you like, the correct password should be ‘fb’
- private and finance pages should be visible, but not system admin pages
- you should still be able to modify options on the “My News” page
- pressing the “system option” button on “My News” will result in a “You do not have permission” message
- log out
- log in as ‘admin’, password= ‘password’
- private, finance and system admin pages pages should all be visible
- you should still be able to modify options on the “My News” page
- pressing the “system option” button on “My News” remove the CEO news
So far this has all been done using page and option permissions. The
visibility of pages is actually managed through PageAccessControl
which limits what is made available to the navigation components. You
can take also direct control using code or Shiro annotations.
Control Access Through Code¶
At the moment the “system option” button on “My News” can result in a “You do not have permission” message. It does not make much sense to make the button available to a user who is not allowed to use it, so let’s hide the button unless the user has permission.
- to get access to the current Shiro
Subject
, we inject aSubjectProvider
- modify
MyNews
to do so:
@Inject
public MyNews(Option option, OptionPopup optionPopup, SubjectProvider subjectProvider, Translate translate) {
super(translate);
this.option = option;
this.optionPopup = optionPopup;
this.subjectProvider = subjectProvider;
}
- in
MyNews.doBuild()
make the visibility conditional on the user having permission
if (subjectProvider.get().isPermitted("option:edit:SimpleUserHierarchy:*:1:*:*")) {
systemOptionButton.setVisible(true);
}else{
systemOptionButton.setVisible(false);
}
Here we have asked Shiro to confirm permission at the most specific
level, as recommended by Shiro. This permission string is checking that
the user has permission to edit any option at level 1 (the ‘system’
level) in the SimpleUserHierarchy
- run the application and log in as ‘eq’ or ‘fb’ and you will not be able to see the “system option” button. Log in as ‘admin’, however, and the “system option” button is visible.
Control Access Through Annotations¶
Shiro provides a set of annotations to cover most circumstances. We will use @RequiresPermissions as an example
- on the
MyNews
page add another button indoBuild()
payRiseButton = new Button("request a pay rise");
payRiseButton.addClickListener(event-> requestAPayRise());
setBottomLeft(payRiseButton);
- inject the
UserNotifier
@Inject
public MyNews(Option option, OptionPopup optionPopup, SubjectProvider subjectProvider, Translate translate, UserNotifier userNotifier) {
super(translate);
this.option = option;
this.optionPopup = optionPopup;
this.subjectProvider = subjectProvider;
this.userNotifier = userNotifier;
}
- create the
requestAPayRise
method - use
userNotifier
to give feedback - create the enum constant DescriptionKey.You_just_asked_for_a_pay_increase
protected void requestAPayRise() {
userNotifier.notifyInformation(DescriptionKey.You_just_asked_for_a_pay_increase);
}
- We want to restrict who can use the method, so we will annotate it with a new permission
@RequiresPermissions("pay:request-increase")
protected void requestAPayRise() {
userNotifier.notifyInformation(DescriptionKey.You_just_asked_for_a_pay_increase);
}
Nobody currently has permission to do this, so let’s allow user ‘eq’ to do this
- modify the entry for ‘eq’ in
TrivialCredentialsStore
to add this permission
addAccount("eq", "eq", "page:view:private:*","option:edit:SimpleUserHierarchy:eq:0:*:*","pay:request-increase");
- run the application
- log in as ‘eq’
- navigate to “My News” and press “request a pay rise”.
- A notification pops up to confirm the request. (Unfortunately it doesn’t say what will happen to the request)
- log in as ‘fb’ or ‘admin’
- navigate to “My News” and press “request a pay rise”.
- you receive a “not permitted” message
Summary¶
We have: - Shown how to control access to pages<br> - Shown how access control is applied to Options<br> - Shown how to control access using code<br> - Shown how to control access using annotations<br> - Built a very simple credential store with user accounts<br> - Demonstrated some uses of Shiro’s Wildcard permissions<br>
Download from GitHub¶
To get to this point straight from GitHub:
git clone https://github.com/davidsowerby/krail-tutorial.git
cd krail-tutorial
git checkout --track origin/krail_0.10.0.0
Revert to commit User Access Control Complete