keycloak-uncached

Merge pull request #370 from mposolda/master Social fixes

5/7/2014 12:50:19 PM

Changes

Details

diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java
index 2fbe8c6..481f1d9 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java
@@ -21,6 +21,7 @@ import org.keycloak.social.SocialProvider;
 public class AccountSocialBean {
 
     private final List<SocialLinkEntry> socialLinks;
+    private final boolean removeLinkPossible;
 
     public AccountSocialBean(RealmModel realm, UserModel user, URI baseUri) {
         URI accountSocialUpdateUri = Urls.accountSocialUpdate(baseUri, realm.getName());
@@ -29,12 +30,16 @@ public class AccountSocialBean {
         Map<String, String> socialConfig = realm.getSocialConfig();
         Set<SocialLinkModel> userSocialLinks = realm.getSocialLinks(user);
 
+        int availableLinks = 0;
         if (socialConfig != null && !socialConfig.isEmpty()) {
             for (SocialProvider provider : SocialLoader.load()) {
                 String socialProviderId = provider.getId();
                 if (socialConfig.containsKey(socialProviderId + ".key")) {
                     SocialLinkModel socialLink = getSocialLink(userSocialLinks, socialProviderId);
 
+                    if (socialLink != null) {
+                        availableLinks++;
+                    }
                     String action = socialLink != null ? "remove" : "add";
                     String actionUrl = UriBuilder.fromUri(accountSocialUpdateUri).queryParam("action", action).queryParam("provider_id", socialProviderId).build().toString();
 
@@ -43,6 +48,9 @@ public class AccountSocialBean {
                 }
             }
         }
+
+        // Removing last social provider is not possible if you don't have other possibility to authenticate
+        this.removeLinkPossible = availableLinks > 1 || realm.getAuthenticationLink(user) != null;
     }
 
     private SocialLinkModel getSocialLink(Set<SocialLinkModel> userSocialLinks, String socialProviderId) {
@@ -58,6 +66,10 @@ public class AccountSocialBean {
         return socialLinks;
     }
 
+    public boolean isRemoveLinkPossible() {
+        return removeLinkPossible;
+    }
+
     public class SocialLinkEntry {
 
         private SocialLinkModel link;
diff --git a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
index 1945c64..152f61c 100755
--- a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
+++ b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties
@@ -30,6 +30,7 @@ missingSocialProvider=Social provider not specified
 invalidSocialAction=Invalid or missing action
 socialProviderNotFound=Specified social provider not found
 socialLinkNotActive=This social link is not active anymore
+socialRemovingLastProvider=You can't remove last social provider as you don't have password
 socialRedirectError=Failed to redirect to social provider
 socialProviderRemoved=Social provider removed successfully
 
diff --git a/forms/common-themes/src/main/resources/theme/account/base/social.ftl b/forms/common-themes/src/main/resources/theme/account/base/social.ftl
index e238248..2bba805 100644
--- a/forms/common-themes/src/main/resources/theme/account/base/social.ftl
+++ b/forms/common-themes/src/main/resources/theme/account/base/social.ftl
@@ -18,7 +18,9 @@
                 </div>
                 <div class="col-sm-5 col-md-5">
                     <#if socialLink.connected>
-                        <a href="${socialLink.actionUrl}" type="submit" class="btn btn-primary btn-lg">Remove ${socialLink.providerName!}</a>
+                        <#if social.removeLinkPossible>
+                            <a href="${socialLink.actionUrl}" type="submit" class="btn btn-primary btn-lg">Remove ${socialLink.providerName!}</a>
+                        </#if>
                     <#else>
                         <a href="${socialLink.actionUrl}" type="submit" class="btn btn-primary btn-lg">Add ${socialLink.providerName!}</a>
                     </#if>
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
index 14b58b5..70a87be 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js
@@ -260,6 +260,21 @@ module.config([ '$routeProvider', function($routeProvider) {
             },
             controller : 'UserSessionsCtrl'
         })
+        .when('/realms/:realm/users/:user/social-links', {
+            templateUrl : 'partials/user-social-links.html',
+            resolve : {
+                realm : function(RealmLoader) {
+                    return RealmLoader();
+                },
+                user : function(UserLoader) {
+                    return UserLoader();
+                },
+                socialLinks : function(UserSocialLinksLoader) {
+                    return UserSocialLinksLoader();
+                }
+            },
+            controller : 'UserSocialCtrl'
+        })
         .when('/realms/:realm/users', {
             templateUrl : 'partials/user-list.html',
             resolve : {
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js
index 9bb89eb..7ab8da7 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js
@@ -146,6 +146,13 @@ module.controller('UserSessionsCtrl', function($scope, realm, user, stats, UserL
 
 });
 
+module.controller('UserSocialCtrl', function($scope, realm, user, socialLinks) {
+    $scope.realm = realm;
+    $scope.user = user;
+    $scope.socialLinks = socialLinks;
+    console.log('showing social links of user');
+});
+
 
 module.controller('UserListCtrl', function($scope, realm, User) {
     $scope.realm = realm;
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js
index e1e6a78..53101e4 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js
@@ -89,6 +89,15 @@ module.factory('UserSessionStatsLoader', function(Loader, UserSessionStats, $rou
     });
 });
 
+module.factory('UserSocialLinksLoader', function(Loader, UserSocialLinks, $route, $q) {
+    return Loader.query(UserSocialLinks, function() {
+        return {
+            realm : $route.current.params.realm,
+            user : $route.current.params.user
+        }
+    });
+});
+
 module.factory('RoleLoader', function(Loader, Role, $route, $q) {
     return Loader.get(Role, function() {
         return {
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js
index b3d5964..749f373 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js
@@ -185,6 +185,12 @@ module.factory('UserLogout', function($resource) {
         user : '@user'
     });
 });
+module.factory('UserSocialLinks', function($resource) {
+    return $resource(authUrl + '/rest/admin/realms/:realm/users/:user/social-links', {
+        realm : '@realm',
+        user : '@user'
+    });
+});
 
 module.factory('UserCredentials', function($resource) {
     var credentials = {};
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/role-mappings.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/role-mappings.html
index 63e34da..09dd60d 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/role-mappings.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/role-mappings.html
@@ -5,6 +5,7 @@
         <li data-ng-show="access.manageUsers"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/user-credentials">Credentials</a></li>
         <li class="active"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/role-mappings">Role Mappings</a></li>
         <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/sessions">Sessions</a></li>
+        <li data-ng-show="realm.social"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/social-links">Social Links</a></li>
     </ul>
     <div id="content">
         <ol class="breadcrumb">
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-credentials.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-credentials.html
index 38ca069..dea6b5b 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-credentials.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-credentials.html
@@ -6,6 +6,7 @@
         <li class="active"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/user-credentials">Credentials</a></li>
         <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/role-mappings">Role Mappings</a></li>
         <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/sessions">Sessions</a></li>
+        <li data-ng-show="realm.social"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/social-links">Social Links</a></li>
     </ul>
 
     <div id="content">
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html
index f0b32d4..137efdb 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-detail.html
@@ -6,6 +6,7 @@
         <li data-ng-show="access.manageUsers"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/user-credentials">Credentials</a></li>
         <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/role-mappings">Role Mappings</a></li>
         <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/sessions">Sessions</a></li>
+        <li data-ng-show="realm.social"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/social-links">Social Links</a></li>
     </ul>
 
     <div id="content">
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-sessions.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-sessions.html
index b702f25..1144c5b 100755
--- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-sessions.html
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-sessions.html
@@ -5,6 +5,7 @@
         <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/user-credentials">Credentials</a></li>
         <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/role-mappings">Role Mappings</a></li>
         <li class="active"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/sessions">Sessions</a></li>
+        <li data-ng-show="realm.social"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/social-links">Social Links</a></li>
     </ul>
     <div id="content">
         <ol class="breadcrumb">
diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-social-links.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-social-links.html
new file mode 100644
index 0000000..d50ee06
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/user-social-links.html
@@ -0,0 +1,33 @@
+<div class="bs-sidebar col-md-3 clearfix" data-ng-include data-src="'partials/realm-menu.html'"></div>
+<div id="content-area" class="col-md-9" role="main">
+    <ul class="nav nav-tabs nav-tabs-pf">
+        <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}">Attributes</a></li>
+        <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/user-credentials">Credentials</a></li>
+        <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/role-mappings">Role Mappings</a></li>
+        <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}/sessions">Sessions</a></li>
+        <li class="active"><a href="#/realms/{{realm.realm}}/users/{{user.username}}/social-links">Social Links</a></li>
+    </ul>
+    <div id="content">
+        <ol class="breadcrumb">
+            <li><a href="#/realms/{{realm.realm}}">{{realm.realm}}</a></li>
+            <li><a href="#/realms/{{realm.realm}}/users">Users</a></li>
+            <li><a href="#/realms/{{realm.realm}}/users/{{user.username}}">{{user.username}}</a></li>
+            <li class="active">Social Links</li>
+        </ol>
+        <h2><span>{{user.username}}</span> Social Links</h2>
+        <table class="table">
+            <thead>
+            <tr>
+                <th>Provider Name</th>
+                <th>Social Username</th>
+            </tr>
+            </thead>
+            <tbody>
+            <tr data-ng-repeat="socialLink in socialLinks">
+                <td>{{socialLink.socialProvider}}</td>
+                <td>{{socialLink.socialUsername}}</td>
+            </tr>
+            </tbody>
+        </table>
+    </div>
+</div>
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java
index 197046b..5919c26 100755
--- a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java
+++ b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java
@@ -8,6 +8,7 @@ import org.keycloak.models.Constants;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RequiredCredentialModel;
 import org.keycloak.models.RoleModel;
+import org.keycloak.models.SocialLinkModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.representations.idm.AuthenticationProviderRepresentation;
@@ -16,6 +17,7 @@ import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.RealmAuditRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.SocialLinkRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 
 import java.util.ArrayList;
@@ -168,4 +170,12 @@ public class ModelToRepresentation {
         rep.setUsername(ClaimMask.hasUsername(model.getAllowedClaimsMask()));
         return rep;
     }
+
+    public static SocialLinkRepresentation toRepresentation(SocialLinkModel socialLink) {
+        SocialLinkRepresentation rep = new SocialLinkRepresentation();
+        rep.setSocialUsername(socialLink.getSocialUsername());
+        rep.setSocialProvider(socialLink.getSocialProvider());
+        rep.setSocialUserId(socialLink.getSocialUserId());
+        return rep;
+    }
 }
diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java
index cc0ab1d..01c6f80 100755
--- a/services/src/main/java/org/keycloak/services/messages/Messages.java
+++ b/services/src/main/java/org/keycloak/services/messages/Messages.java
@@ -71,6 +71,8 @@ public class Messages {
 
     public static final String SOCIAL_LINK_NOT_ACTIVE = "socialLinkNotActive";
 
+    public static final String SOCIAL_REMOVING_LAST_PROVIDER = "socialRemovingLastProvider";
+
     public static final String SOCIAL_REDIRECT_ERROR = "socialRedirectError";
 
     public static final String SOCIAL_PROVIDER_REMOVED = "socialProviderRemoved";
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 1a45b6c..3666600 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -422,15 +422,21 @@ public class AccountService {
             case REMOVE:
                 SocialLinkModel link = realm.getSocialLink(user, providerId);
                 if (link != null) {
-                    realm.removeSocialLink(user, providerId);
 
-                    logger.debug("Social provider " + providerId + " removed successfully from user " + user.getLoginName());
+                    // Removing last social provider is not possible if you don't have other possibility to authenticate
+                    if (realm.getSocialLinks(user).size() > 1 || realm.getAuthenticationLink(user) != null) {
+                        realm.removeSocialLink(user, providerId);
 
-                    audit.event(Events.REMOVE_SOCIAL_LINK).client(auth.getClient()).user(auth.getUser())
-                            .detail(Details.USERNAME, link.getSocialUserId() + "@" + link.getSocialProvider())
-                            .success();
+                        logger.debug("Social provider " + providerId + " removed successfully from user " + user.getLoginName());
 
-                    return account.setSuccess(Messages.SOCIAL_PROVIDER_REMOVED).createResponse(AccountPages.SOCIAL);
+                        audit.event(Events.REMOVE_SOCIAL_LINK).client(auth.getClient()).user(auth.getUser())
+                                .detail(Details.USERNAME, link.getSocialUserId() + "@" + link.getSocialProvider())
+                                .success();
+
+                        return account.setSuccess(Messages.SOCIAL_PROVIDER_REMOVED).createResponse(AccountPages.SOCIAL);
+                    } else {
+                        return account.setError(Messages.SOCIAL_REMOVING_LAST_PROVIDER).createResponse(AccountPages.SOCIAL);
+                    }
                 } else {
                     return account.setError(Messages.SOCIAL_LINK_NOT_ACTIVE).createResponse(AccountPages.SOCIAL);
                 }
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 8dfcf59..10ce72d 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -11,6 +11,7 @@ import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
+import org.keycloak.models.SocialLinkModel;
 import org.keycloak.models.UserCredentialModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.representations.adapters.action.UserStats;
@@ -18,6 +19,7 @@ import org.keycloak.representations.idm.ApplicationMappingsRepresentation;
 import org.keycloak.representations.idm.CredentialRepresentation;
 import org.keycloak.representations.idm.MappingsRepresentation;
 import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.SocialLinkRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.services.email.EmailException;
 import org.keycloak.services.email.EmailSender;
@@ -179,6 +181,25 @@ public class UsersResource {
         return stats;
     }
 
+    @Path("{username}/social-links")
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<SocialLinkRepresentation> getSocialLinks(final @PathParam("username") String username) {
+        auth.requireView();
+        UserModel user = realm.getUser(username);
+        if (user == null) {
+            throw new NotFoundException("User not found");
+        }
+        Set<SocialLinkModel> socialLinks = realm.getSocialLinks(user);
+        List<SocialLinkRepresentation> result = new ArrayList<SocialLinkRepresentation>();
+        for (SocialLinkModel socialLink : socialLinks) {
+            SocialLinkRepresentation rep = ModelToRepresentation.toRepresentation(socialLink);
+            result.add(rep);
+        }
+        return result;
+    }
+
     @Path("{username}/logout")
     @POST
     public void logout(final @PathParam("username") String username) {
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index b0be76b..74c30bb 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -174,6 +174,10 @@
             <version>${project.version}</version>
         </dependency>
         <dependency>
+            <groupId>org.twitter4j</groupId>
+            <artifactId>twitter4j-core</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.keycloak</groupId>
             <artifactId>keycloak-social-facebook</artifactId>
             <version>${project.version}</version>