keycloak-aplcache

Details

diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js
index d780e60..7ab95aa 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/app.js
@@ -131,6 +131,39 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'RealmLdapSettingsCtrl'
         })
+        .when('/realms/:realm/auth-settings', {
+            templateUrl : 'partials/realm-auth-list.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                }
+            },
+            controller : 'RealmAuthSettingsCtrl'
+        })
+        .when('/realms/:realm/auth-settings/create', {
+            templateUrl : 'partials/realm-auth-detail.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
+                }
+            },
+            controller : 'RealmAuthSettingsDetailCtrl'
+        })
+        .when('/realms/:realm/auth-settings/:index', {
+            templateUrl : 'partials/realm-auth-detail.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                serverInfo : function(ServerInfoLoader) {
+                    return ServerInfoLoader();
+                }
+            },
+            controller : 'RealmAuthSettingsDetailCtrl'
+        })
         .when('/create/user/:realm', {
             templateUrl : 'partials/user-detail.html',
             resolve : {
@@ -1011,4 +1044,23 @@ module.filter('capitalize', function() {
     return function(input) {
         return input.substring(0, 1).toUpperCase() + input.substring(1);
     }
+});
+
+// Convert string like "externalRealmId" to more human-friendly "External Realm Id"
+module.filter('humanFriendlyFormat', function() {
+    return function(input) {
+        if (!input) {
+            return;
+        }
+        var result = input.substring(0, 1).toUpperCase();
+        var s = input.substring(1);
+        for (var i=0; i<s.length ; i++) {
+            var c = s[i];
+            if (c.match(/[A-Z]/)) {
+                result = result.concat(" ")
+            };
+            result = result.concat(c);
+        };
+        return result;
+    };
 });
\ No newline at end of file
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
index b441e31..3b38659 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js
@@ -887,7 +887,7 @@ module.controller('RealmSMTPSettingsCtrl', function($scope, Current, Realm, real
     }
 });
 
-module.controller('RealmLdapSettingsCtrl', function($scope, Realm, realm, $location, Notifications) {
+module.controller('RealmLdapSettingsCtrl', function($scope, $location, Notifications, Realm, realm) {
     console.log('RealmLdapSettingsCtrl');
 
     $scope.realm = realm;
@@ -914,4 +914,100 @@ module.controller('RealmLdapSettingsCtrl', function($scope, Realm, realm, $locat
         $scope.realm = angular.copy(oldCopy);
         $scope.changed = false;
     };
+});
+
+module.controller('RealmAuthSettingsCtrl', function($scope, realm) {
+    console.log('RealmAuthSettingsCtrl');
+
+    $scope.realm = realm;
+    $scope.authenticationProviders = realm.authenticationProviders;
+});
+
+module.controller('RealmAuthSettingsDetailCtrl', function($scope, $routeParams, $location, Notifications, Dialog, Realm, realm, serverInfo) {
+    console.log('RealmAuthSettingsDetailCtrl');
+
+    $scope.realm = realm;
+    $scope.availableProviders = serverInfo.authProviders;
+    $scope.availableProviderNames = Object.keys(serverInfo.authProviders);
+
+    $scope.create = !$routeParams.index;
+    $scope.changed = false;
+
+    if ($scope.create) {
+        $scope.authProvider = {
+            passwordUpdateSupported: true,
+            config: {}
+        };
+
+        $scope.authProviderOptionNames = [];
+    } else {
+        $scope.authProvider = realm.authenticationProviders[ $routeParams.index ];
+        if (!$scope.authProvider.config) {
+            $scope.authProvider.config = {};
+        }
+
+        $scope.authProviderOptionNames = serverInfo.authProviders[ $scope.authProvider.providerName ];
+        $scope.authProviderIndex = $routeParams.index;
+    }
+
+    var oldCopy = angular.copy($scope.authProvider);
+    $scope.$watch('authProvider', function() {
+        if (!angular.equals($scope.authProvider, oldCopy)) {
+            $scope.changed = true;
+        }
+    }, true);
+
+    $scope.changeAuthProvider = function() {
+        console.log('RealmAuthSettingsDetailCtrl: provider changed to ' + $scope.authProvider.providerName);
+        $scope.authProviderOptionNames = serverInfo.authProviders[ $scope.authProvider.providerName ];
+    }
+
+    $scope.cancel = function() {
+        $location.url("/realms/" + realm.realm + "/auth-settings");
+    }
+
+    $scope.reset = function() {
+        $scope.authProvider = angular.copy(oldCopy);
+        $scope.changed = false;
+    }
+
+    $scope.save = function() {
+        if (!$scope.authProvider.providerName) {
+            console.log('RealmAuthSettingsDetailCtrl: no provider selected. Skip creation');
+            return;
+        }
+
+        console.log('RealmAuthSettingsDetailCtrl: creating provider ' + $scope.authProvider.providerName);
+        var realmCopy = angular.copy($scope.realm);
+        if (!realmCopy.authenticationProviders) {
+            realmCopy.authenticationProviders = [];
+        }
+
+        if ($scope.create) {
+            realmCopy.authenticationProviders.push($scope.authProvider);
+        } else {
+            realmCopy.authenticationProviders[ $scope.authProviderIndex ] = $scope.authProvider;
+        }
+
+        $scope.changed = false;
+        Realm.update(realmCopy, function () {
+            $location.url("/realms/" + realm.realm + "/auth-settings");
+            Notifications.success("Authentication provider has been saved.");
+        });
+    };
+
+    $scope.remove = function() {
+        Dialog.confirmDelete($scope.realm.authenticationProviders.providerName, 'authentication Provider', function() {
+            console.log('RealmAuthSettingsDetailCtrl: deleting provider ' + $scope.authProvider.providerName);
+
+            var realmCopy = angular.copy($scope.realm);
+            realmCopy.authenticationProviders.splice($scope.authProviderIndex, 1);
+
+            $scope.changed = false;
+            Realm.update(realmCopy, function () {
+                $location.url("/realms/" + realm.realm + "/auth-settings");
+                Notifications.success("Authentication provider has been deleted.");
+            });
+        });
+    };
 });
\ No newline at end of file
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-auth-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-auth-detail.html
new file mode 100644
index 0000000..cc3981b
--- /dev/null
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-auth-detail.html
@@ -0,0 +1,74 @@
+<div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div>
+<div id="content-area" class="col-md-9" role="main">
+    <data-kc-navigation data-kc-current="auth-settings" data-kc-realm="realm.realm" data-kc-social="realm.social"></data-kc-navigation>
+
+    <div id="content">
+        <ol class="breadcrumb" data-ng-show="create">
+            <li><a href="#/realms/{{realm.realm}}">{{realm.realm}}</a></li>
+            <li><a href="#/realms/{{realm.realm}}">Settings</a></li>
+            <li><a href="#/realms/{{realm.realm}}/auth-settings">Authentication</a></li>
+            <li class="active">Add</li>
+        </ol>
+        <h2 data-ng-show="create">Add Authentication provider</h2>
+        <ol class="breadcrumb" data-ng-hide="create">
+            <li><a href="#/realms/{{realm.realm}}">{{realm.realm}}</a></li>
+            <li><a href="#/realms/{{realm.realm}}">Settings</a></li>
+            <li><a href="#/realms/{{realm.realm}}/auth-settings">Authentication</a></li>
+            <li class="active">{{authProviderIndex}}</li>
+        </ol>
+        <h2 data-ng-hide="create"><span>{{authProvider.providerName|humanFriendlyFormat}}'s</span> Attributes</h2>
+
+        <form class="form-horizontal" name="userForm" novalidate kc-read-only="!access.manageRealm">
+            <fieldset class="border-top">
+                <div class="form-group input-select">
+                    <label class="col-sm-2 control-label" for="authProviders">Provider Name</label>
+                    <div class="col-sm-4">
+                        <div class="input-group">
+                            <div class="select-kc">
+                                <select id="authProviders" name="authProviders"
+                                        data-ng-change="changeAuthProvider()"
+                                        data-ng-model="authProvider.providerName"
+                                        data-ng-options="(p|humanFriendlyFormat) for p in availableProviderNames"
+                                        data-ng-disabled="!create">
+                                    <option value="" selected> Select Authentication Provider...</option>
+                                </select>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="form-group clearfix block">
+                    <label class="col-sm-2 control-label" for="passwordUpdateSupported">Password Update Supported</label>
+                    <div class="col-sm-4">
+                        <input ng-model="authProvider.passwordUpdateSupported" name="passwordUpdateSupported" id="passwordUpdateSupported" onoffswitch />
+                    </div>
+                </div>
+
+            </fieldset>
+
+            <fieldset>
+                <legend data-ng-show="authProvider.providerName"><span class="text">{{authProvider.providerName|humanFriendlyFormat}}'s provider options</span></legend>
+                <div data-ng-repeat="option in authProviderOptionNames" class="form-group">
+                    <label class="col-sm-2 control-label">{{option|humanFriendlyFormat}} </label>
+
+                    <div class="col-sm-4">
+                        <input class="form-control" type="text" data-ng-model="authProvider.config[ option ]" >
+                    </div>
+                </div>
+
+            </fieldset>
+
+            <div class="pull-right form-actions" data-ng-show="create && access.manageRealm">
+                <button kc-cancel data-ng-click="cancel()">Cancel</button>
+                <button kc-save data-ng-show="changed && authProvider.providerName">Save</button>
+            </div>
+
+            <div class="pull-right form-actions" data-ng-show="!create && access.manageRealm">
+                <button kc-reset data-ng-show="changed">Clear changes</button>
+                <button kc-save  data-ng-show="changed">Save</button>
+                <button kc-delete data-ng-click="remove()" data-ng-hide="changed">Delete</button>
+            </div>
+
+        </form>
+    </div>
+</div>
\ No newline at end of file
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-auth-list.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-auth-list.html
new file mode 100644
index 0000000..d2aebfd
--- /dev/null
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-auth-list.html
@@ -0,0 +1,41 @@
+<div class="bs-sidebar col-sm-3 " data-ng-include data-src="'partials/realm-menu.html'"></div>
+<div id="content-area" class="col-sm-9" role="main">
+    <data-kc-navigation data-kc-current="auth-settings" data-kc-realm="realm.realm" data-kc-social="realm.social"></data-kc-navigation>
+    <div id="content">
+        <ol class="breadcrumb">
+            <li><a href="#/realms/{{realm.realm}}">{{realm.realm}}</a></li>
+            <li><a href="#/realms/{{realm.realm}}">Settings</a></li>
+            <li class="active">Authentication</li>
+        </ol>
+        <h2><span>{{realm.realm}}</span> Authentication Providers</h2>
+
+        <div class="panel">
+            <table class="table">
+                <thead>
+                <tr>
+                    <th class="kc-table-actions" colspan="3">
+                        <div class="pull-right" data-ng-show="access.manageRealm">
+                            <a class="btn btn-primary" href="#/realms/{{realm.realm}}/auth-settings/create">Add Provider</a>
+                        </div>
+                    </th>
+                </tr>
+                <tr data-ng-show="authenticationProviders && authenticationProviders.length > 0">
+                    <th>Provider Name</th>
+                    <th>Password Update Supported</th>
+                    <th>Configuration</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr ng-repeat="authProvider in authenticationProviders">
+                    <td><a href="#/realms/{{realm.realm}}/auth-settings/{{$index}}">{{authProvider.providerName|humanFriendlyFormat}}</a></td>
+                    <td>{{authProvider.passwordUpdateSupported}}</td>
+                    <td>{{authProvider.config}}</td>
+                </tr>
+                <tr data-ng-show="!authenticationProviders || authenticationProviders.length == 0">
+                    <td>No authentication providers available</td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html
index b0ae386..fc80c29 100755
--- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html
@@ -1,7 +1,9 @@
 <ul data-ng-hide="createRealm">
     <li data-ng-show="access.viewRealm" data-ng-class="((!path[2] || path[1] == 'role' || path[2] == 'roles' || path[2] == 'token-settings' ||
     path[2] == 'social-settings' || path[2] == 'required-credentials' || path[2] == 'default-roles' || path[2] == 'registration-settings' ||
-    path[2] == 'keys-settings' || path[2] == 'smtp-settings' || path[2] == 'ldap-settings') && path[3] != 'applications') && 'active'"><a href="#/realms/{{realm.realm}}">Settings</a></li>
+    path[2] == 'keys-settings' || path[2] == 'smtp-settings' || path[2] == 'ldap-settings' || path[2] == 'auth-settings') && path[3] != 'applications') && 'active'">
+        <a href="#/realms/{{realm.realm}}">Settings</a>
+    </li>
     <li data-ng-show="access.viewUsers" data-ng-class="(path[2] == 'users' || path[1] == 'user') && 'active'"><a href="#/realms/{{realm.realm}}/users">Users</a>
     </li>
     <li data-ng-show="access.viewApplications" data-ng-class="(path[2] == 'applications' || path[1] == 'application' || path[3] == 'applications') && 'active'"><a href="#/realms/{{realm.realm}}/applications">Applications</a></li>
diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html b/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html
index 95dac11..5abcf08 100644
--- a/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html
+++ b/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html
@@ -8,4 +8,5 @@
     <li ng-class="{active: path[2] == 'keys-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/keys-settings">Keys</a></li>
     <li ng-class="{active: path[2] == 'smtp-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/smtp-settings">Email</a></li>
     <li ng-class="{active: path[2] == 'ldap-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/ldap-settings">Ldap</a></li>
+    <li ng-class="{active: path[2] == 'auth-settings'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/auth-settings">Authentication</a></li>
 </ul>
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
index 04cda21..46449f3 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ServerInfoAdminResource.java
@@ -3,6 +3,8 @@ package org.keycloak.services.resources.admin;
 import org.keycloak.freemarker.Theme;
 import org.keycloak.freemarker.ThemeProvider;
 import org.keycloak.social.SocialProvider;
+import org.keycloak.spi.authentication.AuthenticationProvider;
+import org.keycloak.spi.authentication.AuthenticationProviderManager;
 import org.keycloak.util.ProviderLoader;
 
 import javax.ws.rs.GET;
@@ -22,6 +24,7 @@ public class ServerInfoAdminResource {
         ServerInfoRepresentation info = new ServerInfoRepresentation();
         setSocialProviders(info);
         setThemes(info);
+        setAuthProviders(info);
         return info;
     }
 
@@ -46,12 +49,22 @@ public class ServerInfoAdminResource {
         Collections.sort(info.socialProviders);
     }
 
+    private void setAuthProviders(ServerInfoRepresentation info) {
+        info.authProviders = new HashMap<String, List<String>>();
+        Iterable<AuthenticationProvider> authProviders = AuthenticationProviderManager.load();
+        for (AuthenticationProvider authProvider : authProviders) {
+            info.authProviders.put(authProvider.getName(), authProvider.getAvailableOptions());
+        }
+    }
+
     public static class ServerInfoRepresentation {
 
         private Map<String, List<String>> themes;
 
         private List<String> socialProviders;
 
+        private Map<String, List<String>> authProviders;
+
         public ServerInfoRepresentation() {
         }
 
@@ -63,6 +76,9 @@ public class ServerInfoAdminResource {
             return socialProviders;
         }
 
+        public Map<String, List<String>> getAuthProviders() {
+            return authProviders;
+        }
     }
 
 }
diff --git a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java
index a4d129b..a1dfa10 100644
--- a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java
+++ b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ExternalModelAuthenticationProvider.java
@@ -1,5 +1,7 @@
 package org.keycloak.spi.authentication.model;
 
+import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 
 import org.jboss.resteasy.spi.ResteasyProviderFactory;
@@ -23,6 +25,11 @@ public class ExternalModelAuthenticationProvider extends AbstractModelAuthentica
     }
 
     @Override
+    public List<String> getAvailableOptions() {
+        return Arrays.asList(AuthProviderConstants.EXTERNAL_REALM_ID);
+    }
+
+    @Override
     public RealmModel getRealm(RealmModel currentRealm, Map<String, String> configuration) throws AuthenticationProviderException {
         String realmId = configuration.get(AuthProviderConstants.EXTERNAL_REALM_ID);
         if (realmId == null) {
diff --git a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java
index 29ab43a..cd28aba 100644
--- a/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java
+++ b/spi/authentication-model/src/main/java/org/keycloak/spi/authentication/model/ModelAuthenticationProvider.java
@@ -1,11 +1,11 @@
 package org.keycloak.spi.authentication.model;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserModel;
 import org.keycloak.spi.authentication.AuthProviderConstants;
-import org.keycloak.spi.authentication.AuthUser;
 
 /**
  * AbstractModelAuthenticationProvider, which uses current realm to call operations on
@@ -20,6 +20,11 @@ public class ModelAuthenticationProvider extends AbstractModelAuthenticationProv
     }
 
     @Override
+    public List<String> getAvailableOptions() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
     protected RealmModel getRealm(RealmModel currentRealm, Map<String, String> config) {
         return currentRealm;
     }
diff --git a/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java b/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java
index ffc1ab6..4111806 100755
--- a/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java
+++ b/spi/authentication-picketlink/src/main/java/org/keycloak/spi/authentication/picketlink/PicketlinkAuthenticationProvider.java
@@ -1,5 +1,7 @@
 package org.keycloak.spi.authentication.picketlink;
 
+import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 import org.jboss.logging.Logger;
@@ -35,6 +37,11 @@ public class PicketlinkAuthenticationProvider implements AuthenticationProvider 
     }
 
     @Override
+    public List<String> getAvailableOptions() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
     public AuthUser getUser(RealmModel realm, Map<String, String> configuration, String username) throws AuthenticationProviderException {
         IdentityManager identityManager = getIdentityManager(realm);
         User picketlinkUser = BasicModel.getUser(identityManager, username);
diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java
index 014d432..550da90 100644
--- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java
+++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProvider.java
@@ -1,5 +1,6 @@
 package org.keycloak.spi.authentication;
 
+import java.util.List;
 import java.util.Map;
 
 import org.keycloak.models.RealmModel;
@@ -12,6 +13,13 @@ public interface AuthenticationProvider {
     String getName();
 
     /**
+     * Get names of all available configuration options of current provider
+     *
+     * @return options or empty list if no options available
+     */
+    List<String> getAvailableOptions();
+
+    /**
      * Get user by given username or email. Return user instance or null if user doesn't exists in this authentication provider
      *
      * @param realm
diff --git a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java
index c6a59a1..3319cff 100644
--- a/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java
+++ b/spi/authentication-spi/src/main/java/org/keycloak/spi/authentication/AuthenticationProviderManager.java
@@ -38,7 +38,7 @@ public class AuthenticationProviderManager {
         return new AuthenticationProviderManager(realm, providersMap);
     }
 
-    private static Iterable<AuthenticationProvider> load() {
+    public static Iterable<AuthenticationProvider> load() {
         return ProviderLoader.load(AuthenticationProvider.class);
     }