KAAccessControlA framework for easy user access control. Samuel Pelletier
Why ?
• All apps require some user right management.
• Creating permission management is boring and may be tedious.
• A basic implementation always need refactoring after new modules are added to an application.
• I'm lazy and prefer to use a proved model and limit code required in a new app.
• I do not like having to rewrite non app specific code all the time.
Some history
• Basic principles (Roles and Lists) used since 2003 in PHP apps.
• First implementation of the model in PHP in 2006 for a central user management system used by many php apps.
• The model can manage all private apps I worked on.
• First WebObjects implementation in 2011.
• Complete rewrite in 2013 for easier integration.
Do / don't do
• Manage user access rights and include a permission editor UI component.
• Manage component access rights with annotation.
• Support territory or other data segmentation needs with list associated to role.
• Do not provide authentication but includes password hashing classes you may use.
• Probably too complex for a public accessible site or app.
Definition: List
• List are used to segment data or access right.
• List are usually linked to another entity of the application like territory, division, warehouse, department, ...
• A list contains items; a territory list may contain north America, south America, Europe and Asia for example.
• Items are used to specify with part of the data is relevant for a user; a manager for the north and south Americas for example.
Definition: Role
• Role are the base permission unit.
• Used to define what a user can do; a user has roles.
• Used to define who can access a component; a component is accessible by user with a specified role.
• Role can be qualified by item from list for data segmentation; a salesman role is qualified by territory item(s). used to define watch a user can do
Definition: Profile
• A profile define a user type; administrator is a common user profile.
• A profile is defined by a set of included roles and a set of optional roles.
• Profile are used to simplify the user right management. By selecting a profile for a user, the set of options is limited to relevant roles.
Definition: User Profile
• A user profile is a complete user access right definition with:
• A profile with it's included roles and selected role's items,
• A set of optional roles selected with their role's items.
• A user can have multiple user profiles but only one is effective and one is marked as default (selected when the user log in).
• Multiple user profiles are used for peoples requiring complex permission with incompatible data segmentation like a warehouse manager also a sale manager.
UML model
Managed by the UserPermissionEditor component or your own permission editor.
Managed by a plist file specified in properties or manually.
Updated automatically if the list is linked to an entity.
You have to create this entity.idpasswordHashapp specific attributes...
Real user entity
idroleGroup_idcodedisplayOrderpermissionList_idallowsMultipleItems
KARole
idcode
KAProfile
iddisplayOrdercode
KARoleGroup
*
* **
*
idcode
KAAccessList
userProfileRole_idpermissionListItem_id
KAUserProfileRoleItem
iduserProfile_idrole_id
KAUserProfileRoleiduser_idprofile_idisDefaultProfile
KAUserProfile
profile_idrole_idisOptionnal
KAProfileRole*
*
*
*
idaccessList_idcode
KAAccessListItem
*
*
idpasswordHash
KAUser
Using it in a project
• Add the framework to build path.
• Create the real user entity in your model by selecting the KAUser as parent class.
• Implements the createHomePageForUserProfile(...) method in your user class.
• Create the foreign key constraint on KAUserProfile in your migration code.
• Create the role.plist file.
• Add some properties.
• Create your user management components using the provided UserPermissionsEditor.
• Total time: about 15 minutes for a working skeleton.
New app demo
Framework Properties
• ka.accesscontrol.rolesFile : the name of the plist file with your role and profile definition.
• ka.accesscontrol.rolesFileBundle : the name of the framework containing your roles file. Leave blank if the file is in app bundle.
• ka.accesscontrol.loggedOutPageName : the name of the component to return when a user log out.
• er.migration.migrateAtStartup=true
• er.migration.modelNames=KAAccessControl,yourModels
In your framework or application Properties file, add:
#Path to the plist file read at startup to updates the frameworks data tables AccessList, RoleGroup, Role and Profile.ka.accesscontrol.rolesFile=Roles#Name of the framework that contain the plist file, not needed if the file is inside the app bundle.ka.accesscontrol.rolesFileBundle=FrameworkName!#Name of the WOComponent to return when a user log out of the system.ka.accesscontrol.logedOutPageName=LoggedOut!# Migrationser.migration.migrateAtStartup=trueer.migration.createTablesIfNecessary=trueer.migration.modelNames=KAAccessControl,YourModelNames...!#Make sure you put KAAccessControl before your migration if you want to # create the foreign key constraint or some bootstrap users.
Some useful addition to your migration class
To create the foreign key constrain in the database, add this line in your migration class:!KAAccessControlMigrationHelper.addForeignKeyAndIndex(database, "YourRealUserTableName");!Make sure the Roles are up to date before creating the first userRolesFileLoader.loadRolesFile(editingContext);editingContext.saveChanges();!#Bootstrap an admin user.#Suppose your real user class is User...User admin = ERXEOControlUtilities.createAndInsertObject(editingContext, User.class);admin.changePassword("adminPassword");admin.defaultUserProfile().setProfileWithCode("Admin");!#Set other mandatory attributes in your User entity...admin.setLanguage(User.englishLanguageCode);admin.setUsername("admin");!
Create the role.plist file (use the sample as template)
{lists = ( "Territory", "Wharehouse");roleGroups = ( { code = "Sales"; roles = ( { code = "CustomerSalesReport"; listCode = "Territory"; allowsMultipleItems = YES; }, { "code" = "SeeGrossProfits"; }, ); });profiles = ( { code = "Salesman"; roles = ( "CustomerSalesReport" ); optionalRoles = ( "SeeGrossProfits" ); });
The User class
// This required method create the home page based on the UserProfile selected.! @Override public WOComponent createHomePageForUserProfile(WOContext context, KAUserProfile userProfile) { if (userProfile.profileCode().equals(Profile.Admin)) { return ERXApplication.application().pageWithName("UserList", context); } if (userProfile.profileCode().equals(Profile.WharehouseManager)) { return ERXApplication.application().pageWithName("WarehouseDashboard", context); } return ERXApplication.application().pageWithName("CustomerList", context); }
The List, Profile and Role classes
• These classes are not mandatory but highly recommended.
• Put the list, profile and role code in string constants.
• Use these contants for annotations or permission checking in the code.
• Using constants helps to reduce typing errors.
• Eclipse has a nice function to find all references to a constant.
Localization
• Profile name in key "profile.profileCode"
• Role name in key "role.roleCode"
• RoleGroup name in key "group.groupCode"
• Few strings found in the framework Localizable.strings
ListItemAutoUpdater
• Link a list to an entity using a Java annotation.
• @AutoSyncWithAccessList(listCode="ListCode", nameProperty=EntityClass.NAME_KEY)
• List item copy the name returned by the key specified in the annotation as displayed name on the UI.
• Retrieve selected items as EOs from a user profile with itemsAsObjectsForRole(eoClass, roleCode).
• The system autostart in the framework didFinishInitialization().
Auto synced entity
@AutoSyncWithAccessList(listCode=List.Territory, nameProperty=SaleTerritory.LOCALIZED_NAME_KEY)@SuppressWarnings("serial")public class SaleTerritory extends com.kaviju.accesscontroldemo.model.base._SaleTerritory { @SuppressWarnings("unused") private static Logger log = Logger.getLogger(SaleTerritory.class);! static public final String LOCALIZED_NAME_KEY = "localizedName"; static public final ERXKey<String> LOCALIZED_NAME = new ERXKey<String>(LOCALIZED_NAME_KEY);! public String localizedName() { if (ERXLocalizer.currentLocalizer().languageCode().equalsIgnoreCase(User.frenchLanguageCode)) { return nom(); } return name(); }}
UserAccessControlService• Responsible to manage the current userProfile and the personify
stack.
• Personify allow a user to log as another user, very useful for technical support.
• There is a real and current user (usually the same) like in UNIX.
• The session create the service and pass a delegate for logon events; usually itself.
• The delegate receive userProfileDidLogon(userProfile) to prepare or clean the session for the new active profile.
Session.java
public class Session extends ERXSession implements UserLogonDelegate{ private UserAccessControlService<User> userAccessService;! public Session() { userAccessService = new UserAccessControlService<User>(this); setTimeOut(10*60); ... }! @Override public void userProfileDidLogon(KAUserProfile userProfile) { setLocalizerWithUserLanguage(userProfile.user(User.class)); setTimeOut(4*3600); if (Profile.ShortSession.equals(userProfile.profileCode())) { setTimeOut(15*60); } }! public void setLocalizerWithUserLanguage(User user) { if (User.frenchLanguageCode.equals(user.language()) ) { setLanguage("French"); shortDateFormatter = new ERXJodaLocalDateFormatter("d MMMM", Locale.CANADA_FRENCH, null); } if (User.englishLanguageCode.equals(user.language()) ) { setLanguage("English"); shortDateFormatter = new ERXJodaLocalDateFormatter("MMMM d", Locale.CANADA, null);
The logon method
public WOActionResults logon() { // Fetch the User with your custom attributes. User user = User.fetchUser(session().defaultEditingContext(), User.USERNAME.eq(username()));! if (user != null && password != null) { // Use the PasswordHasher to verify the password and upgrade it to new hasher if required. if (user.someAthenticationMethod(password)) { // Tell the UserAccessControl someone logged in and return the provided home page. return session().userAccessService().logonAsUser(user); } } displayError = true; return null; }
ComponentAccessService
• This object read the component classes annotation.
• It is used in the checkAccess() method of your ERXComponents.
BaseComponent.java
@Override protected void checkAccess() throws SecurityException { if (context().page().equals(this)) { ComponentAccessService accessService = ComponentAccessService.getInstance(); if ( accessService.isComponentAccessibleForUserProfile(getClass(), currentUserProfile()) == false) { throw new SecurityException("Component "+getClass().getSimpleName()+" require one of these roles "+ accessService.readAllowedForRolesAnnotationInClass(getClass())+ " current user "+currentUser()+" have these "+currentUserProfile().allEffectiveRoles()); } } super.checkAccess(); }! public KAUserProfile currentUserProfile() { return session().userAccessService().currentUserProfile(); }! public User currentUser() { return session().userAccessService().currentUser(); } public boolean isUserAdmin() { String currentUserProfileCode = session().userAccessService().currentUserProfile().profileCode(); return currentUserProfileCode.equals(Profile.Admin); }
Using PasswordHasher
• Currently the framework contains Pbkdf2Hasher and LatinSimpleMD5Hasher.
• You can add your Hasher by creating a subclass of PasswordHasher.
• Register knows hasher(s) during application init.
• Saved hash contains a hasher identifier params and salt.
• Set the default hasher to use when changing a user password with KAUser.setCurrentPasswordHasher(hasher).
Using legacy password hashes
• Register a default hasher to use with incomplete hashes using KAUser.setDefaultPasswordHasher(hasher).
• The default hasher is used when the current hash for user does not contains an hasher code.
• authenticateWithPasswordAndUpgradeHashIfRequired(password) will upgrade the hash with the current hasher if the password is verified.
• This method will upgrade any verified hash to the current hasher.
Next features ?
• New password hashers.
• Support for LDAP authentication.
• UI to edit the roles.plist file.
• Adjustments for WOInject.
Q&ASamuel Pelletier Kaviju inc. [email protected] https://github.com/spelletier