Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
Difference between revisions of "Scout/Tutorial/3.8/Minicrm/Permissions"
m (→Granting Permissions to Users) |
m (→Authentication) |
||
Line 366: | Line 366: | ||
== Authentication == | == Authentication == | ||
− | Before you continue: '''make sure | + | Before you continue: '''make sure administrator has the username "admin" and both roles'''. Make sure the Standard role has been assigned all permissions. Once we're done here you can lock yourself out of the application. If that happens, you will have to fix the permissions on the database. |
=== Identifying Users === | === Identifying Users === |
Revision as of 09:35, 24 October 2012
The Scout documentation has been moved to https://eclipsescout.github.io/.
Contents
What is this chapter about?
This chapter is about authorization and authentication.
When creating forms and table pages, the wizards have always created Permission classes in the background:
- CreateCompanyPermission
- ReadCompanyPermission
- UpdateCompanyPermission
- DeleteCompanyPermission (actually, you will have to create this permission yourself if you implement a delete menu)
We want to create an Administration View where Users get assigned Roles. These Roles have have Permissions. When a user logs in, the appropriate Permissions are loaded.
For this to work, users must be authenticated. We'll add a SecurityFilter to handle this.
This chapter assumes that you're pretty proficient at creating tables, forms and services. No more hand-holding. :)
Database
The sample database already contains the following tables:
ROLE ROLE_PERMISSION USER_ROLE --------- --------------- --------- ROLE_NR ROLE_NR USER_NR NAME PERMISSION_NAME ROLE_NR
The PERSON table has a USERNAME column.
Let us create two roles before we get started:
INSERT INTO minicrm.ROLE (role_nr, name) VALUES (1, 'Administrator'); INSERT INTO minicrm.ROLE (role_nr, name) VALUES (2, 'Standard');
Roles
Create a new Lookup Call RoleLookupCall.
The RoleLookupService uses this statement:
return "" + "SELECT ROLE_NR, " + " NAME " + "FROM ROLE " + "WHERE 1=1 " + "<key> AND ROLE_NR = :key </key> " + "<text> AND UPPER(NAME) LIKE UPPER(:text||'%') </text> " + "<all> </all> ";
Person Form
Create a Person Form and a Person Process Service to create and edit persons. Make sure you use the same class names if you want to copy and paste the SQL statements later in this section.
Label | Class Name | Column Name | Type |
---|---|---|---|
Name | NameField | LAST_NAME | String |
First Name | FirstNameField | FIRST_NAME | |
Employer | EmployerField | COMPANY_NR | SmartField (CompanyLookupCall) |
Username | UsernameField | USERNAME | String |
Roles | RolesField | USER_ROLE.ROLE_NR | Listview (RoleLookupCall, Grid H 4) |
Menus
Add a "New Person..." and a "Edit Person..." menu to the PersonTablePage.
@Override protected void execAction() throws ProcessingException { PersonForm form = new PersonForm(); form.startNew(); form.waitFor(); if (form.isFormStored()) { reloadPage(); } }
@Override public void execAction() throws ProcessingException { PersonForm form = new PersonForm(); form.setPersonNr(getPersonNrColumn().getSelectedValue()); form.startModify(); form.waitFor(); if (form.isFormStored()) { reloadPage(); } }
Process Service
Here are the SQL statements you will need.
Creation:
SQL.selectInto("" + "SELECT MAX(PERSON_NR)+1 " + "FROM PERSON " + "INTO :personNr" , formData); SQL.insert("" + "INSERT INTO PERSON (PERSON_NR, LAST_NAME, FIRST_NAME, COMPANY_NR, USERNAME) " + "VALUES (:personNr, :name, :firstName, :employer, :username)" , formData); SQL.insert("" + "INSERT INTO USER_ROLE (USER_NR, ROLE_NR) " + "VALUES (:personNr, :{roles})" , formData);
Loading:
SQL.selectInto("" + "SELECT LAST_NAME, " + " FIRST_NAME, " + " COMPANY_NR, " + " USERNAME " + "FROM PERSON " + "WHERE PERSON_NR = :personNr " + "INTO :name, " + " :firstName, " + " :employer, " + " :username" , formData); SQL.select("" + "SELECT ROLE_NR " + "FROM USER_ROLE " + "WHERE USER_NR = :personNr " + "INTO :roles" , formData);
Updating:
SQL.update("" + "UPDATE PERSON SET" + " LAST_NAME = :name, " + " FIRST_NAME = :firstName, " + " COMPANY_NR = :employer, " + " USERNAME = :username " + "WHERE PERSON_NR = :personNr" , formData); SQL.delete("" + "DELETE FROM USER_ROLE " + "WHERE USER_NR = :personNr " , formData); SQL.insert("" + "INSERT INTO USER_ROLE (USER_NR, ROLE_NR) " + "VALUES (:personNr, :{roles})" , formData);
Screenshot
If you're working in a multilingual environment, this is what it might look like:
Administration View
Create the following outline on the client side:
Administration Outline │ ├─Role Table Page │ │ │ └─Permission Table Page │ └─Permissions Table Page
Additional table pages might be useful: which roles use a particular permission? which users have a particular role? These are left as an exercise for the reader.
Permission Table Page and Outline Service
We'll be using this table in two places, thus we need a variable for the role (RoleNr).
If a role is provided, we need a service on the server side to provide these. Create a new outline service (AdministrationOutlineService) with an operation to get all the roles (getPermissionTableData) with a single argument of type Long (roleNr).
@Override public Object[][] getPermissionTableData(Long roleNr) throws ProcessingException { return SQL.select("" + "SELECT PERMISSION_NAME " + "FROM ROLE_PERMISSION " + "WHERE ROLE_NR = :roleNr " , new NVPair("roleNr", roleNr)); }
If no role is provided, we list all the permissions. These are all available on the client. We don't need service on the server side to fetch them. Thus, on the client side, things look a bit different:
@Override protected Object[][] execLoadTableData(SearchFilter filter) throws ProcessingException { if (getRoleNr() == null) { ArrayList<String> rows = new ArrayList<String>(); BundleClassDescriptor[] permissions = SERVICES.getService(IPermissionService.class).getAllPermissionClasses(); for (int i = 0; i < permissions.length; i++) { if (permissions[i].getBundleSymbolicName().contains("minicrm")) { rows.add(permissions[i].getSimpleClassName()); } else { // Skip bookmark permissions and other permissions that are not specific to our application } } Collections.sort(rows); Object[][] data = new Object[rows.size()][1]; for (int i = 0; i < rows.size(); i++) { data[i][0] = rows.get(i); } return data; } else { return SERVICES.getService(IAdministrationOutlineService.class).getPermissionTableData(getRoleNr()); } }
Don't forget to create a column for your table page!
This is what you'd like to see:
(Using the Swing client.)
Role Table Page and Outline Service
Create a new table page in the administration outline (RoleTablePage) with two columns (non-displayable RoleNr of type Long and a String column called Role).
If you want to change the order of the child tapes, edit the execCreateChildPages method of the AdministrationOutline.
Add a new service operation to the AdministrationOutlineService called getRoleTableData. This one is very simple:
@Override public Object[][] getRoleTableData() throws ProcessingException { return SQL.select("" + "SELECT ROLE_NR, NAME " + "FROM ROLE"); }
Use it on the execLoadTableData operation of the table page:
@Override protected Object[][] execLoadTableData(SearchFilter filter) throws ProcessingException { return SERVICES.getService(IAdministrationOutlineService.class).getRoleTableData(); }
Add the PermissionTablePage as a child to the RoleTablePage. Note how the SDK already guessed that you will want to pass the primary of the current row to the child page:
@Override protected IPage execCreateChildPage(ITableRow row) throws ProcessingException { PermissionTablePage childPage = new PermissionTablePage(); childPage.setRoleNr(getTable().getRoleNrColumn().getValue(row)); return childPage; }
If everything worked as intended, this is how it should look:
No data is visible because the permissions haven't been assigned to roles, yet. This will be our next task.
Assigning Permissions to Roles
We will create a menu which calls a tiny form to assign one or more permissions to a role. The form will contain nothing but a smart field with roles.
Let's start with the form.
Create a new form called AssignToRoleForm; do not create am Id (no AssignToRoleNr). On the second page of the wizard, get rid of the ModifyHandler, the ReadAssignToRolePermission and the UpdateAssignToRolePermission.
Add a smart field RoleField using LookupCall RoleLookupCall.
Add a variable of type String called Permission. Now do something which the SDK doesn't do for you: change the type of m_permission from String to String[] and change getPermission and setPermission to match.
Switch to the AssignToRoleProcessService and remove the load and store operations. (You may have to remove them from the interface IAssignToRoleProcessService as well.)
For the moment, there is nothing to do for the prepareCreate operation. For the create operation, use the following statement:
SQL.insert("" + "INSERT INTO ROLE_PERMISSION (ROLE_NR, PERMISSION_NAME) " + "VALUES (:role, :{permission})" , formData);
Switch to the PermissionTablePage and add a menu called AssignToRoleMenu. Have it start the AssignToRoleForm and call the NewHandler. Mark the menu as Multi Selection Action and change the execAction as follows:
@Override protected void execAction() throws ProcessingException { AssignToRoleForm form = new AssignToRoleForm(); form.setPermission(getTable().getPermissionColumn().getSelectedValues()); form.startNew(); form.waitFor(); if (form.isFormStored()) { reloadPage(); } }
Ideally, this is what it will look like:
Select all the permissions and assign them to the Standard role.
Here's what you should get:
Missing Pieces
Things you can add to practice:
- a multi-select menu to remove permissions from a role; the menu should call the new operation void remove (Long roleNr, String[] permission) on the process service and refresh the table page
- a form to create, modify and remove roles
Authentication
Before you continue: make sure administrator has the username "admin" and both roles. Make sure the Standard role has been assigned all permissions. Once we're done here you can lock yourself out of the application. If that happens, you will have to fix the permissions on the database.
Identifying Users
To get an idea of what goes on, find the ServerSession and change execLoadSession as follows:
@Override protected void execLoadSession() throws ProcessingException{ logger.warn("created a new session for "+getUserId()); }
When you start a new client, you'll see:
!MESSAGE eclipse.org.minicrm.server.ServerSession.execLoadSession(ServerSession.java:46) created a new session for anonymous
What user id? This is handled by security filters. Go to the config file of your server product (/eclipse.org.minicrm.server/products/development/config.ini
). You'll see that the AnonymousSecurityFilter is active. This is what provides the user id "anonymous".
Change the config.ini as follows:
### Servlet Filter Runtime Configuration org.eclipse.scout.http.servletfilter.security.BasicSecurityFilter#active=true org.eclipse.scout.http.servletfilter.security.BasicSecurityFilter#realm=minicrm Development org.eclipse.scout.http.servletfilter.security.BasicSecurityFilter#users=admin\=manager,allen\=allen,blake\=blake org.eclipse.scout.http.servletfilter.security.AnonymousSecurityFilter#active=false
Restart server and client.
You will be greeted with a login box! Provide one of the combinations from the config file. Username admin password manager for example.
The server log will show:
!MESSAGE eclipse.org.minicrm.server.ServerSession.execLoadSession(ServerSession.java:46) created a new session for admin
In order to deny access to unknown people, change execLoadSession as follows:
@Override protected void execLoadSession() throws ProcessingException { SQL.selectInto("" + "SELECT PERSON_NR " + "FROM PERSON " + "WHERE UPPER(USERNAME) = UPPER(:userId) " + "INTO :personNr "); if (getPersonNr() == null) { logger.error("attempted login by " + getUserId()); throw new ProcessingException("Unknown User: " + getUserId(), new SecurityException("access denied")); } logger.info("created a new session for " + getUserId()); }
Now you can attempt to login with the "valid" username/password combination of allen/allen and the system will reject you anyway.
(You'll note that username/password combinations are stored in the config file. We'll take a look at using LDAP further down.)
Granting Permissions to Users
Find the AccessControlService class and take a look at the execLoadPermissions method:
@Override protected Permissions execLoadPermissions() { Permissions permissions = new Permissions(); permissions.add(new RemoteServiceAccessPermission("*.shared.*", "*")); //TODO fill access control service permissions.add(new AllPermission()); return permissions; }
This grants every user all permissions. We need to replace this with something based on database. The following example even has a backdoor for the user with id 1!
@Override protected Permissions execLoadPermissions() { Permissions permissions = new Permissions(); // calling services is free permissions.add(new RemoteServiceAccessPermission("*.shared.*", "*")); // backdoor: the first user may do everything? if (ServerSession.get().getPersonNr().equals(1L)) { logger.warn("backdoor used: person nr 1 was granted all permissions"); permissions.add(new AllPermission()); } else { try { // get simple class names from the databse StringArrayHolder permission = new StringArrayHolder(); SQL.selectInto("" + "SELECT DISTINCT P.PERMISSION_NAME " + "FROM ROLE_PERMISSION P, USER_ROLE R " + "WHERE R.USER_NR = :personNr " + "AND R.ROLE_NR = P.ROLE_NR " + "INTO :permission ", new NVPair("permission", permission)); // create a map from simple class names to qualified class names HashMap<String, String> map = new HashMap<String, String>(); for (BundleClassDescriptor descriptor : SERVICES.getService(IPermissionService.class).getAllPermissionClasses()) { map.put(descriptor.getSimpleClassName(), descriptor.getClassName()); } // instantiate the real permissions and assign them for (String simpleClass : permission.getValue()) { try { permissions.add((Permission) Class.forName(map.get(simpleClass)).newInstance()); } catch (Exception e) { logger.error("cannot find permission " + simpleClass + ": " + e.getMessage()); } } } catch (ProcessingException e) { logger.error("cannot read permissions: " + e.getStackTrace()); } } return permissions; }
If you log in, you'll notice a strange warning on the server console:
... cannot find permission CreateVisitPermission: null
... cannot find permission ReadVisitPermission: null
... cannot find permission UpdateVisitPermission: null
If you investigate the demo database, you'll note data junk in ROLE_PERMISSION and USER_ROLE for the non-existent roles 5 to 13.
If you really want to clean it up:
DELETE FROM minicrm.role_permission WHERE role_nr > 2; DELETE FROM minicrm.user_role WHERE role_nr > 2;