keycloak-uncached

KEYCLOAK-1070 Improve Applications page and add available

4/24/2015 3:57:20 AM

Details

diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
index ab1dcad..1ce00c2 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -20,7 +20,7 @@ import javax.ws.rs.core.UriInfo;
 import org.jboss.logging.Logger;
 import org.keycloak.account.AccountPages;
 import org.keycloak.account.AccountProvider;
-import org.keycloak.account.freemarker.model.ConsentBean;
+import org.keycloak.account.freemarker.model.ApplicationsBean;
 import org.keycloak.account.freemarker.model.AccountBean;
 import org.keycloak.account.freemarker.model.AccountFederatedIdentityBean;
 import org.keycloak.account.freemarker.model.FeaturesBean;
@@ -186,7 +186,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
                 attributes.put("sessions", new SessionsBean(realm, sessions));
                 break;
             case APPLICATIONS:
-                attributes.put("consent", new ConsentBean(user));
+                attributes.put("applications", new ApplicationsBean(realm, user));
                 attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
                 break;
             case PASSWORD:
diff --git a/forms/common-themes/src/main/resources/theme/base/account/applications.ftl b/forms/common-themes/src/main/resources/theme/base/account/applications.ftl
index 9b5cf88..7442c49 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/applications.ftl
+++ b/forms/common-themes/src/main/resources/theme/base/account/applications.ftl
@@ -13,42 +13,71 @@
         <table class="table table-striped table-bordered">
             <thead>
               <tr>
-                <td>${msg("client")}</td>
-                <td>${msg("grantedPersonalInfo")}</td>
+                <td>${msg("application")}</td>
+                <td>${msg("availablePermissions")}</td>
                 <td>${msg("grantedPermissions")}</td>
+                <td>${msg("grantedPersonalInfo")}</td>
                 <td>${msg("action")}</td>
               </tr>
             </thead>
 
             <tbody>
-              <#list consent.clientGrants as clientGrant>
+              <#list applications.applications as application>
                 <tr>
                     <td>
-                        <#if clientGrant.client.baseUrl??><a href="${clientGrant.client.baseUrl}"></#if>
-                            <#if clientGrant.client.name??>${advancedMsg(clientGrant.client.name)}<#else>${clientGrant.client.clientId}</#if>
-                        <#if clientGrant.client.baseUrl??></a></#if>
-                    </td>
-                    <td>
-                        <#list clientGrant.claimsGranted as claim>
-                            ${advancedMsg(claim)}<#if claim_has_next>, </#if>
-                        </#list>
+                        <#if application.client.baseUrl??><a href="${application.client.baseUrl}"></#if>
+                            <#if application.client.name??>${advancedMsg(application.client.name)}<#else>${application.client.clientId}</#if>
+                        <#if application.client.baseUrl??></a></#if>
                     </td>
+
                     <td>
-                        <#list clientGrant.realmRolesGranted as role>
+                        <#list application.realmRolesAvailable as role>
                             <#if role.description??>${advancedMsg(role.description)}<#else>${advancedMsg(role.name)}</#if>
                             <#if role_has_next>, </#if>
                         </#list>
-                        <#list clientGrant.resourceRolesGranted?keys as resource>
-                            <#if clientGrant.realmRolesGranted?has_content>, </#if>
-                            <#list clientGrant.resourceRolesGranted[resource] as clientRole>
+                        <#list application.resourceRolesAvailable?keys as resource>
+                            <#if application.realmRolesAvailable?has_content>, </#if>
+                            <#list application.resourceRolesAvailable[resource] as clientRole>
                                 <#if clientRole.roleDescription??>${advancedMsg(clientRole.roleDescription)}<#else>${advancedMsg(clientRole.roleName)}</#if>
                                 ${msg("inResource")} <strong><#if clientRole.clientName??>${advancedMsg(clientRole.clientName)}<#else>${clientRole.clientId}</#if></strong>
                                 <#if clientRole_has_next>, </#if>
                             </#list>
                         </#list>
                     </td>
+
+                    <td>
+                        <#if application.client.consentRequired>
+                            <#list application.realmRolesGranted as role>
+                                <#if role.description??>${advancedMsg(role.description)}<#else>${advancedMsg(role.name)}</#if>
+                                <#if role_has_next>, </#if>
+                            </#list>
+                            <#list application.resourceRolesGranted?keys as resource>
+                                <#if application.realmRolesGranted?has_content>, </#if>
+                                <#list application.resourceRolesGranted[resource] as clientRole>
+                                    <#if clientRole.roleDescription??>${advancedMsg(clientRole.roleDescription)}<#else>${advancedMsg(clientRole.roleName)}</#if>
+                                    ${msg("inResource")} <strong><#if clientRole.clientName??>${advancedMsg(clientRole.clientName)}<#else>${clientRole.clientId}</#if></strong>
+                                    <#if clientRole_has_next>, </#if>
+                                </#list>
+                            </#list>
+                        <#else>
+                            <strong>${msg("fullAccess")}</strong>
+                        </#if>
+                    </td>
+
+                    <td>
+                        <#if application.client.consentRequired>
+                            <#list application.claimsGranted as claim>
+                                ${advancedMsg(claim)}<#if claim_has_next>, </#if>
+                            </#list>
+                        <#else>
+                            <strong>${msg("fullAccess")}</strong>
+                        </#if>
+                    </td>
+
                     <td>
-                        <button type='submit' class='btn btn-primary' id='revoke-${clientGrant.client.clientId}' name='clientId' value="${clientGrant.client.id}">${msg("revoke")}</button>
+                        <#if application.client.consentRequired>
+                            <button type='submit' class='btn btn-primary' id='revoke-${application.client.clientId}' name='clientId' value="${application.client.id}">${msg("revoke")}</button>
+                        </#if>
                     </td>
                 </tr>
               </#list>
diff --git a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
index a2d99fe..9e20adf 100755
--- a/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/forms/common-themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -12,7 +12,7 @@ changePasswordHtmlTitle=Change Password
 sessionsHtmlTitle=Sessions
 accountManagementTitle=Keycloak Account Management
 authenticatorTitle=Authenticator
-applicationsHtmlTitle=Manage Granted Permissions
+applicationsHtmlTitle=Available Applications
 
 authenticatorCode=One-time code
 email=Email
@@ -32,7 +32,7 @@ region=State, Province, or Region
 postal_code=Zip or Postal code
 country=Country
 emailVerified=Email verified
-gssDelegationCredential=gss delegation credential
+gssDelegationCredential=GSS Delegation Credential
 
 role_admin=Admin
 role_realm-admin=Realm Admin
@@ -50,6 +50,7 @@ role_manage-identity-providers=Manage identity providers
 role_manage-clients=Manage clients
 role_manage-events=Manage events
 role_view-profile=View profile
+role_manage-account=Manage account
 client_account=Account
 client_security-admin-console=Security Admin Console
 client_realm-management=Realm Management
@@ -78,10 +79,13 @@ authenticator=Authenticator
 sessions=Sessions
 log=Log
 
-grantedPersonalInfo=Granted Personal Info
+application=Application
+availablePermissions=Available Permissions
 grantedPermissions=Granted Permissions
+grantedPersonalInfo=Granted Personal Info
 action=Action
 inResource=in
+fullAccess=Full Access
 revoke=Revoke Grant
 
 configureAuthenticators=Configured Authenticators
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index ed5ae9e..6761920 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -76,10 +76,10 @@ public interface UserModel {
     void setFederationLink(String link);
 
     void addConsent(UserConsentModel consent);
-    UserConsentModel getConsentByClient(String clientId);
+    UserConsentModel getConsentByClient(String clientInternalId);
     List<UserConsentModel> getConsents();
     void updateConsent(UserConsentModel consent);
-    boolean revokeConsentForClient(String clientId);
+    boolean revokeConsentForClient(String clientInternalId);
 
     public static enum RequiredAction {
         VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 83bc85c..39d647d 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -370,7 +370,7 @@ public class MongoUserProvider implements UserProvider {
                 .and("grantedProtocolMappers").is(protocolMapper.getId())
                 .get();
         DBObject pull = new BasicDBObject("$pull", query);
-        getMongoStore().updateEntities(MongoUserEntity.class, query, pull, invocationContext);
+        getMongoStore().updateEntities(MongoUserConsentEntity.class, query, pull, invocationContext);
     }
 
     @Override
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 8ba95d2..7672df1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -28,6 +28,7 @@ import org.junit.BeforeClass;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
+import org.keycloak.account.freemarker.model.ApplicationsBean;
 import org.keycloak.events.Details;
 import org.keycloak.events.Event;
 import org.keycloak.events.EventType;
@@ -43,6 +44,7 @@ import org.keycloak.services.resources.AccountService;
 import org.keycloak.services.resources.RealmsResource;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AccountApplicationsPage;
 import org.keycloak.testsuite.pages.AccountLogPage;
 import org.keycloak.testsuite.pages.AccountPasswordPage;
 import org.keycloak.testsuite.pages.AccountSessionsPage;
@@ -63,6 +65,7 @@ import org.openqa.selenium.WebDriver;
 import javax.ws.rs.core.UriBuilder;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -130,6 +133,9 @@ public class AccountTest {
     protected AccountSessionsPage sessionsPage;
 
     @WebResource
+    protected AccountApplicationsPage applicationsPage;
+
+    @WebResource
     protected ErrorPage errorPage;
 
     private TimeBasedOTP totp = new TimeBasedOTP();
@@ -517,4 +523,38 @@ public class AccountTest {
         }
     }
 
+    // More tests (including revoke) are in OAuthGrantTest
+    @Test
+    public void applications() {
+        applicationsPage.open();
+        loginPage.login("test-user@localhost", "password");
+
+        events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=applications").assertEvent();
+        Assert.assertTrue(applicationsPage.isCurrent());
+
+        Map<String, AccountApplicationsPage.AppEntry> apps = applicationsPage.getApplications();
+        Assert.assertEquals(3, apps.size());
+
+        AccountApplicationsPage.AppEntry accountEntry = apps.get("Account");
+        Assert.assertEquals(2, accountEntry.getRolesAvailable().size());
+        Assert.assertTrue(accountEntry.getRolesAvailable().contains("Manage account in Account"));
+        Assert.assertTrue(accountEntry.getRolesAvailable().contains("View profile in Account"));
+        Assert.assertEquals(1, accountEntry.getRolesGranted().size());
+        Assert.assertTrue(accountEntry.getRolesGranted().contains("Full Access"));
+        Assert.assertEquals(1, accountEntry.getProtocolMappersGranted().size());
+        Assert.assertTrue(accountEntry.getProtocolMappersGranted().contains("Full Access"));
+
+        AccountApplicationsPage.AppEntry testAppEntry = apps.get("test-app");
+        Assert.assertEquals(4, testAppEntry.getRolesAvailable().size());
+        Assert.assertTrue(testAppEntry.getRolesGranted().contains("Full Access"));
+        Assert.assertTrue(testAppEntry.getProtocolMappersGranted().contains("Full Access"));
+
+        AccountApplicationsPage.AppEntry thirdPartyEntry = apps.get("third-party");
+        Assert.assertEquals(2, thirdPartyEntry.getRolesAvailable().size());
+        Assert.assertTrue(thirdPartyEntry.getRolesAvailable().contains("Have User privileges"));
+        Assert.assertTrue(thirdPartyEntry.getRolesAvailable().contains("Have Customer User privileges in test-app"));
+        Assert.assertEquals(0, thirdPartyEntry.getRolesGranted().size());
+        Assert.assertEquals(0, thirdPartyEntry.getProtocolMappersGranted().size());
+    }
+
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java
index b40cda7..ede7d31 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/ApplicationServlet.java
@@ -52,7 +52,7 @@ public class ApplicationServlet extends HttpServlet {
         PrintWriter pw = resp.getWriter();
         pw.printf("<html><head><title>%s</title></head><body>", title);
         UriBuilder base = UriBuilder.fromUri("http://localhost:8081/auth");
-        pw.printf(LINK, RealmsResource.accountUrl(base), "account", "account");
+        pw.printf(LINK, RealmsResource.accountUrl(base).build("test"), "account", "account");
 
         pw.print("</body></html>");
         pw.flush();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
index 6985e88..939231f 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthGrantTest.java
@@ -26,11 +26,24 @@ import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
+import org.keycloak.constants.KerberosConstants;
 import org.keycloak.events.Details;
 import org.keycloak.events.Event;
+import org.keycloak.events.EventType;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
+import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
 import org.keycloak.representations.AccessToken;
+import org.keycloak.services.managers.RealmManager;
 import org.keycloak.testsuite.AssertEvents;
 import org.keycloak.testsuite.OAuthClient;
+import org.keycloak.testsuite.pages.AccountApplicationsPage;
+import org.keycloak.testsuite.pages.AppPage;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.pages.OAuthGrantPage;
 import org.keycloak.testsuite.rule.KeycloakRule;
@@ -69,11 +82,17 @@ public class OAuthGrantTest {
     @WebResource
     protected OAuthGrantPage grantPage;
 
+    @WebResource
+    protected AccountApplicationsPage accountAppsPage;
+
+    @WebResource
+    protected AppPage appPage;
+
     private static String ROLE_USER = "Have User privileges";
     private static String ROLE_CUSTOMER = "Have Customer User privileges";
 
     @Test
-    public void oauthGrantAcceptTest() throws IOException {
+    public void oauthGrantAcceptTest() {
         oauth.clientId("third-party");
         oauth.doLoginGrant("test-user@localhost", "password");
 
@@ -106,10 +125,16 @@ public class OAuthGrantTest {
         Assert.assertTrue(resourceAccess.get("test-app").isUserInRole("customer-user"));
 
         events.expectCodeToToken(codeId, loginEvent.getSessionId()).client("third-party").assertEvent();
+
+        accountAppsPage.open();
+        accountAppsPage.revokeGrant("third-party");
+
+        events.expect(EventType.REVOKE_GRANT)
+                .client("account").detail(Details.REVOKED_CLIENT, "third-party").assertEvent();
     }
 
     @Test
-    public void oauthGrantCancelTest() throws IOException {
+    public void oauthGrantCancelTest() {
         oauth.clientId("third-party");
         oauth.doLoginGrant("test-user@localhost", "password");
 
@@ -125,4 +150,118 @@ public class OAuthGrantTest {
         events.expectLogin().client("third-party").error("rejected_by_user").assertEvent();
     }
 
+    @Test
+    public void oauthGrantNotShownWhenAlreadyGranted() {
+        // Grant permissions on grant screen
+        oauth.clientId("third-party");
+        oauth.doLoginGrant("test-user@localhost", "password");
+
+        grantPage.assertCurrent();
+        grantPage.accept();
+
+        events.expectLogin().client("third-party").assertEvent();
+
+        // Assert permissions granted on Account mgmt. applications page
+        accountAppsPage.open();
+        AccountApplicationsPage.AppEntry thirdPartyEntry = accountAppsPage.getApplications().get("third-party");
+        Assert.assertTrue(thirdPartyEntry.getRolesGranted().contains(ROLE_USER));
+        Assert.assertTrue(thirdPartyEntry.getRolesGranted().contains("Have Customer User privileges in test-app"));
+        Assert.assertTrue(thirdPartyEntry.getProtocolMappersGranted().contains("Full name"));
+        Assert.assertTrue(thirdPartyEntry.getProtocolMappersGranted().contains("Email"));
+
+        // Open login form and assert grantPage not shown
+        oauth.openLoginForm();
+        appPage.assertCurrent();
+        events.expectLogin().detail(Details.AUTH_METHOD, "sso").removeDetail(Details.USERNAME).client("third-party").assertEvent();
+
+        // Revoke grant in account mgmt.
+        accountAppsPage.open();
+        accountAppsPage.revokeGrant("third-party");
+
+        events.expect(EventType.REVOKE_GRANT)
+                .client("account").detail(Details.REVOKED_CLIENT, "third-party").assertEvent();
+
+        // Open login form again and assert grant Page is shown
+        oauth.openLoginForm();
+        grantPage.assertCurrent();
+        Assert.assertTrue(driver.getPageSource().contains(ROLE_USER));
+        Assert.assertTrue(driver.getPageSource().contains(ROLE_CUSTOMER));
+    }
+
+    @Test
+    public void oauthGrantAddAnotherRoleAndMapper() {
+        // Grant permissions on grant screen
+        oauth.clientId("third-party");
+        oauth.doLoginGrant("test-user@localhost", "password");
+
+        // Add new protocolMapper and role before showing grant page
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                ProtocolMapperModel protocolMapper = UserSessionNoteMapper.createClaimMapper(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME,
+                        KerberosConstants.GSS_DELEGATION_CREDENTIAL,
+                        KerberosConstants.GSS_DELEGATION_CREDENTIAL, "String",
+                        true, KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME,
+                        true, false);
+
+                ClientModel thirdPartyApp = appRealm.getClientByClientId("third-party");
+                thirdPartyApp.addProtocolMapper(protocolMapper);
+
+                RoleModel newRole = appRealm.addRole("new-role");
+                thirdPartyApp.addScopeMapping(newRole);
+                UserModel testUser = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
+                testUser.grantRole(newRole);
+            }
+
+        });
+
+        // Confirm grant page
+        grantPage.assertCurrent();
+        grantPage.accept();
+        events.expectLogin().client("third-party").assertEvent();
+
+        // Assert new role and protocol mapper not in account mgmt.
+        accountAppsPage.open();
+        AccountApplicationsPage.AppEntry appEntry = accountAppsPage.getApplications().get("third-party");
+        Assert.assertFalse(appEntry.getRolesGranted().contains("new-role"));
+        Assert.assertFalse(appEntry.getProtocolMappersGranted().contains(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME));
+
+        // Show grant page another time. Just new role and protocol mapper are on the page
+        oauth.openLoginForm();
+        grantPage.assertCurrent();
+        Assert.assertFalse(driver.getPageSource().contains(ROLE_USER));
+        Assert.assertFalse(driver.getPageSource().contains("Full name"));
+        Assert.assertTrue(driver.getPageSource().contains("new-role"));
+        Assert.assertTrue(driver.getPageSource().contains(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME));
+        grantPage.accept();
+        events.expectLogin().client("third-party").assertEvent();
+
+        // Go to account mgmt. Everything is granted now
+        accountAppsPage.open();
+        appEntry = accountAppsPage.getApplications().get("third-party");
+        Assert.assertTrue(appEntry.getRolesGranted().contains("new-role"));
+        Assert.assertTrue(appEntry.getProtocolMappersGranted().contains(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME));
+
+        // Revoke
+        accountAppsPage.revokeGrant("third-party");
+        events.expect(EventType.REVOKE_GRANT)
+                .client("account").detail(Details.REVOKED_CLIENT, "third-party").assertEvent();
+
+        // Cleanup
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                ClientModel thirdPartyApp = appRealm.getClientByClientId("third-party");
+                ProtocolMapperModel gssMapper = thirdPartyApp.getProtocolMapperByName(OIDCLoginProtocol.LOGIN_PROTOCOL, KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME);
+                thirdPartyApp.removeProtocolMapper(gssMapper);
+
+                RoleModel newRole = appRealm.getRole("new-role");
+                appRealm.removeRole(newRole);
+            }
+
+        });
+    }
+
 }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java
index fd13b9c..f88db27 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/AccountApplicationsPage.java
@@ -37,62 +37,81 @@ public class AccountApplicationsPage extends AbstractAccountPage {
         driver.findElement(By.id("revoke-" + clientId)).click();
     }
 
-    public Map<String, ClientGrant> getClientGrants() {
-        Map<String, ClientGrant> table = new HashMap<String, ClientGrant>();
+    public Map<String, AppEntry> getApplications() {
+        Map<String, AppEntry> table = new HashMap<String, AppEntry>();
         for (WebElement r : driver.findElements(By.tagName("tr"))) {
             int count = 0;
-            ClientGrant currentGrant = null;
+            AppEntry currentEntry = null;
 
             for (WebElement col : r.findElements(By.tagName("td"))) {
                 count++;
                 switch (count) {
                     case 1:
-                        currentGrant = new ClientGrant();
-                        String clientId = col.getText();
-                        table.put(clientId, currentGrant);
+                        currentEntry = new AppEntry();
+                        String client = col.getText();
+                        table.put(client, currentEntry);
                         break;
                     case 2:
-                        String protMappersStr = col.getText();
-                        String[] protMappers = protMappersStr.split(",");
-                        for (String protMapper : protMappers) {
-                            protMapper = protMapper.trim();
-                            currentGrant.addMapper(protMapper);
+                        String rolesStr = col.getText();
+                        String[] roles = rolesStr.split(",");
+                        for (String role : roles) {
+                            role = role.trim();
+                            currentEntry.addAvailableRole(role);
                         }
                         break;
                     case 3:
-                        String rolesStr = col.getText();
-                        String[] roles = rolesStr.split(",");
+                        rolesStr = col.getText();
+                        if (rolesStr.isEmpty()) break;
+                        roles = rolesStr.split(",");
                         for (String role : roles) {
                             role = role.trim();
-                            currentGrant.addRole(role);
+                            currentEntry.addGrantedRole(role);
+                        }
+                        break;
+                    case 4:
+                        String protMappersStr = col.getText();
+                        if (protMappersStr.isEmpty()) break;
+                        String[] protMappers = protMappersStr.split(",");
+                        for (String protMapper : protMappers) {
+                            protMapper = protMapper.trim();
+                            currentEntry.addMapper(protMapper);
                         }
                         break;
                 }
             }
         }
-        table.remove("Client");
+        table.remove("Application");
         return table;
     }
 
-    public static class ClientGrant {
+    public static class AppEntry {
+
+        private final List<String> rolesAvailable = new ArrayList<String>();
+        private final List<String> rolesGranted = new ArrayList<String>();
+        private final List<String> protocolMappersGranted = new ArrayList<String>();
 
-        private final List<String> protocolMapperDescriptions = new ArrayList<String>();
-        private final List<String> roleDescriptions = new ArrayList<String>();
+        private void addAvailableRole(String role) {
+            rolesAvailable.add(role);
+        }
+
+        private void addGrantedRole(String role) {
+            rolesGranted.add(role);
+        }
 
         private void addMapper(String protocolMapper) {
-            protocolMapperDescriptions.add(protocolMapper);
+            protocolMappersGranted.add(protocolMapper);
         }
 
-        private void addRole(String role) {
-            roleDescriptions.add(role);
+        public List<String> getRolesGranted() {
+            return rolesGranted;
         }
 
-        public List<String> getProtocolMapperDescriptions() {
-            return protocolMapperDescriptions;
+        public List<String> getRolesAvailable() {
+            return rolesAvailable;
         }
 
-        public List<String> getRoleDescriptions() {
-            return roleDescriptions;
+        public List<String> getProtocolMappersGranted() {
+            return protocolMappersGranted;
         }
     }
 }