keycloak-aplcache

Details

diff --git a/ui/src/main/java/org/keycloak/ui/example/Admin.java b/ui/src/main/java/org/keycloak/ui/example/Admin.java
index 603feae..c60b72d 100644
--- a/ui/src/main/java/org/keycloak/ui/example/Admin.java
+++ b/ui/src/main/java/org/keycloak/ui/example/Admin.java
@@ -1,12 +1,11 @@
 package org.keycloak.ui.example;
 
-
 import java.net.URI;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.UUID;
 
 import javax.ws.rs.ApplicationPath;
@@ -18,8 +17,10 @@ import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
 
 @ApplicationPath("ui/api")
 @Path("")
@@ -29,7 +30,27 @@ public class Admin extends javax.ws.rs.core.Application {
 
     private static Map<String, Realm> realms = new HashMap<String, Realm>();
 
-    private static Map<UserId, User> users = new HashMap<UserId, User>();
+    private static Map<String, List<User>> users = new HashMap<String, List<User>>();
+
+    private static Map<Id, List<String>> roles = new HashMap<Id, List<String>>();
+
+    static {
+        Realm realm = new Realm();
+        realm.setId(UUID.randomUUID().toString());
+        realm.setName("Test realm");
+        realm.setEnabled(true);
+        realm.setRoles(new String[] { "admin", "user" });
+        realms.put(realm.getId(), realm);
+
+        User user = new User();
+        user.setUserId("user");
+        user.setPassword("password");
+
+        List<User> l = new LinkedList<User>();
+        l.add(user);
+
+        users.put(realm.getId(), l);
+    }
 
     @DELETE
     @Path("applications/{id}")
@@ -41,7 +62,6 @@ public class Admin extends javax.ws.rs.core.Application {
     @Path("realms/{id}")
     public void deleteRealm(@PathParam("id") String id) {
         realms.remove(id);
-        users.remove(id);
     }
 
     @GET
@@ -65,7 +85,6 @@ public class Admin extends javax.ws.rs.core.Application {
         return realms.get(id);
     }
 
-
     @GET
     @Path("realms")
     @Produces(MediaType.APPLICATION_JSON)
@@ -97,6 +116,11 @@ public class Admin extends javax.ws.rs.core.Application {
     @Path("applications/{id}")
     @Consumes(MediaType.APPLICATION_JSON)
     public void save(@PathParam("id") String id, Application application) {
+        try {
+            deleteApplication(id);
+        } catch (WebApplicationException e) {
+        }
+
         applications.put(id, application);
     }
 
@@ -104,6 +128,11 @@ public class Admin extends javax.ws.rs.core.Application {
     @Path("realms/{id}")
     @Consumes(MediaType.APPLICATION_JSON)
     public void save(@PathParam("id") String id, Realm realm) {
+        try {
+            deleteRealm(id);
+        } catch (WebApplicationException e) {
+        }
+
         realms.put(id, realm);
     }
 
@@ -111,33 +140,91 @@ public class Admin extends javax.ws.rs.core.Application {
     @Path("realms/{realm}/users/{id}")
     @Produces(MediaType.APPLICATION_JSON)
     public User getUser(@PathParam("realm") String realm, @PathParam("id") String id) {
-        return users.get(new UserId(realm, id));
+        for (User u : getUsers(realm)) {
+            if (u.getUserId().equals(id)) {
+                return u;
+            }
+        }
+        throw new WebApplicationException(Status.NOT_FOUND);
     }
 
     @GET
     @Path("realms/{realm}/users")
     @Produces(MediaType.APPLICATION_JSON)
     public List<User> getUsers(@PathParam("realm") String realm) {
-        LinkedList<User> list = new LinkedList<User>();
-        for (Entry<UserId, User> e : users.entrySet()) {
-            if (e.getKey().getRealm().equals(realm)) {
-                list.add(e.getValue());
-            }
+        List<User> l = users.get(realm);
+        if (l == null) {
+            l = new LinkedList<User>();
+            users.put(realm, l);
         }
-        return list;
+        return l;
     }
 
     @PUT
     @Path("realms/{realm}/users/{id}")
     @Consumes(MediaType.APPLICATION_JSON)
     public void save(@PathParam("realm") String realm, @PathParam("id") String id, User user) {
-        users.put(new UserId(realm, id), user);
+        try {
+            deleteUser(realm, id);
+        } catch (WebApplicationException e) {
+        }
+
+        getUsers(realm).add(user);
     }
 
     @DELETE
     @Path("realms/{realm}/users/{id}")
     public void deleteUser(@PathParam("realm") String realm, @PathParam("id") String id) {
-        users.remove(new UserId(realm, id));
+        getUsers(realm).remove(getUser(realm, id));
     }
 
+    @GET
+    @Path("roles/{realm}/{role}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<User> getRoleMapping(@PathParam("realm") String realm, @PathParam("role") String role) {
+        List<String> ids = getRoleMapping(new Id(realm, role));
+
+        List<User> users = new LinkedList<User>();
+        List<User> realmUsers = getUsers(realm);
+        for (String id : ids) {
+            for (User u : realmUsers) {
+                if (u.getUserId().equals(id)) {
+                    users.add(u);
+                }
+            }
+        }
+
+        return users;
+    }
+
+    private List<String> getRoleMapping(Id id) {
+        List<String> l = roles.get(id);
+        if (l == null) {
+            l = new LinkedList<String>();
+            roles.put(id, l);
+        }
+        return l;
+    }
+
+    @PUT
+    @Path("roles/{realm}/{role}/{user}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public void addRoleMapping(@PathParam("realm") String realm, @PathParam("role") String role,
+            @PathParam("user") String user, User u) {
+        getRoleMapping(new Id(realm, role)).add(user);
+    }
+
+    @DELETE
+    @Path("roles/{realm}/{role}/{user}")
+    public void deleteRoleMapping(@PathParam("realm") String realm, @PathParam("role") String role,
+            @PathParam("user") String user) {
+        Iterator<String> itr = getRoleMapping(new Id(realm, role)).iterator();
+        while (itr.hasNext()) {
+            if (itr.next().equals(user)) {
+                itr.remove();
+                return;
+            }
+        }
+        throw new WebApplicationException(Status.NOT_FOUND);
+    }
 }
\ No newline at end of file
diff --git a/ui/src/main/resources/META-INF/resources/ui/index.html b/ui/src/main/resources/META-INF/resources/ui/index.html
index 49a3ce9..80579c5 100644
--- a/ui/src/main/resources/META-INF/resources/ui/index.html
+++ b/ui/src/main/resources/META-INF/resources/ui/index.html
@@ -20,6 +20,7 @@
 
 <script src="js/app.js"></script>
 <script src="js/controllers.js"></script>
+<script src="js/loaders.js"></script>
 <script src="js/services.js"></script>
 </head>
 
diff --git a/ui/src/main/resources/META-INF/resources/ui/js/app.js b/ui/src/main/resources/META-INF/resources/ui/js/app.js
index 08691e5..8e5a3c0 100644
--- a/ui/src/main/resources/META-INF/resources/ui/js/app.js
+++ b/ui/src/main/resources/META-INF/resources/ui/js/app.js
@@ -1,40 +1,29 @@
 'use strict';
 
-var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.controllers', 'ui.bootstrap' ]);
+var module = angular.module('keycloak', [ 'keycloak.services', 'keycloak.loaders', 'keycloak.controllers', 'ui.bootstrap' ]);
 var resourceRequests = 0;
 
 module.config([ '$routeProvider', function($routeProvider) {
+	
 	$routeProvider.when('/create/application', {
 		templateUrl : 'partials/application-detail.html',
 		resolve : {
-			applications : function(ApplicationListLoader) {
-				return ApplicationListLoader();
-			},
 			application : function(ApplicationLoader) {
 				return {};
 			},
 			realms : function(RealmListLoader) {
 				return RealmListLoader();
-			},
-			providers : function(ProviderListLoader) {
-				return ProviderListLoader();
 			}
 		},
 		controller : 'ApplicationDetailCtrl'
 	}).when('/applications/:application', {
 		templateUrl : 'partials/application-detail.html',
 		resolve : {
-			applications : function(ApplicationListLoader) {
-				return ApplicationListLoader();
-			},
 			application : function(ApplicationLoader) {
 				return ApplicationLoader();
 			},
 			realms : function(RealmListLoader) {
 				return RealmListLoader();
-			},
-			providers : function(ProviderListLoader) {
-				return ProviderListLoader();
 			}
 		},
 		controller : 'ApplicationDetailCtrl'
@@ -46,16 +35,36 @@ module.config([ '$routeProvider', function($routeProvider) {
 			}
 		},
 		controller : 'ApplicationListCtrl'
-	}).when('/create/user/:realm', {
+	})
+	
+	.when('/create/realm', {
+		templateUrl : 'partials/realm-detail.html',
+		resolve : {
+			realm : function(RealmLoader) {
+				return {};
+			}
+		},
+		controller : 'RealmDetailCtrl'
+	}).when('/realms/:realm', {
+		templateUrl : 'partials/realm-detail.html',
+		resolve : {
+			realm : function(RealmLoader) {
+				return RealmLoader();
+			}
+		},
+		controller : 'RealmDetailCtrl'
+	}).when('/realms', {
+		templateUrl : 'partials/realm-list.html',
+		controller : 'RealmListCtrl'
+	})
+	
+	.when('/create/user/:realm', {
 		templateUrl : 'partials/user-detail.html',
 		resolve : {
-			realms : function(RealmListLoader) {
-				return RealmListLoader();
-			},
 			realm : function(RealmLoader) {
 				return RealmLoader();
 			},
-			user : function(UserLoader) {
+			user : function() {
 				return {};
 			}
 		},
@@ -63,9 +72,6 @@ module.config([ '$routeProvider', function($routeProvider) {
 	}).when('/realms/:realm/users/:user', {
 		templateUrl : 'partials/user-detail.html',
 		resolve : {
-			realms : function(RealmListLoader) {
-				return RealmListLoader();
-			},
 			realm : function(RealmLoader) {
 				return RealmLoader();
 			},
@@ -77,9 +83,6 @@ module.config([ '$routeProvider', function($routeProvider) {
 	}).when('/realms/:realm/users', {
 		templateUrl : 'partials/user-list.html',
 		resolve : {
-			realms : function(RealmListLoader) {
-				return RealmListLoader();
-			},
 			realm : function(RealmLoader) {
 				return RealmLoader();
 			},
@@ -88,39 +91,19 @@ module.config([ '$routeProvider', function($routeProvider) {
 			}
 		},
 		controller : 'UserListCtrl'
-	}).when('/create/realm', {
-		templateUrl : 'partials/realm-detail.html',
-		resolve : {
-			realms : function(RealmListLoader) {
-				return RealmListLoader();
-			},
-			realm : function(RealmLoader) {
-				return {};
-			}
-		},
-		controller : 'RealmDetailCtrl'
-	}).when('/realms/:realm', {
-		templateUrl : 'partials/realm-detail.html',
-		resolve : {
-			realms : function(RealmListLoader) {
-				return RealmListLoader();
-			},
-			realm : function(RealmLoader) {
-				return RealmLoader();
-			}
-		},
-		controller : 'RealmDetailCtrl'
-	}).when('/realms/:realm/roles', {
+	})
+	
+	.when('/realms/:realm/roles', {
 		templateUrl : 'partials/role-mapping.html',
 		resolve : {
-			realms : function(RealmListLoader) {
-				return RealmListLoader();
-			},
 			realm : function(RealmLoader) {
 				return RealmLoader();
 			},
-			users : function(UserListLoader) {
-				return UserListLoader();
+			application : function() {
+				return null;
+			},
+			users : function() {
+				return null;
 			},
 			role : function() {
 				return null;
@@ -130,29 +113,53 @@ module.config([ '$routeProvider', function($routeProvider) {
 	}).when('/realms/:realm/roles/:role', {
 		templateUrl : 'partials/role-mapping.html',
 		resolve : {
-			realms : function(RealmListLoader) {
-				return RealmListLoader();
-			},
 			realm : function(RealmLoader) {
 				return RealmLoader();
 			},
-			users : function(UserListLoader) {
-				return UserListLoader();
+			application : function() {
+				return null;
 			},
 			role : function($route) {
 				return $route.current.params.role;
+			},
+			users : function(RoleMappingLoader) {
+				return RoleMappingLoader();
 			}
 		},
 		controller : 'RoleMappingCtrl'
-	}).when('/realms', {
-		templateUrl : 'partials/realm-list.html',
+	})
+	
+	.when('/applications/:application/roles', {
+		templateUrl : 'partials/role-mapping.html',
 		resolve : {
-			realms : function(RealmListLoader) {
-				return RealmListLoader();
+			realm : function(ApplicationLoader) {
+				return ApplicationLoader();
+			},
+			users : function() {
+				return null;
+			},
+			role : function() {
+				return null;
 			}
 		},
-		controller : 'RealmListCtrl'
-	}).otherwise({
+		controller : 'RoleMappingCtrl'
+	}).when('/applications/:application/roles/:role', {
+		templateUrl : 'partials/role-mapping.html',
+		resolve : {
+			realm : function(ApplicationLoader) {
+				return ApplicationLoader();
+			},
+			role : function($route) {
+				return $route.current.params.role;
+			},
+			users : function(RoleMappingLoader) {
+				return RoleMappingLoader();
+			}
+		},
+		controller : 'RoleMappingCtrl'
+	})
+	
+	.otherwise({
 		templateUrl : 'partials/home.html'
 	});
 } ]);
@@ -250,17 +257,34 @@ module.directive('kcEnter', function() {
 });
 
 module.filter('remove', function() {
-	return function(input, remove) {
+	return function(input, remove, attribute) {
 		if (!input || !remove) {
 			return input;
 		}
-		
+
 		var out = [];
-		for (var i = 0; i < input.length; i++) {
-			if (remove.indexOf(input[i]) == -1) {
-				out.push(input[i]);
+		for ( var i = 0; i < input.length; i++) {
+			var e = input[i];
+
+			for (var j = 0; j < remove.length; j++) {
+				if (attribute) {
+					if (remove[j][attribute] == e[attribute]) {
+						e = null;
+						break;
+					}
+				} else {
+					if (remove[j] == e) {
+						e = null;
+						break;
+					}
+				}
+			}
+
+			if (e != null) {
+				out.push(e);
 			}
 		}
+
 		return out;
 	};
 });
\ No newline at end of file
diff --git a/ui/src/main/resources/META-INF/resources/ui/js/controllers.js b/ui/src/main/resources/META-INF/resources/ui/js/controllers.js
index f24797e..33938da 100644
--- a/ui/src/main/resources/META-INF/resources/ui/js/controllers.js
+++ b/ui/src/main/resources/META-INF/resources/ui/js/controllers.js
@@ -16,21 +16,16 @@ module.controller('GlobalCtrl', function($scope, Auth, $location, Notifications)
 	});
 });
 
-module.controller('ApplicationListCtrl', function($scope, applications) {
-	$scope.applications = applications;
+module.controller('ApplicationListCtrl', function($scope, Application) {
+	$scope.applications = Application.query();
 });
 
-module.controller('ApplicationDetailCtrl', function($scope, applications, application, Application, realms, providers, $location, $window, Dialog,
+module.controller('ApplicationDetailCtrl', function($scope, application, Application, realms, $location, $window, Dialog,
 		Notifications) {
 	$scope.application = angular.copy(application);
-	$scope.applications = applications;
 	$scope.realms = realms;
-	$scope.providers = providers;
-
-	$scope.callbackUrl = $window.location.origin + "/ejs-identity/api/callback/" + application.id;
 
 	$scope.create = !application.id;
-
 	$scope.changed = $scope.create;
 
 	$scope.$watch('application', function() {
@@ -66,7 +61,10 @@ module.controller('ApplicationDetailCtrl', function($scope, applications, applic
 			if (i > -1) {
 				$scope.application.roles.splice(i, 1);
 			}
-			$scope.removeInitialRole(role);
+			
+			if ($scope.application.initialRoles) {
+				$scope.removeInitialRole(role);
+			}
 		});
 	};
 
@@ -127,137 +125,14 @@ module.controller('ApplicationDetailCtrl', function($scope, applications, applic
 			});
 		});
 	};
-
-	$scope.availableProviders = [];
-
-	$scope.addProvider = function() {
-		if (!$scope.application.providers) {
-			$scope.application.providers = [];
-		}
-
-		$scope.application.providers.push({
-			"providerId" : $scope.newProviderId
-		});
-
-		$scope.newProviderId = null;
-	};
-
-	$scope.getProviderDescription = function(providerId) {
-		for ( var i = 0; i < $scope.providers.length; i++) {
-			if ($scope.providers[i].id == providerId) {
-				return $scope.providers[i];
-			}
-		}
-	};
-
-	$scope.removeProvider = function(i) {
-		$scope.application.providers.splice(i, 1);
-	};
-
-	var updateAvailableProviders = function() {
-		$scope.availableProviders.splice(0, $scope.availableProviders.length);
-
-		for ( var i in $scope.providers) {
-			var add = true;
-
-			for ( var j in $scope.application.providers) {
-				if ($scope.application.providers[j].providerId == $scope.providers[i].id) {
-					add = false;
-					break;
-				}
-			}
-
-			if (add) {
-				$scope.availableProviders.push($scope.providers[i]);
-			}
-		}
-	};
-
-	$scope.openHelp = function(i) {
-		$scope.providerHelpModal = true;
-		$scope.providerHelp = {};
-		$scope.providerHelp.index = i;
-		$scope.providerHelp.description = $scope.getProviderDescription($scope.application.providers[i].providerId);
-	};
-
-	$scope.closeHelp = function() {
-		$scope.providerHelpModal = false;
-		$scope.providerHelp = null;
-	};
-
-	$scope.$watch("providers.length + application.providers.length", updateAvailableProviders);
 });
 
-module.controller('RealmListCtrl', function($scope, realms) {
-	$scope.realms = realms;
-});
 
-module.controller('UserListCtrl', function($scope, realms, realm, users) {
-	$scope.realms = realms;
-	$scope.realm = realm;
-	$scope.users = users;
+module.controller('RealmListCtrl', function($scope, Realm) {
+	$scope.realms = Realm.query();
 });
 
-module.controller('UserDetailCtrl', function($scope, realms, realm, user, User, $location, Dialog, Notifications) {
-	$scope.realms = realms;
-	$scope.realm = realm;
-	$scope.user = angular.copy(user);
-	$scope.create = !user.userId;
-
-	$scope.changed = $scope.create;
-
-	$scope.$watch('user', function() {
-		if (!angular.equals($scope.user, user)) {
-			$scope.changed = true;
-		}
-	}, true);
-
-	$scope.save = function() {
-		if ($scope.userForm.$valid) {
-			User.save({
-				realm : realm.id,
-				id : $scope.user.userId
-			}, $scope.user, function() {
-				$scope.changed = false;
-				user = angular.copy($scope.user);
-
-				if ($scope.create) {
-					$location.url("/realms/" + realm.id + "/users/" + $scope.user.userId);
-					Notifications.success("Created user");
-				} else {
-					Notifications.success("Saved changes to user");
-				}
-			});
-		} else {
-			$scope.userForm.showErrors = true;
-		}
-	};
-
-	$scope.reset = function() {
-		$scope.user = angular.copy(user);
-		$scope.changed = false;
-		$scope.userForm.showErrors = false;
-	};
-
-	$scope.cancel = function() {
-		$location.url("/realms/" + realm.id + "/users");
-	};
-
-	$scope.remove = function() {
-		Dialog.confirmDelete($scope.user.userId, 'user', function() {
-			$scope.user.$remove({
-				realm : realm.id,
-				id : $scope.user.userId
-			}, function() {
-				$location.url("/realms/" + realm.id + "/users");
-				Notifications.success("Deleted user");
-			});
-		});
-	};
-});
-
-module.controller('RealmDetailCtrl', function($scope, Realm, realms, realm, $location, Dialog, Notifications) {
-	$scope.realms = realms;
+module.controller('RealmDetailCtrl', function($scope, Realm, realm, $location, Dialog, Notifications) {
 	$scope.realm = angular.copy(realm);
 	$scope.create = !realm.name;
 
@@ -296,7 +171,10 @@ module.controller('RealmDetailCtrl', function($scope, Realm, realms, realm, $loc
 			if (i > -1) {
 				$scope.realm.roles.splice(i, 1);
 			}
-			$scope.removeInitialRole(role);
+			
+			if ($scope.realm.initialRoles) {
+				$scope.removeInitialRole(role);
+			}
 		});
 	};
 
@@ -359,36 +237,111 @@ module.controller('RealmDetailCtrl', function($scope, Realm, realms, realm, $loc
 	};
 });
 
-module.controller('RoleMappingCtrl', function($scope, realms, realm, users, role, Notifications) {
-	$scope.realms = realms;
+
+module.controller('UserListCtrl', function($scope, realm, users) {
+	$scope.realm = realm;
+	$scope.users = users;
+});
+
+module.controller('UserDetailCtrl', function($scope, realm, user, User, $location, Dialog, Notifications) {
+	$scope.realm = realm;
+	$scope.user = angular.copy(user);
+	$scope.create = !user.userId;
+
+	$scope.changed = $scope.create;
+
+	$scope.$watch('user', function() {
+		if (!angular.equals($scope.user, user)) {
+			$scope.changed = true;
+		}
+	}, true);
+
+	$scope.save = function() {
+		if ($scope.userForm.$valid) {
+			User.save({
+				realm : realm.id,
+			}, $scope.user, function() {
+				$scope.changed = false;
+				user = angular.copy($scope.user);
+
+				if ($scope.create) {
+					$location.url("/realms/" + realm.id + "/users/" + $scope.user.userId);
+					Notifications.success("Created user");
+				} else {
+					Notifications.success("Saved changes to user");
+				}
+			});
+		} else {
+			$scope.userForm.showErrors = true;
+		}
+	};
+
+	$scope.reset = function() {
+		$scope.user = angular.copy(user);
+		$scope.changed = false;
+		$scope.userForm.showErrors = false;
+	};
+
+	$scope.cancel = function() {
+		$location.url("/realms/" + realm.id + "/users");
+	};
+
+	$scope.remove = function() {
+		Dialog.confirmDelete($scope.user.userId, 'user', function() {
+			$scope.user.$remove({
+				realm : realm.id,
+				userId : $scope.user.userId
+			}, function() {
+				$location.url("/realms/" + realm.id + "/users");
+				Notifications.success("Deleted user");
+			});
+		});
+	};
+});
+
+module.controller('RoleMappingCtrl', function($scope, realm, User, users, role, RoleMapping, Notifications) {
 	$scope.realm = realm;
-	$scope.allUsers = users;
-	$scope.users = [];
-	$scope.name = realm.name;
+	$scope.realmId = realm.realm || realm.id;
+	$scope.allUsers = User.query({ realm : $scope.realmId });
+	$scope.users = users;
 	$scope.role = role;
-	
-	console.debug("role: " + role)
-	
+
 	$scope.addUser = function() {
-		for (var i = 0; i < $scope.allUsers.length; i++) {
-			if ($scope.allUsers[i].userId == $scope.newUser) {
-				console.debug("add user " + $scope.allUsers[i]);
-				$scope.users.push($scope.allUsers[i]);
-				$scope.newUser = null;
-				
-				// Send notification when rest call is success
-				Notifications.success("Saved role mapping for user");
+		var user = $scope.newUser;
+		$scope.newUser = null;
+		
+		for ( var i = 0; i < $scope.allUsers.length; i++) {
+			if ($scope.allUsers[i].userId == user) {
+				user = $scope.allUsers[i];
+				RoleMapping.save({
+					realm : $scope.realmId,
+					role : role
+				}, user, function() {
+					$scope.users = RoleMapping.query({
+						realm : $scope.realmId,
+						role : role
+					});
+					Notifications.success("Added role mapping for user");
+				});
 			}
 		}
 	}
-	
-	$scope.removeUser = function(id) {
+
+	$scope.removeUser = function(userId) {
 		for (var i = 0; i < $scope.users.length; i++) {
-			if ($scope.users[i].userId == id) {
-				$scope.users.splice(i, 1);
-				
-				// Send notification when rest call is success
-				Notifications.success("Removed role mapping for user");
+			var user = $scope.users[i];
+			if ($scope.users[i].userId == userId) {
+				RoleMapping.delete({
+					realm : $scope.realmId,
+					role : role
+				}, user, function() {
+					$scope.users = RoleMapping.query({
+						realm : $scope.realmId,
+						role : role
+					});
+
+					Notifications.success("Removed role mapping for user");
+				});
 			}
 		}
 	}
diff --git a/ui/src/main/resources/META-INF/resources/ui/js/loaders.js b/ui/src/main/resources/META-INF/resources/ui/js/loaders.js
new file mode 100644
index 0000000..e3170ae
--- /dev/null
+++ b/ui/src/main/resources/META-INF/resources/ui/js/loaders.js
@@ -0,0 +1,84 @@
+'use strict';
+
+var module = angular.module('keycloak.loaders', [ 'keycloak.services', 'ngResource' ]);
+
+module.factory('Loader', function($q) {
+	var loader = {};
+	loader.get = function(service, id) {
+		return function() {
+			var i = id && id();
+			var delay = $q.defer();
+			service.get(i, function(entry) {
+				delay.resolve(entry);
+			}, function() {
+				delay.reject('Unable to fetch ' + i);
+			});
+			return delay.promise;
+		};
+	}
+	loader.query = function(service, id) {
+		return function() {
+			var i = id && id();
+			var delay = $q.defer();
+			service.query(i, function(entry) {
+				delay.resolve(entry);
+			}, function() {
+				delay.reject('Unable to fetch ' + i);
+			});
+			return delay.promise;
+		};
+	}
+	return loader;
+});
+
+module.factory('ApplicationListLoader', function(Loader, Application, $q) {
+	return Loader.query(Application);
+});
+
+module.factory('ApplicationLoader', function(Loader, Application, $route, $q) {
+	return Loader.get(Application, function() {
+		return {
+			id : $route.current.params.application
+		}
+	});
+});
+
+module.factory('RealmListLoader', function(Loader, Realm, $q) {
+	return Loader.query(Realm);
+});
+
+module.factory('RealmLoader', function(Loader, Realm, $route, $q) {
+	return Loader.get(Realm, function() {
+		return {
+			id : $route.current.params.realm
+		}
+	});
+});
+
+module.factory('UserListLoader', function(Loader, User, $route, $q) {
+	return Loader.query(User, function() {
+		return {
+			realm : $route.current.params.realm
+		}
+	});
+});
+
+module.factory('UserLoader', function(Loader, User, $route, $q) {
+	return Loader.get(User, function() {
+		return {
+			realm : $route.current.params.realm,
+			userId : $route.current.params.user
+		}
+	});
+});
+
+module.factory('RoleMappingLoader', function(Loader, RoleMapping, $route, $q) {
+	var realm = $route.current.params.realm || $route.current.params.application;
+
+	return Loader.query(RoleMapping, function() {
+		return {
+			realm : realm,
+			role : $route.current.params.role
+		}
+	});
+});
\ No newline at end of file
diff --git a/ui/src/main/resources/META-INF/resources/ui/js/services.js b/ui/src/main/resources/META-INF/resources/ui/js/services.js
index f8d46c2..0788d75 100644
--- a/ui/src/main/resources/META-INF/resources/ui/js/services.js
+++ b/ui/src/main/resources/META-INF/resources/ui/js/services.js
@@ -2,6 +2,40 @@
 
 var module = angular.module('keycloak.services', [ 'ngResource' ]);
 
+module.service('Auth', function($resource, $http, $location, $routeParams) {
+	var auth = {
+		loggedIn : true
+	};
+	auth.user = {
+		userId : 'test',
+		displayName : 'Test User'
+	};
+	return auth;
+});
+
+module.service('Dialog', function($dialog) {
+	var dialog = {};
+	dialog.confirmDelete = function(name, type, success) {
+		var title = 'Delete ' + name;
+		var msg = 'Are you sure you want to permanently delete this ' + type + '?';
+		var btns = [ {
+			result : 'cancel',
+			label : 'Cancel'
+		}, {
+			result : 'ok',
+			label : 'Delete this ' + type,
+			cssClass : 'btn-primary'
+		} ];
+
+		$dialog.messageBox(title, msg, btns).open().then(function(result) {
+			if (result == "ok") {
+				success();
+			}
+		});
+	}
+	return dialog
+});
+
 module.factory('Notifications', function($rootScope, $timeout) {
 	var notifications = {};
 
@@ -59,49 +93,10 @@ module.factory('Application', function($resource) {
 	});
 });
 
-module.factory('ApplicationListLoader', function(Application, $q) {
-	return function() {
-		var delay = $q.defer();
-		Application.query(function(applications) {
-			delay.resolve(applications);
-		}, function() {
-			delay.reject('Unable to fetch applications');
-		});
-		return delay.promise;
-	};
-});
-
-module.factory('ApplicationLoader', function(Application, $route, $q) {
-	return function() {
-		var id = $route.current.params.application;
-		var delay = $q.defer();
-		Application.get({
-			id : id
-		}, function(application) {
-			delay.resolve(application);
-		}, function() {
-			delay.reject('Unable to fetch application ' + id);
-		});
-		return delay.promise;
-	};
-});
-
 module.factory('Provider', function($resource) {
 	return $resource('/ejs-identity/api/admin/providers');
 });
 
-module.factory('ProviderListLoader', function(Provider, $q) {
-	return function() {
-		var delay = $q.defer();
-		Provider.query(function(providers) {
-			delay.resolve(providers);
-		}, function() {
-			delay.reject('Unable to fetch providers');
-		});
-		return delay.promise;
-	};
-});
-
 module.factory('Realm', function($resource) {
 	return $resource('/keycloak-server/ui/api/realms/:id', {
 		id : '@id'
@@ -112,104 +107,25 @@ module.factory('Realm', function($resource) {
 	});
 });
 
-module.factory('RealmListLoader', function(Realm, $q) {
-	return function() {
-		var delay = $q.defer();
-		Realm.query(function(realms) {
-			delay.resolve(realms);
-		}, function() {
-			delay.reject('Unable to fetch realms');
-		});
-		return delay.promise;
-	};
-});
-
-module.factory('RealmLoader', function(Realm, $route, $q) {
-	return function() {
-		var id = $route.current.params.realm;
-		var delay = $q.defer();
-		Realm.get({
-			id : id
-		}, function(realm) {
-			delay.resolve(realm);
-		}, function() {
-			delay.reject('Unable to fetch realm ' + name);
-		});
-		return delay.promise;
-	};
+module.factory('RoleMapping', function($resource) {
+	return $resource('/keycloak-server/ui/api/roles/:realm/:role/:userId', {
+		realm : '@realm',
+		role : '@role',
+		userId : '@userId'
+	}, {
+		save : {
+			method : 'PUT'
+		}
+	});
 });
 
 module.factory('User', function($resource) {
-	return $resource('/keycloak-server/ui/api/realms/:realm/users/:id', {
+	return $resource('/keycloak-server/ui/api/realms/:realm/users/:userId', {
 		realm : '@realm',
-		id : '@id'
+		userId : '@userId'
 	}, {
 		save : {
 			method : 'PUT'
 		}
 	});
-});
-
-module.factory('UserListLoader', function(User, $route, $q) {
-	return function() {
-		var delay = $q.defer();
-		User.query({
-			realm : $route.current.params.realm
-		}, function(users) {
-			delay.resolve(users);
-		}, function() {
-			delay.reject('Unable to fetch users');
-		});
-		return delay.promise;
-	};
-});
-
-module.factory('UserLoader', function(User, $route, $q) {
-	return function() {
-		var id = $route.current.params.user;
-		var delay = $q.defer();
-		User.get({
-			realm : $route.current.params.realm,
-			id : id
-		}, function(user) {
-			delay.resolve(user);
-		}, function() {
-			delay.reject('Unable to fetch user ' + name);
-		});
-		return delay.promise;
-	};
-});
-
-module.service('Dialog', function($dialog) {
-	var dialog = {};
-	dialog.confirmDelete = function(name, type, success) {
-		var title = 'Delete ' + name;
-		var msg = 'Are you sure you want to permanently delete this ' + type + '?';
-		var btns = [ {
-			result : 'cancel',
-			label : 'Cancel'
-		}, {
-			result : 'ok',
-			label : 'Delete this ' + type,
-			cssClass : 'btn-primary'
-		} ];
-
-		$dialog.messageBox(title, msg, btns).open().then(function(result) {
-			if (result == "ok") {
-				success();
-			}
-		});
-	}
-	return dialog
-});
-
-module.service('Auth', function($resource, $http, $location, $routeParams) {
-	var auth = {
-		loggedIn : true
-	};
-	auth.user = {
-		userId : 'test',
-		displayName : 'Test User'
-	};
-	return auth;
 });
\ No newline at end of file
diff --git a/ui/src/main/resources/META-INF/resources/ui/partials/application-detail.html b/ui/src/main/resources/META-INF/resources/ui/partials/application-detail.html
index 0cbd1aa..fa3948e 100644
--- a/ui/src/main/resources/META-INF/resources/ui/partials/application-detail.html
+++ b/ui/src/main/resources/META-INF/resources/ui/partials/application-detail.html
@@ -4,9 +4,9 @@
         <div id="actions-bg"></div>
         
         <div id="container-right" class="span9">
-            <h1>
-                <span class="gray" data-ng-show="create">New Application</span>
-				<span class="gray" data-ng-hide="create">{{application.name}}</span> configuration
+            <h1 data-ng-show="create"><span class="gray">New Application</span></h1>
+            <h1 data-ng-hide="create">
+                <span class="gray">{{application.name}}</span> configuration
             </h1>
 
             <div data-ng-show="applicationForm.showErrors && applicationForm.$error.required" class="alert alert-error">Please fill in all required fields</div>
diff --git a/ui/src/main/resources/META-INF/resources/ui/partials/application-menu.html b/ui/src/main/resources/META-INF/resources/ui/partials/application-menu.html
index 5197082..ca94f97 100644
--- a/ui/src/main/resources/META-INF/resources/ui/partials/application-menu.html
+++ b/ui/src/main/resources/META-INF/resources/ui/partials/application-menu.html
@@ -1,12 +1,16 @@
-<nav id="local-nav">
+<nav id="local-nav" data-ng-controller="ApplicationListCtrl">
     <ul class="nav nav-list">
         <li>
             <div>
                 <span class="toggle">Applications</span>
             </div>
             <ul>
-                <li data-ng-repeat="application in applications" data-ng-class="path[1] == application.id && 'active'">
-                	<a href="#/applications/{{application.id}}">{{application.name}}</a>
+                <li data-ng-repeat="a in applications" data-ng-class="path[1] == a.id && 'active'">
+                	<a href="#/applications/{{a.id}}">{{a.name}}</a>
+                    <ul class="sub-items" data-ng-show="path[1] == a.id">
+                        <li data-ng-class="!path[2] && 'active'"><a href="#/applications/{{a.id}}">Configuration</a></li>
+                        <li data-ng-class="path[2] == 'roles' && 'active'"><a href="#/applications/{{a.id}}/roles">Role mapping</a></li>
+                    </ul>
                 </li>
             </ul>
         </li>
diff --git a/ui/src/main/resources/META-INF/resources/ui/partials/realm-detail.html b/ui/src/main/resources/META-INF/resources/ui/partials/realm-detail.html
index f80b606..479bf83 100644
--- a/ui/src/main/resources/META-INF/resources/ui/partials/realm-detail.html
+++ b/ui/src/main/resources/META-INF/resources/ui/partials/realm-detail.html
@@ -4,9 +4,9 @@
         <div id="actions-bg"></div>
 
         <div id="container-right" class="span9">
-            <h1>
-                <span class="gray" data-ng-show="create">New Realm</span>
-                <span class="gray" data-ng-hide="create">{{realm.name}}</span> configuration
+            <h1 data-ng-show="create"><span class="gray">New Realm</span></h1>
+            <h1 data-ng-hide="create">
+                <span class="gray">{{realm.name}}</span> configuration
             </h1>
             
             <div data-ng-show="realmForm.showErrors && realmForm.$error.required" class="alert alert-error">Please fill in all required fields</div>
diff --git a/ui/src/main/resources/META-INF/resources/ui/partials/realm-menu.html b/ui/src/main/resources/META-INF/resources/ui/partials/realm-menu.html
index d36f2c8..ac8e0be 100644
--- a/ui/src/main/resources/META-INF/resources/ui/partials/realm-menu.html
+++ b/ui/src/main/resources/META-INF/resources/ui/partials/realm-menu.html
@@ -1,13 +1,13 @@
-<nav id="local-nav">
+<nav id="local-nav" data-ng-controller="RealmListCtrl">
     <ul class="nav nav-list">
         <li>
             <div>
                 <span class="toggle">Realms</span>
             </div>
             <ul>
-                <li data-ng-repeat="r in realms" data-ng-class="realm.id == r.id && 'active'">
+                <li data-ng-repeat="r in realms" data-ng-class="path[1] == r.id && 'active'">
                     <a href=#/realms/{{r.id}}>{{r.name}}</a>
-                    <ul class="sub-items" data-ng-show="realm.id == r.id">
+                    <ul class="sub-items" data-ng-show="path[1] == r.id">
                         <li data-ng-class="!path[2] && 'active'"><a href="#/realms/{{r.id}}">Configuration</a></li>
                         <li data-ng-class="path[2] == 'users' && 'active'"><a href="#/realms/{{r.id}}/users">Users</a></li>
                         <li data-ng-class="path[2] == 'roles' && 'active'"><a href="#/realms/{{r.id}}/roles">Role mapping</a></li>
diff --git a/ui/src/main/resources/META-INF/resources/ui/partials/role-mapping.html b/ui/src/main/resources/META-INF/resources/ui/partials/role-mapping.html
index e9eeace..c65ed91 100644
--- a/ui/src/main/resources/META-INF/resources/ui/partials/role-mapping.html
+++ b/ui/src/main/resources/META-INF/resources/ui/partials/role-mapping.html
@@ -1,20 +1,21 @@
 <div id="wrapper" class="container">
 	<div class="row">
-		<aside class="span3" data-ng-include data-src="'partials/realm-menu.html'"></aside>
+		<aside class="span3" data-ng-include data-src="'partials/' + path[0].slice(0, -1) + '-menu.html'"></aside>
+		
 		<div id="actions-bg"></div>
 
 		<div id="container-right" class="span9">
 			<h1>
-				<span class="gray" data-ng-hide="create">{{name}}</span> role mapping
+				<span class="gray" data-ng-hide="create">{{realm.name}}</span> role mapping
 			</h1>
 
 			<ul class="nav nav-tabs">
-				<li data-ng-class="path[3] == r && 'active'" data-ng-repeat="r in (realm.roles|orderBy:'toString()')"><a href="#/realms/{{realm.id}}/roles/{{r}}">{{r}}</a></li>
+				<li data-ng-class="path[3] == r && 'active'" data-ng-repeat="r in (realm.roles|orderBy:'toString()')"><a href="#/{{path[0]}}/{{realm.id}}/roles/{{r}}">{{r}}</a></li>
 			</ul>
 
 			<div data-ng-show="role">
-				<select id="realm" name="realm" data-ng-model="newUser" data-ng-click="addUser(u)">
-					<option data-ng-repeat="u in (allUsers|remove:users)" value="{{u.userId}}">{{u.userId}}</option>
+				<select style="width: auto;" id="realm" name="realm" data-ng-model="newUser" data-ng-click="addUser(u)">
+					<option data-ng-repeat="u in (allUsers|remove:users:'userId')" value="{{u.userId}}">{{u.userId}}</option>
 				</select>
 
 				<table class="table table-striped table-bordered">
@@ -32,7 +33,7 @@
 						<td>{{user.firstName}}</td>
 						<td>{{user.lastName}}</td>
 						<td>{{user.email}}</td>
-						<td><button ng-click="removeUser(user.userId)">
+						<td><button data-ng-click="removeUser(user.userId)">
 								<i class="icon-remove"></i>
 							</button></td>
 					</tr>
diff --git a/ui/src/main/resources/META-INF/resources/ui/partials/user-detail.html b/ui/src/main/resources/META-INF/resources/ui/partials/user-detail.html
index 8591a8b..fcb95b4 100644
--- a/ui/src/main/resources/META-INF/resources/ui/partials/user-detail.html
+++ b/ui/src/main/resources/META-INF/resources/ui/partials/user-detail.html
@@ -4,9 +4,9 @@
         <div id="actions-bg"></div>
 
         <div id="container-right" class="span9">
-            <h1>
-                <span class="gray" data-ng-show="create">New User</span>
-                <span class="gray" data-ng-hide="create">{{user.userId}}</span>
+            <h1 data-ng-show="create"><span class="gray">New User</span></h1>
+            <h1 data-ng-hide="create">
+                <span class="gray">{{user.userId}}</span> configuration
             </h1>
 
             <div data-ng-show="userForm.showErrors && userForm.$error.required" class="alert alert-error">Please fill in all required fields</div>