+
+
+ +

+
+
+
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
new file mode 100755
index 0000000000..2990675a06
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
@@ -0,0 +1,168 @@
+var module = angular.module('photoz', ['ngRoute', 'ngResource']);
+
+var resourceServerId = 'photoz-restful-api';
+var apiUrl = window.location.origin + '/' + resourceServerId;
+
+angular.element(document).ready(function ($http) {
+    var keycloak = new Keycloak('keycloak.json');
+    keycloak.init({onLoad: 'login-required'}).success(function () {
+        console.log('User is now authenticated.');
+
+        module.factory('Identity', function () {
+            return new Identity(keycloak);
+        });
+
+        angular.bootstrap(document, ["photoz"]);
+    }).error(function () {
+        window.location.reload();
+    });
+});
+
+module.config(function ($httpProvider, $routeProvider) {
+    $httpProvider.interceptors.push('authInterceptor');
+    $routeProvider.when('/', {
+        templateUrl: 'partials/home.html',
+        controller: 'GlobalCtrl'
+    }).when('/album/create', {
+        templateUrl: 'partials/album/create.html',
+        controller: 'AlbumCtrl',
+    }).when('/album/:id', {
+        templateUrl: 'partials/album/detail.html',
+        controller: 'AlbumCtrl',
+    }).when('/admin/album', {
+        templateUrl: 'partials/admin/albums.html',
+        controller: 'AdminAlbumCtrl',
+    }).when('/profile', {
+        templateUrl: 'partials/profile.html',
+        controller: 'ProfileCtrl',
+    });
+});
+
+module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Album, Identity) {
+    Album.query(function (albums) {
+        $scope.albums = albums;
+    });
+
+    $scope.Identity = Identity;
+
+    $scope.deleteAlbum = function (album) {
+        new Album(album).$delete({id: album.id}, function () {
+            $route.reload();
+        });
+    }
+});
+
+module.controller('TokenCtrl', function ($scope, Identity) {
+    $scope.showRpt = function () {
+        document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.authorization.rpt), null, '  ');
+    }
+
+    $scope.showAccessToken = function () {
+        document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.authc.token), null, '  ');
+    }
+
+    $scope.requestEntitlements = function () {
+        Identity.authorization.entitlement('photoz-restful-api').then(function (rpt) {});
+    }
+});
+
+module.controller('AlbumCtrl', function ($scope, $http, $routeParams, $location, Album) {
+    $scope.album = {};
+    if ($routeParams.id) {
+        $scope.album = Album.get({id: $routeParams.id});
+    }
+    $scope.create = function () {
+        var newAlbum = new Album($scope.album);
+        newAlbum.$save({}, function (data) {
+            $location.path('/');
+        });
+    };
+});
+
+module.controller('ProfileCtrl', function ($scope, $http, $routeParams, $location, Profile) {
+    $scope.profile = Profile.get();
+});
+
+module.controller('AdminAlbumCtrl', function ($scope, $http, $route, AdminAlbum, Album) {
+    $scope.albums = {};
+    $http.get(apiUrl + '/admin/album').success(function (data) {
+        $scope.albums = data;
+    });
+    $scope.deleteAlbum = function (album) {
+        var newAlbum = new Album(album);
+        newAlbum.$delete({id: album.id}, function () {
+            $route.reload();
+        });
+    }
+});
+
+module.factory('Album', ['$resource', function ($resource) {
+    return $resource(apiUrl + '/album/:id');
+}]);
+
+module.factory('Profile', ['$resource', function ($resource) {
+    return $resource(apiUrl + '/profile');
+}]);
+
+module.factory('AdminAlbum', ['$resource', function ($resource) {
+    return $resource(apiUrl + '/admin/album/:id');
+}]);
+
+module.factory('authInterceptor', function ($q, $injector, $timeout, Identity) {
+    return {
+        request: function (request) {
+            document.getElementById("output").innerHTML = '';
+            if (Identity.authorization && Identity.authorization.rpt && request.url.indexOf('/authorize') == -1) {
+                retries = 0;
+                request.headers.Authorization = 'Bearer ' + Identity.authorization.rpt;
+            } else {
+                request.headers.Authorization = 'Bearer ' + Identity.authc.token;
+            }
+            return request;
+        },
+        responseError: function (rejection) {
+            var status = rejection.status;
+
+            if (status == 403 || status == 401) {
+                var retry = (!rejection.config.retry ||  rejection.config.retry < 1);
+
+                if (!retry) {
+                    document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
+                    return $q.reject(rejection);
+                }
+
+                if (rejection.config.url.indexOf('/authorize') == -1 && retry) {
+                    var deferred = $q.defer();
+
+                    // here is the authorization logic, which tries to obtain an authorization token from the server in case the resource server
+                    // returns a 403 or 401.
+                    Identity.authorization.authorize(rejection.headers('WWW-Authenticate')).then(function (rpt) {
+                        deferred.resolve(rejection);
+                    }, function () {
+                        document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
+                    }, function () {
+                        document.getElementById("output").innerHTML = 'Unexpected error from server.';
+                    });
+
+                    var promise = deferred.promise;
+
+                    return promise.then(function (res) {
+                        if (!res.config.retry) {
+                            res.config.retry = 1;
+                        } else {
+                            res.config.retry++;
+                        }
+
+                        var $http = $injector.get("$http");
+
+                        return $http(res.config).then(function (response) {
+                            return response;
+                        });
+                    });
+                }
+            }
+
+            return $q.reject(rejection);
+        }
+    };
+});
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js
new file mode 100644
index 0000000000..9a018e4747
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js
@@ -0,0 +1,60 @@
+/*
+ *  Copyright 2016 Red Hat, Inc. and/or its affiliates
+ *  and other contributors as indicated by the @author tags.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *
+ */
+
+/**
+ * Creates an Identity object holding the information obtained from the access token issued by Keycloak, after a successful authentication,
+ * and a few utility methods to manage it.
+ */
+(function (window, undefined) {
+    var Identity = function (keycloak) {
+        this.loggedIn = true;
+
+        this.claims = {};
+        this.claims.name = keycloak.idTokenParsed.name;
+
+        this.authc = {};
+        this.authc.token = keycloak.token;
+
+        this.logout = function () {
+            keycloak.logout();
+        };
+
+        this.hasRole = function (name) {
+            if (keycloak && keycloak.hasRealmRole(name)) {
+                return true;
+            }
+            return false;
+        };
+
+        this.isAdmin = function () {
+            return this.hasRole("admin");
+        };
+
+        this.authorization = new KeycloakAuthorization(keycloak);
+    }
+
+    if ( typeof module === "object" && module && typeof module.exports === "object" ) {
+        module.exports = Identity;
+    } else {
+        window.Identity = Identity;
+
+        if ( typeof define === "function" && define.amd ) {
+            define( "identity", [], function () { return Identity; } );
+        }
+    }
+})( window );
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/keycloak.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/keycloak.json
new file mode 100644
index 0000000000..c1dee24574
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/keycloak.json
@@ -0,0 +1,8 @@
+{
+  "realm": "photoz",
+  "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "auth-server-url" : "http://localhost:8080/auth",
+  "ssl-required" : "external",
+  "resource" : "photoz-html5-client",
+  "public-client" : true
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-resource.min.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-resource.min.js
new file mode 100644
index 0000000000..3f196c3538
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-resource.min.js
@@ -0,0 +1,13 @@
+/*
+ AngularJS v1.3.0-beta.5
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(H,a,A){'use strict';function D(p,g){g=g||{};a.forEach(g,function(a,c){delete g[c]});for(var c in p)!p.hasOwnProperty(c)||"$"===c.charAt(0)&&"$"===c.charAt(1)||(g[c]=p[c]);return g}var v=a.$$minErr("$resource"),C=/^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;a.module("ngResource",["ng"]).factory("$resource",["$http","$q",function(p,g){function c(a,c){this.template=a;this.defaults=c||{};this.urlParams={}}function t(n,w,l){function r(h,d){var e={};d=x({},w,d);s(d,function(b,d){u(b)&&(b=b());var k;if(b&&
+b.charAt&&"@"==b.charAt(0)){k=h;var a=b.substr(1);if(null==a||""===a||"hasOwnProperty"===a||!C.test("."+a))throw v("badmember",a);for(var a=a.split("."),f=0,c=a.length;f").append(b).html();try{return 3===b[0].nodeType?I(c):c.match(/^(<[^>]+>)/)[1].replace(/^<([\w\-]+)/,
+function(a,b){return"<"+I(b)})}catch(d){return I(c)}}function $b(b){try{return decodeURIComponent(b)}catch(a){}}function ac(b){var a={},c,d;q((b||"").split("&"),function(b){b&&(c=b.split("="),d=$b(c[0]),B(d)&&(b=B(c[1])?$b(c[1]):!0,a[d]?M(a[d])?a[d].push(b):a[d]=[a[d],b]:a[d]=b))});return a}function bc(b){var a=[];q(b,function(b,d){M(b)?q(b,function(b){a.push(Aa(d,!0)+(!0===b?"":"="+Aa(b,!0)))}):a.push(Aa(d,!0)+(!0===b?"":"="+Aa(b,!0)))});return a.length?a.join("&"):""}function Bb(b){return Aa(b,
+!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function Aa(b,a){return encodeURIComponent(b).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,a?"%20":"+")}function ed(b,a){function c(a){a&&d.push(a)}var d=[b],e,g,f=["ng:app","ng-app","x-ng-app","data-ng-app"],h=/\sng[:\-]app(:\s*([\w\d_]+);?)?\s/;q(f,function(a){f[a]=!0;c(U.getElementById(a));a=a.replace(":","\\:");b.querySelectorAll&&(q(b.querySelectorAll("."+a),c),q(b.querySelectorAll("."+
+a+"\\:"),c),q(b.querySelectorAll("["+a+"]"),c))});q(d,function(a){if(!e){var b=h.exec(" "+a.className+" ");b?(e=a,g=(b[2]||"").replace(/\s+/g,",")):q(a.attributes,function(b){!e&&f[b.name]&&(e=a,g=b.value)})}});e&&a(e,g?[g]:[])}function cc(b,a){var c=function(){b=y(b);if(b.injector()){var c=b[0]===U?"document":ha(b);throw Oa("btstrpd",c);}a=a||[];a.unshift(["$provide",function(a){a.value("$rootElement",b)}]);a.unshift("ng");c=dc(a);c.invoke(["$rootScope","$rootElement","$compile","$injector","$animate",
+function(a,b,c,d,e){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},d=/^NG_DEFER_BOOTSTRAP!/;if(O&&!d.test(O.name))return c();O.name=O.name.replace(d,"");Qa.resumeBootstrap=function(b){q(b,function(b){a.push(b)});c()}}function ib(b,a){a=a||"_";return b.replace(fd,function(b,d){return(d?a:"")+b.toLowerCase()})}function Cb(b,a,c){if(!b)throw Oa("areq",a||"?",c||"required");return b}function Ra(b,a,c){c&&M(b)&&(b=b[b.length-1]);Cb(P(b),a,"not a function, got "+(b&&"object"==typeof b?
+b.constructor.name||"Object":typeof b));return b}function Ba(b,a){if("hasOwnProperty"===b)throw Oa("badname",a);}function ec(b,a,c){if(!a)return b;a=a.split(".");for(var d,e=b,g=a.length,f=0;f")+d[2];for(d=d[0];d--;)c=c.lastChild;g=g.concat(sa.call(c.childNodes,void 0));c=e.firstChild;c.textContent=""}else g.push(a.createTextNode(b));e.textContent="";e.innerHTML="";q(g,function(a){e.appendChild(a)});return e}function N(b){if(b instanceof
+N)return b;t(b)&&(b=ca(b));if(!(this instanceof N)){if(t(b)&&"<"!=b.charAt(0))throw Hb("nosel");return new N(b)}if(t(b)){var a;a=U;var c;b=(c=ve.exec(b))?[a.createElement(c[1])]:(c=se(b,a))?c.childNodes:[]}kc(this,b)}function Ib(b){return b.cloneNode(!0)}function Ia(b){lc(b);var a=0;for(b=b.childNodes||[];a=T?(c.preventDefault=
+null,c.stopPropagation=null,c.isDefaultPrevented=null):(delete c.preventDefault,delete c.stopPropagation,delete c.isDefaultPrevented)};c.elem=b;return c}function Ja(b){var a=typeof b,c;"object"==a&&null!==b?"function"==typeof(c=b.$$hashKey)?c=b.$$hashKey():c===s&&(c=b.$$hashKey=eb()):c=b;return a+":"+c}function Wa(b){q(b,this.put,this)}function sc(b){var a,c;"function"==typeof b?(a=b.$inject)||(a=[],b.length&&(c=b.toString().replace(ye,""),c=c.match(ze),q(c[1].split(Ae),function(b){b.replace(Be,function(b,
+c,d){a.push(d)})})),b.$inject=a):M(b)?(c=b.length-1,Ra(b[c],"fn"),a=b.slice(0,c)):Ra(b,"fn",!0);return a}function dc(b){function a(a){return function(b,c){if(X(b))q(b,Ub(a));else return a(b,c)}}function c(a,b){Ba(a,"service");if(P(b)||M(b))b=n.instantiate(b);if(!b.$get)throw Xa("pget",a);return l[a+h]=b}function d(a,b){return c(a,{$get:b})}function e(a){var b=[],c,d,g,h;q(a,function(a){if(!k.get(a)){k.put(a,!0);try{if(t(a))for(c=Sa(a),b=b.concat(e(c.requires)).concat(c._runBlocks),d=c._invokeQueue,
+g=0,h=d.length;g 4096 bytes)!"));else{if(m.cookie!==da)for(da=m.cookie,d=da.split("; "),Q={},g=0;gk&&this.remove(p.key),b},get:function(a){if(k<
+Number.MAX_VALUE){var b=l[a];if(!b)return;e(b)}return m[a]},remove:function(a){if(k").parent()[0])});var g=L(a,b,a,c,d,e);ma(a,"ng-scope");return function(b,c,d){Cb(b,"scope");var e=c?Ka.clone.call(a):a;q(d,function(a,b){e.data("$"+b+"Controller",a)});d=0;for(var f=e.length;darguments.length&&(b=a,a=s);A&&(c=da);return p(a,b,c)}var J,x,w,G,R,E,da={},ob;J=c===g?d:Xb(d,new Kb(y(g),
+d.$attr));x=J.$$element;if(Q){var S=/^\s*([@=&])(\??)\s*(\w*)\s*$/;f=y(g);E=e.$new(!0);ia&&ia===Q.$$originalDirective?f.data("$isolateScope",E):f.data("$isolateScopeNoTemplate",E);ma(f,"ng-isolate-scope");q(Q.scope,function(a,c){var d=a.match(S)||[],g=d[3]||c,f="?"==d[2],d=d[1],m,l,n,p;E.$$isolateBindings[c]=d+g;switch(d){case "@":J.$observe(g,function(a){E[c]=a});J.$$observers[g].$$scope=e;J[g]&&(E[c]=b(J[g])(e));break;case "=":if(f&&!J[g])break;l=r(J[g]);p=l.literal?za:function(a,b){return a===
+b};n=l.assign||function(){m=E[c]=l(e);throw ja("nonassign",J[g],Q.name);};m=E[c]=l(e);E.$watch(function(){var a=l(e);p(a,E[c])||(p(a,m)?n(e,a=E[c]):E[c]=a);return m=a},null,l.literal);break;case "&":l=r(J[g]);E[c]=function(a){return l(e,a)};break;default:throw ja("iscp",Q.name,c,a);}})}ob=p&&z;L&&q(L,function(a){var b={$scope:a===Q||a.$$isolateScope?E:e,$element:x,$attrs:J,$transclude:ob},c;R=a.controller;"@"==R&&(R=J[a.name]);c=u(R,b);da[a.name]=c;A||x.data("$"+a.name+"Controller",c);a.controllerAs&&
+(b.$scope[a.controllerAs]=c)});f=0;for(w=m.length;fF.priority)break;if(V=F.scope)G=G||F,F.templateUrl||(I("new/isolated scope",Q,F,Z),X(V)&&(Q=F));v=F.name;!F.templateUrl&&F.controller&&(V=F.controller,L=L||{},I("'"+v+"' controller",L[v],F,Z),L[v]=F);if(V=F.transclude)D=!0,F.$$tlb||(I("transclusion",S,F,Z),S=F),"element"==V?(A=!0,w=F.priority,V=E(c,T,W),Z=d.$$element=y(U.createComment(" "+v+": "+d[v]+" ")),c=Z[0],pb(g,y(sa.call(V,0)),
+c),Ya=x(V,e,w,f&&f.name,{nonTlbTranscludeDirective:S})):(V=y(Ib(c)).contents(),Z.empty(),Ya=x(V,e));if(F.template)if(I("template",ia,F,Z),ia=F,V=P(F.template)?F.template(Z,d):F.template,V=Y(V),F.replace){f=F;V=Gb.test(V)?y(V):[];c=V[0];if(1!=V.length||1!==c.nodeType)throw ja("tplrt",v,"");pb(g,Z,c);oa={$attr:{}};V=da(c,[],oa);var $=a.splice(N+1,a.length-(N+1));Q&&tc(V);a=a.concat(V).concat($);B(d,oa);oa=a.length}else Z.html(V);if(F.templateUrl)I("template",ia,F,Z),ia=F,F.replace&&(f=F),H=C(a.splice(N,
+a.length-N),Z,d,g,Ya,m,n,{controllerDirectives:L,newIsolateScopeDirective:Q,templateDirective:ia,nonTlbTranscludeDirective:S}),oa=a.length;else if(F.compile)try{O=F.compile(Z,d,Ya),P(O)?z(null,O,T,W):O&&z(O.pre,O.post,T,W)}catch(aa){l(aa,ha(Z))}F.terminal&&(H.terminal=!0,w=Math.max(w,F.priority))}H.scope=G&&!0===G.scope;H.transclude=D&&Ya;p.hasElementTranscludeDirective=A;return H}function tc(a){for(var b=0,c=a.length;bp.priority)&&-1!=p.restrict.indexOf(g)&&(n&&(p=Wb(p,{$$start:n,$$end:r})),b.push(p),k=p)}catch(K){l(K)}}return k}function B(a,b){var c=b.$attr,d=a.$attr,e=a.$$element;q(a,function(d,e){"$"!=e.charAt(0)&&(b[e]&&(d+=("style"===e?";":" ")+b[e]),a.$set(e,d,!0,c[e]))});q(b,function(b,g){"class"==g?(ma(e,b),a["class"]=(a["class"]?a["class"]+" ":"")+b):"style"==g?(e.attr("style",e.attr("style")+";"+b),a.style=
+(a.style?a.style+";":"")+b):"$"==g.charAt(0)||a.hasOwnProperty(g)||(a[g]=b,d[g]=c[g])})}function C(a,b,c,d,e,g,f,k){var m=[],l,r,u=b[0],z=a.shift(),K=A({},z,{templateUrl:null,transclude:null,replace:null,$$originalDirective:z}),x=P(z.templateUrl)?z.templateUrl(b,c):z.templateUrl;b.empty();n.get(w.getTrustedResourceUrl(x),{cache:p}).success(function(n){var p,H;n=Y(n);if(z.replace){n=Gb.test(n)?y(n):[];p=n[0];if(1!=n.length||1!==p.nodeType)throw ja("tplrt",z.name,x);n={$attr:{}};pb(d,b,p);var w=da(p,
+[],n);X(z.scope)&&tc(w);a=w.concat(a);B(c,n)}else p=u,b.html(n);a.unshift(K);l=ia(a,p,c,e,b,z,g,f,k);q(d,function(a,c){a==p&&(d[c]=b[0])});for(r=L(b[0].childNodes,e);m.length;){n=m.shift();H=m.shift();var G=m.shift(),R=m.shift(),w=b[0];if(H!==u){var E=H.className;k.hasElementTranscludeDirective&&z.replace||(w=Ib(p));pb(G,y(H),w);ma(y(w),E)}H=l.transclude?Q(n,l.transclude):R;l(r,n,w,d,H)}m=null}).error(function(a,b,c,d){throw ja("tpload",d.url);});return function(a,b,c,d,e){m?(m.push(b),m.push(c),
+m.push(d),m.push(e)):l(r,b,c,d,e)}}function D(a,b){var c=b.priority-a.priority;return 0!==c?c:a.name!==b.name?a.namea.status?b:n.reject(b)}var d={method:"get",
+transformRequest:e.transformRequest,transformResponse:e.transformResponse},g=function(a){function b(a){var c;q(a,function(b,d){P(b)&&(c=b(),null!=c?a[d]=c:delete a[d])})}var c=e.headers,d=A({},a.headers),g,f,c=A({},c.common,c[I(a.method)]);b(c);b(d);a:for(g in c){a=I(g);for(f in d)if(I(f)===a)continue a;d[g]=c[g]}return d}(a);A(d,a);d.headers=g;d.method=Ga(d.method);(a=Lb(d.url)?b.cookies()[d.xsrfCookieName||e.xsrfCookieName]:s)&&(g[d.xsrfHeaderName||e.xsrfHeaderName]=a);var f=[function(a){g=a.headers;
+var b=yc(a.data,xc(g),a.transformRequest);D(a.data)&&q(g,function(a,b){"content-type"===I(b)&&delete g[b]});D(a.withCredentials)&&!D(e.withCredentials)&&(a.withCredentials=e.withCredentials);return u(a,b,g).then(c,c)},s],h=n.when(d);for(q(w,function(a){(a.request||a.requestError)&&f.unshift(a.request,a.requestError);(a.response||a.responseError)&&f.push(a.response,a.responseError)});f.length;){a=f.shift();var k=f.shift(),h=h.then(a,k)}h.success=function(a){h.then(function(b){a(b.data,b.status,b.headers,
+d)});return h};h.error=function(a){h.then(null,function(b){a(b.data,b.status,b.headers,d)});return h};return h}function u(b,c,g){function f(a,b,c,e){w&&(200<=a&&300>a?w.put(s,[a,b,wc(c),e]):w.remove(s));m(b,a,c,e);d.$$phase||d.$apply()}function m(a,c,d,e){c=Math.max(c,0);(200<=c&&300>c?p.resolve:p.reject)({data:a,status:c,headers:xc(d),config:b,statusText:e})}function k(){var a=gb(r.pendingRequests,b);-1!==a&&r.pendingRequests.splice(a,1)}var p=n.defer(),u=p.promise,w,q,s=z(b.url,b.params);r.pendingRequests.push(b);
+u.then(k,k);(b.cache||e.cache)&&(!1!==b.cache&&"GET"==b.method)&&(w=X(b.cache)?b.cache:X(e.cache)?e.cache:K);if(w)if(q=w.get(s),B(q)){if(q.then)return q.then(k,k),q;M(q)?m(q[1],q[0],ba(q[2]),q[3]):m(q,200,{},"OK")}else w.put(s,u);D(q)&&a(b.method,s,c,f,g,b.timeout,b.withCredentials,b.responseType);return u}function z(a,b){if(!b)return a;var c=[];ad(b,function(a,b){null===a||D(a)||(M(a)||(a=[a]),q(a,function(a){X(a)&&(a=ta(a));c.push(Aa(b)+"="+Aa(a))}))});0=T&&(!b.match(/^(get|post|head|put|delete|options)$/i)||!O.XMLHttpRequest))return new O.ActiveXObject("Microsoft.XMLHTTP");if(O.XMLHttpRequest)return new O.XMLHttpRequest;throw v("$httpBackend")("noxhr");}function ce(){this.$get=["$browser","$window","$document",function(b,a,c){return Fe(b,Ee,b.defer,a.angular.callbacks,c[0])}]}function Fe(b,a,c,d,e){function g(a,b,c){var g=e.createElement("script"),f=null;g.type="text/javascript";g.src=a;g.async=
+!0;f=function(a){Ua(g,"load",f);Ua(g,"error",f);e.body.removeChild(g);g=null;var h=-1,u="unknown";a&&("load"!==a.type||d[b].called||(a={type:"error"}),u=a.type,h="error"===a.type?404:200);c&&c(h,u)};qb(g,"load",f);qb(g,"error",f);e.body.appendChild(g);return f}var f=-1;return function(e,m,k,l,n,p,r,u){function z(){w=f;G&&G();x&&x.abort()}function K(a,d,e,g,f){L&&c.cancel(L);G=x=null;0===d&&(d=e?200:"file"==ua(m).protocol?404:0);a(1223===d?204:d,e,g,f||"");b.$$completeOutstandingRequest(C)}var w;b.$$incOutstandingRequestCount();
+m=m||b.url();if("jsonp"==I(e)){var H="_"+(d.counter++).toString(36);d[H]=function(a){d[H].data=a;d[H].called=!0};var G=g(m.replace("JSON_CALLBACK","angular.callbacks."+H),H,function(a,b){K(l,a,d[H].data,"",b);d[H]=C})}else{var x=a(e);x.open(e,m,!0);q(n,function(a,b){B(a)&&x.setRequestHeader(b,a)});x.onreadystatechange=function(){if(x&&4==x.readyState){var a=null,b=null;w!==f&&(a=x.getAllResponseHeaders(),b="response"in x?x.response:x.responseText);K(l,w||x.status,b,a,x.statusText||"")}};r&&(x.withCredentials=
+!0);if(u)try{x.responseType=u}catch(s){if("json"!==u)throw s;}x.send(k||null)}if(0=h&&(n.resolve(r),l(p.$$intervalId),delete e[p.$$intervalId]);u||b.$apply()},f);e[p.$$intervalId]=n;return p}var e={};d.cancel=function(a){return a&&a.$$intervalId in e?(e[a.$$intervalId].reject("canceled"),clearInterval(a.$$intervalId),delete e[a.$$intervalId],
+!0):!1};return d}]}function jd(){this.$get=function(){return{id:"en-us",NUMBER_FORMATS:{DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{minInt:1,minFrac:0,maxFrac:3,posPre:"",posSuf:"",negPre:"-",negSuf:"",gSize:3,lgSize:3},{minInt:1,minFrac:2,maxFrac:2,posPre:"\u00a4",posSuf:"",negPre:"(\u00a4",negSuf:")",gSize:3,lgSize:3}],CURRENCY_SYM:"$"},DATETIME_FORMATS:{MONTH:"January February March April May June July August September October November December".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),
+DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),AMPMS:["AM","PM"],medium:"MMM d, y h:mm:ss a","short":"M/d/yy h:mm a",fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",mediumDate:"MMM d, y",shortDate:"M/d/yy",mediumTime:"h:mm:ss a",shortTime:"h:mm a"},pluralCat:function(b){return 1===b?"one":"other"}}}}function Ac(b){b=b.split("/");for(var a=b.length;a--;)b[a]=Bb(b[a]);return b.join("/")}function Bc(b,a,c){b=ua(b,c);a.$$protocol=
+b.protocol;a.$$host=b.hostname;a.$$port=Y(b.port)||Ge[b.protocol]||null}function Cc(b,a,c){var d="/"!==b.charAt(0);d&&(b="/"+b);b=ua(b,c);a.$$path=decodeURIComponent(d&&"/"===b.pathname.charAt(0)?b.pathname.substring(1):b.pathname);a.$$search=ac(b.search);a.$$hash=decodeURIComponent(b.hash);a.$$path&&"/"!=a.$$path.charAt(0)&&(a.$$path="/"+a.$$path)}function pa(b,a){if(0===a.indexOf(b))return a.substr(b.length)}function Za(b){var a=b.indexOf("#");return-1==a?b:b.substr(0,a)}function Mb(b){return b.substr(0,
+Za(b).lastIndexOf("/")+1)}function Dc(b,a){this.$$html5=!0;a=a||"";var c=Mb(b);Bc(b,this,b);this.$$parse=function(a){var e=pa(c,a);if(!t(e))throw Nb("ipthprfx",a,c);Cc(e,this,b);this.$$path||(this.$$path="/");this.$$compose()};this.$$compose=function(){var a=bc(this.$$search),b=this.$$hash?"#"+Bb(this.$$hash):"";this.$$url=Ac(this.$$path)+(a?"?"+a:"")+b;this.$$absUrl=c+this.$$url.substr(1)};this.$$rewrite=function(d){var e;if((e=pa(b,d))!==s)return d=e,(e=pa(a,e))!==s?c+(pa("/",e)||e):b+d;if((e=pa(c,
+d))!==s)return c+e;if(c==d+"/")return c}}function Ob(b,a){var c=Mb(b);Bc(b,this,b);this.$$parse=function(d){var e=pa(b,d)||pa(c,d),e="#"==e.charAt(0)?pa(a,e):this.$$html5?e:"";if(!t(e))throw Nb("ihshprfx",d,a);Cc(e,this,b);d=this.$$path;var g=/^\/?.*?:(\/.*)/;0===e.indexOf(b)&&(e=e.replace(b,""));g.exec(e)||(d=(e=g.exec(d))?e[1]:d);this.$$path=d;this.$$compose()};this.$$compose=function(){var c=bc(this.$$search),e=this.$$hash?"#"+Bb(this.$$hash):"";this.$$url=Ac(this.$$path)+(c?"?"+c:"")+e;this.$$absUrl=
+b+(this.$$url?a+this.$$url:"")};this.$$rewrite=function(a){if(Za(b)==Za(a))return a}}function Ec(b,a){this.$$html5=!0;Ob.apply(this,arguments);var c=Mb(b);this.$$rewrite=function(d){var e;if(b==Za(d))return d;if(e=pa(c,d))return b+a+e;if(c===d+"/")return c}}function rb(b){return function(){return this[b]}}function Fc(b,a){return function(c){if(D(c))return this[b];this[b]=a(c);this.$$compose();return this}}function de(){var b="",a=!1;this.hashPrefix=function(a){return B(a)?(b=a,this):b};this.html5Mode=
+function(b){return B(b)?(a=b,this):a};this.$get=["$rootScope","$browser","$sniffer","$rootElement",function(c,d,e,g){function f(a){c.$broadcast("$locationChangeSuccess",h.absUrl(),a)}var h,m=d.baseHref(),k=d.url();a?(m=k.substring(0,k.indexOf("/",k.indexOf("//")+2))+(m||"/"),e=e.history?Dc:Ec):(m=Za(k),e=Ob);h=new e(m,"#"+b);h.$$parse(h.$$rewrite(k));g.on("click",function(a){if(!a.ctrlKey&&!a.metaKey&&2!=a.which){for(var b=y(a.target);"a"!==I(b[0].nodeName);)if(b[0]===g[0]||!(b=b.parent())[0])return;
+var e=b.prop("href");X(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=ua(e.animVal).href);var f=h.$$rewrite(e);e&&(!b.attr("target")&&f&&!a.isDefaultPrevented())&&(a.preventDefault(),f!=d.url()&&(h.$$parse(f),c.$apply(),O.angular["ff-684208-preventDefault"]=!0))}});h.absUrl()!=k&&d.url(h.absUrl(),!0);d.onUrlChange(function(a){h.absUrl()!=a&&(c.$evalAsync(function(){var b=h.absUrl();h.$$parse(a);c.$broadcast("$locationChangeStart",a,b).defaultPrevented?(h.$$parse(b),d.url(b)):f(b)}),c.$$phase||
+c.$digest())});var l=0;c.$watch(function(){var a=d.url(),b=h.$$replace;l&&a==h.absUrl()||(l++,c.$evalAsync(function(){c.$broadcast("$locationChangeStart",h.absUrl(),a).defaultPrevented?h.$$parse(a):(d.url(h.absUrl(),b),f(a))}));h.$$replace=!1;return l});return h}]}function ee(){var b=!0,a=this;this.debugEnabled=function(a){return B(a)?(b=a,this):b};this.$get=["$window",function(c){function d(a){a instanceof Error&&(a.stack?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:
+a.stack:a.sourceURL&&(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=c.console||{},e=b[a]||b.log||C;a=!1;try{a=!!e.apply}catch(m){}return a?function(){var a=[];q(arguments,function(b){a.push(d(b))});return e.apply(b,a)}:function(a,b){e(a,null==b?"":b)}}return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){b&&c.apply(a,arguments)}}()}}]}function fa(b,a){if("constructor"===b)throw Ca("isecfld",a);return b}function $a(b,
+a){if(b){if(b.constructor===b)throw Ca("isecfn",a);if(b.document&&b.location&&b.alert&&b.setInterval)throw Ca("isecwindow",a);if(b.children&&(b.nodeName||b.prop&&b.attr&&b.find))throw Ca("isecdom",a);}return b}function sb(b,a,c,d,e){e=e||{};a=a.split(".");for(var g,f=0;1e?Gc(d[0],d[1],d[2],d[3],d[4],c,a):function(b,g){var f=0,h;do h=Gc(d[f++],d[f++],d[f++],d[f++],d[f++],c,a)(b,g),g=s,b=h;while(fa)for(b in k++,e)e.hasOwnProperty(b)&&!d.hasOwnProperty(b)&&(q--,delete e[b])}else e!==d&&(e=d,k++);return k},function(){p?(p=!1,b(d,d,c)):b(d,f,c);if(h)if(X(d))if(db(d)){f=Array(d.length);for(var a=0;as&&(y=4-s,Q[y]||(Q[y]=[]),E=P(d.exp)?"fn: "+(d.exp.name||d.exp.toString()):d.exp,E+="; newVal: "+ta(g)+"; oldVal: "+ta(f),Q[y].push(E));else if(d===c){x=!1;break a}}catch(t){p.$$phase=
+null,e(t)}if(!(h=L.$$childHead||L!==this&&L.$$nextSibling))for(;L!==this&&!(h=L.$$nextSibling);)L=L.$parent}while(L=h);if((x||k.length)&&!s--)throw p.$$phase=null,a("infdig",b,ta(Q));}while(x||k.length);for(p.$$phase=null;l.length;)try{l.shift()()}catch(S){e(S)}},$destroy:function(){if(!this.$$destroyed){var a=this.$parent;this.$broadcast("$destroy");this.$$destroyed=!0;this!==p&&(q(this.$$listenerCount,hb(null,l,this)),a.$$childHead==this&&(a.$$childHead=this.$$nextSibling),a.$$childTail==this&&
+(a.$$childTail=this.$$prevSibling),this.$$prevSibling&&(this.$$prevSibling.$$nextSibling=this.$$nextSibling),this.$$nextSibling&&(this.$$nextSibling.$$prevSibling=this.$$prevSibling),this.$parent=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=this.$root=null,this.$$listeners={},this.$$watchers=this.$$asyncQueue=this.$$postDigestQueue=[],this.$destroy=this.$digest=this.$apply=C,this.$on=this.$watch=function(){return C})}},$eval:function(a,b){return g(a)(this,b)},$evalAsync:function(a){p.$$phase||
+p.$$asyncQueue.length||f.defer(function(){p.$$asyncQueue.length&&p.$digest()});this.$$asyncQueue.push({scope:this,expression:a})},$$postDigest:function(a){this.$$postDigestQueue.push(a)},$apply:function(a){try{return m("$apply"),this.$eval(a)}catch(b){e(b)}finally{p.$$phase=null;try{p.$digest()}catch(c){throw e(c),c;}}},$on:function(a,b){var c=this.$$listeners[a];c||(this.$$listeners[a]=c=[]);c.push(b);var d=this;do d.$$listenerCount[a]||(d.$$listenerCount[a]=0),d.$$listenerCount[a]++;while(d=d.$parent);
+var e=this;return function(){c[gb(c,b)]=null;l(e,1,a)}},$emit:function(a,b){var c=[],d,g=this,f=!1,h={name:a,targetScope:g,stopPropagation:function(){f=!0},preventDefault:function(){h.defaultPrevented=!0},defaultPrevented:!1},k=[h].concat(sa.call(arguments,1)),m,l;do{d=g.$$listeners[a]||c;h.currentScope=g;m=0;for(l=d.length;mc.msieDocumentMode)throw wa("iequirks");var e=ba(ga);e.isEnabled=function(){return b};e.trustAs=d.trustAs;e.getTrusted=d.getTrusted;e.valueOf=d.valueOf;b||(e.trustAs=e.getTrusted=function(a,b){return b},e.valueOf=Ea);e.parseAs=function(b,c){var d=a(c);return d.literal&&d.constant?d:function(a,c){return e.getTrusted(b,
+d(a,c))}};var g=e.parseAs,f=e.getTrusted,h=e.trustAs;q(ga,function(a,b){var c=I(b);e[Ta("parse_as_"+c)]=function(b){return g(a,b)};e[Ta("get_trusted_"+c)]=function(b){return f(a,b)};e[Ta("trust_as_"+c)]=function(b){return h(a,b)}});return e}]}function ke(){this.$get=["$window","$document",function(b,a){var c={},d=Y((/android (\d+)/.exec(I((b.navigator||{}).userAgent))||[])[1]),e=/Boxee/i.test((b.navigator||{}).userAgent),g=a[0]||{},f=g.documentMode,h,m=/^(Moz|webkit|O|ms)(?=[A-Z])/,k=g.body&&g.body.style,
+l=!1,n=!1;if(k){for(var p in k)if(l=m.exec(p)){h=l[0];h=h.substr(0,1).toUpperCase()+h.substr(1);break}h||(h="WebkitOpacity"in k&&"webkit");l=!!("transition"in k||h+"Transition"in k);n=!!("animation"in k||h+"Animation"in k);!d||l&&n||(l=t(g.body.style.webkitTransition),n=t(g.body.style.webkitAnimation))}return{history:!(!b.history||!b.history.pushState||4>d||e),hashchange:"onhashchange"in b&&(!f||7b;b=Math.abs(b);var f=b+"",h="",m=[],k=!1;if(-1!==f.indexOf("e")){var l=f.match(/([\d\.]+)e(-?)(\d+)/);l&&"-"==l[2]&&l[3]>e+1?f="0":(h=f,k=!0)}if(k)0b)&&(h=b.toFixed(e));
+else{f=(f.split(Rc)[1]||"").length;D(e)&&(e=Math.min(Math.max(a.minFrac,f),a.maxFrac));f=Math.pow(10,e);b=Math.round(b*f)/f;b=(""+b).split(Rc);f=b[0];b=b[1]||"";var l=0,n=a.lgSize,p=a.gSize;if(f.length>=n+p)for(l=f.length-n,k=0;kb&&(d="-",b=-b);for(b=""+b;b.length-c)e+=c;0===e&&-12==c&&(e=12);return tb(e,a,d)}}function ub(b,a){return function(c,d){var e=c["get"+b](),g=Ga(a?"SHORT"+b:b);return d[g][e]}}function Sc(b){var a=(new Date(b,0,1)).getDay();return new Date(b,0,(4>=a?5:12)-a)}function Tc(b){return function(a){var c=Sc(a.getFullYear());a=+new Date(a.getFullYear(),a.getMonth(),a.getDate()+
+(4-a.getDay()))-+c;a=1+Math.round(a/6048E5);return tb(a,b)}}function Nc(b){function a(a){var b;if(b=a.match(c)){a=new Date(0);var g=0,f=0,h=b[8]?a.setUTCFullYear:a.setFullYear,m=b[8]?a.setUTCHours:a.setHours;b[9]&&(g=Y(b[9]+b[10]),f=Y(b[9]+b[11]));h.call(a,Y(b[1]),Y(b[2])-1,Y(b[3]));g=Y(b[4]||0)-g;f=Y(b[5]||0)-f;h=Y(b[6]||0);b=Math.round(1E3*parseFloat("0."+(b[7]||0)));m.call(a,g,f,h,b)}return a}var c=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;
+return function(c,e){var g="",f=[],h,m;e=e||"mediumDate";e=b.DATETIME_FORMATS[e]||e;t(c)&&(c=Qe.test(c)?Y(c):a(c));Ab(c)&&(c=new Date(c));if(!ra(c))return c;for(;e;)(m=Re.exec(e))?(f=f.concat(sa.call(m,1)),e=f.pop()):(f.push(e),e=null);q(f,function(a){h=Se[a];g+=h?h(c,b.DATETIME_FORMATS):a.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function Me(){return function(b){return ta(b,!0)}}function Ne(){return function(b,a){if(!M(b)&&!t(b))return b;a=Y(a);if(t(b))return a?0<=a?b.slice(0,a):b.slice(a,
+b.length):"";var c=[],d,e;a>b.length?a=b.length:a<-b.length&&(a=-b.length);0a||37<=a&&40>=a)||l()});if(e.hasEvent("paste"))a.on("paste cut",l)}a.on("change",m);d.$render=function(){a.val(d.$isEmpty(d.$viewValue)?"":d.$viewValue)};var n=c.ngPattern;n&&((e=n.match(/^\/(.*)\/([gim]*)$/))?(n=RegExp(e[1],e[2]),e=function(a){return qa(d,"pattern",d.$isEmpty(a)||n.test(a),a)}):e=function(c){var e=b.$eval(n);if(!e||!e.test)throw v("ngPattern")("noregexp",n,
+e,ha(a));return qa(d,"pattern",d.$isEmpty(c)||e.test(c),c)},d.$formatters.push(e),d.$parsers.push(e));if(c.ngMinlength){var p=Y(c.ngMinlength);e=function(a){return qa(d,"minlength",d.$isEmpty(a)||a.length>=p,a)};d.$parsers.push(e);d.$formatters.push(e)}if(c.ngMaxlength){var r=Y(c.ngMaxlength);e=function(a){return qa(d,"maxlength",d.$isEmpty(a)||a.length<=r,a)};d.$parsers.push(e);d.$formatters.push(e)}}function zb(b,a){return function(c){var d;return ra(c)?c:t(c)&&(b.lastIndex=0,c=b.exec(c))?(c.shift(),
+d={yyyy:0,MM:1,dd:1,HH:0,mm:0},q(c,function(b,c){c=c(f.min);h.$setValidity("min",b);return b?a:
+s},h.$parsers.push(e),h.$formatters.push(e));f.max&&(e=function(a){var b=h.$isEmpty(a)||c(a)<=c(f.max);h.$setValidity("max",b);return b?a:s},h.$parsers.push(e),h.$formatters.push(e))}}function Rb(b,a){b="ngClass"+b;return["$animate",function(c){function d(a,b){var c=[],d=0;a:for(;dT?function(b){b=b.nodeName?b:b[0];return b.scopeName&&"HTML"!=b.scopeName?Ga(b.scopeName+":"+b.nodeName):b.nodeName}:function(b){return b.nodeName?b.nodeName:b[0].nodeName};var fd=/[A-Z]/g,id={full:"1.3.0-beta.5",major:1,minor:3,dot:0,codeName:"chimeric-glitterfication"},Va=N.cache={},jb=N.expando="ng-"+
+(new Date).getTime(),we=1,qb=O.document.addEventListener?function(b,a,c){b.addEventListener(a,c,!1)}:function(b,a,c){b.attachEvent("on"+a,c)},Ua=O.document.removeEventListener?function(b,a,c){b.removeEventListener(a,c,!1)}:function(b,a,c){b.detachEvent("on"+a,c)};N._data=function(b){return this.cache[b[this.expando]]||{}};var qe=/([\:\-\_]+(.))/g,re=/^moz([A-Z])/,Hb=v("jqLite"),ve=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,Gb=/<|&#?\w+;/,te=/<([\w:]+)/,ue=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
+ea={option:[1,'"],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ea.optgroup=ea.option;ea.tbody=ea.tfoot=ea.colgroup=ea.caption=ea.thead;ea.th=ea.td;var Ka=N.prototype={ready:function(b){function a(){c||(c=!0,b())}var c=!1;"complete"===U.readyState?setTimeout(a):(this.on("DOMContentLoaded",a),N(O).on("load",a))}, +toString:function(){var b=[];q(this,function(a){b.push(""+a)});return"["+b.join(", ")+"]"},eq:function(b){return 0<=b?y(this[b]):y(this[this.length+b])},length:0,push:Ue,sort:[].sort,splice:[].splice},nb={};q("multiple selected checked disabled readOnly required open".split(" "),function(b){nb[I(b)]=b});var rc={};q("input select option textarea button form details".split(" "),function(b){rc[Ga(b)]=!0});q({data:nc,inheritedData:mb,scope:function(b){return y(b).data("$scope")||mb(b.parentNode||b,["$isolateScope", +"$scope"])},isolateScope:function(b){return y(b).data("$isolateScope")||y(b).data("$isolateScopeNoTemplate")},controller:oc,injector:function(b){return mb(b,"$injector")},removeAttr:function(b,a){b.removeAttribute(a)},hasClass:Jb,css:function(b,a,c){a=Ta(a);if(B(c))b.style[a]=c;else{var d;8>=T&&(d=b.currentStyle&&b.currentStyle[a],""===d&&(d="auto"));d=d||b.style[a];8>=T&&(d=""===d?s:d);return d}},attr:function(b,a,c){var d=I(a);if(nb[d])if(B(c))c?(b[a]=!0,b.setAttribute(a,d)):(b[a]=!1,b.removeAttribute(d)); +else return b[a]||(b.attributes.getNamedItem(a)||C).specified?d:s;else if(B(c))b.setAttribute(a,c);else if(b.getAttribute)return b=b.getAttribute(a,2),null===b?s:b},prop:function(b,a,c){if(B(c))b[a]=c;else return b[a]},text:function(){function b(b,d){var e=a[b.nodeType];if(D(d))return e?b[e]:"";b[e]=d}var a=[];9>T?(a[1]="innerText",a[3]="nodeValue"):a[1]=a[3]="textContent";b.$dv="";return b}(),val:function(b,a){if(D(a)){if("SELECT"===La(b)&&b.multiple){var c=[];q(b.options,function(a){a.selected&& +c.push(a.value||a.text)});return 0===c.length?null:c}return b.value}b.value=a},html:function(b,a){if(D(a))return b.innerHTML;for(var c=0,d=b.childNodes;c":function(a,c,d,e){return d(a,c)>e(a,c)},"<=":function(a,c,d,e){return d(a,c)<=e(a,c)},">=":function(a,c,d,e){return d(a,c)>=e(a,c)},"&&":function(a,c,d,e){return d(a,c)&&e(a,c)},"||":function(a,c,d,e){return d(a,c)||e(a,c)},"&":function(a,c,d,e){return d(a,c)&e(a,c)},"|":function(a,c,d,e){return e(a,c)(a,c,d(a,c))},"!":function(a,c,d){return!d(a,c)}},Xe={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'}, +Qb=function(a){this.options=a};Qb.prototype={constructor:Qb,lex:function(a){this.text=a;this.index=0;this.ch=s;this.lastCh=":";this.tokens=[];var c;for(a=[];this.index=a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"=== +a},isIdent:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,c,d){d=d||this.index;c=B(c)?"s "+c+"-"+this.index+" ["+this.text.substring(c,d)+"]":" "+d;throw Ca("lexerr",a,c,this.text);},readNumber:function(){for(var a="",c=this.index;this.index","<=",">="))a=this.binaryFn(a,c.fn,this.relational());return a},additive:function(){for(var a=this.multiplicative(),c;c=this.expect("+","-");)a=this.binaryFn(a,c.fn,this.multiplicative());return a},multiplicative:function(){for(var a=this.unary(),c;c=this.expect("*","/","%");)a=this.binaryFn(a,c.fn,this.unary());return a},unary:function(){var a;return this.expect("+")?this.primary():(a=this.expect("-"))?this.binaryFn(ab.ZERO,a.fn, +this.unary()):(a=this.expect("!"))?this.unaryFn(a.fn,this.unary()):this.primary()},fieldAccess:function(a){var c=this,d=this.expect().text,e=Hc(d,this.options,this.text);return A(function(c,d,h){return e(h||a(c,d))},{assign:function(e,f,h){return sb(a(e,h),d,f,c.text,c.options)}})},objectIndex:function(a){var c=this,d=this.expression();this.consume("]");return A(function(e,g){var f=a(e,g),h=d(e,g),m;if(!f)return s;(f=$a(f[h],c.text))&&(f.then&&c.options.unwrapPromises)&&(m=f,"$$v"in f||(m.$$v=s,m.then(function(a){m.$$v= +a})),f=f.$$v);return f},{assign:function(e,g,f){var h=d(e,f);return $a(a(e,f),c.text)[h]=g}})},functionCall:function(a,c){var d=[];if(")"!==this.peekToken().text){do d.push(this.expression());while(this.expect(","))}this.consume(")");var e=this;return function(g,f){for(var h=[],m=c?c(g,f):g,k=0;ka.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<=a?"+":"")+(tb(Math[0=T&&(c.href||c.name||c.$set("href",""),a.append(U.createComment("IE fix")));if(!c.href&&!c.xlinkHref&&!c.name)return function(a,c){var g="[object SVGAnimatedString]"===ya.call(c.prop("href"))?"xlink:href":"href";c.on("click",function(a){c.attr(g)||a.preventDefault()})}}}),Eb={};q(nb,function(a,c){if("multiple"!=a){var d=na("ng-"+c);Eb[d]=function(){return{priority:100,link:function(a,g,f){a.$watch(f[d],function(a){f.$set(c, +!!a)})}}}}});q(["src","srcset","href"],function(a){var c=na("ng-"+a);Eb[c]=function(){return{priority:99,link:function(d,e,g){var f=a,h=a;"href"===a&&"[object SVGAnimatedString]"===ya.call(e.prop("href"))&&(h="xlinkHref",g.$attr[h]="xlink:href",f=null);g.$observe(c,function(a){a&&(g.$set(h,a),T&&f&&e.prop(f,g[h]))})}}}});var xb={$addControl:C,$removeControl:C,$setValidity:C,$setDirty:C,$setPristine:C};Uc.$inject=["$element","$attrs","$scope","$animate"];var Vc=function(a){return["$timeout",function(c){return{name:"form", +restrict:a?"EAC":"E",controller:Uc,compile:function(){return{pre:function(a,e,g,f){if(!g.action){var h=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1};qb(e[0],"submit",h);e.on("$destroy",function(){c(function(){Ua(e[0],"submit",h)},0,!1)})}var m=e.parent().controller("form"),k=g.name||g.ngForm;k&&sb(a,k,f,k);if(m)e.on("$destroy",function(){m.$removeControl(f);k&&sb(a,k,s,k);A(f,xb)})}}}}}]},md=Vc(),zd=Vc(!0),Ye=/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/, +Ze=/^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i,$e=/^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/,Wc=/^(\d{4})-(\d{2})-(\d{2})$/,Xc=/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)$/,Sb=/^(\d{4})-W(\d\d)$/,Yc=/^(\d{4})-(\d\d)$/,Zc=/^(\d\d):(\d\d)$/,$c={text:bb,date:cb("date",Wc,zb(Wc,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":cb("datetimelocal",Xc,zb(Xc,["yyyy","MM","dd","HH","mm"]),"yyyy-MM-ddTHH:mm"),time:cb("time",Zc,zb(Zc,["HH","mm"]),"HH:mm"),week:cb("week",Sb,function(a){if(ra(a))return a; +if(t(a)){Sb.lastIndex=0;var c=Sb.exec(a);if(c){a=+c[1];var d=+c[2],c=Sc(a),d=7*(d-1);return new Date(a,0,c.getDate()+d)}}return NaN},"yyyy-Www"),month:cb("month",Yc,zb(Yc,["yyyy","MM"]),"yyyy-MM"),number:function(a,c,d,e,g,f){bb(a,c,d,e,g,f);e.$parsers.push(function(a){var c=e.$isEmpty(a);if(c||$e.test(a))return e.$setValidity("number",!0),""===a?null:c?a:parseFloat(a);e.$setValidity("number",!1);return s});Te(e,"number",c);e.$formatters.push(function(a){return e.$isEmpty(a)?"":""+a});d.min&&(a=function(a){var c= +parseFloat(d.min);return qa(e,"min",e.$isEmpty(a)||a>=c,a)},e.$parsers.push(a),e.$formatters.push(a));d.max&&(a=function(a){var c=parseFloat(d.max);return qa(e,"max",e.$isEmpty(a)||a<=c,a)},e.$parsers.push(a),e.$formatters.push(a));e.$formatters.push(function(a){return qa(e,"number",e.$isEmpty(a)||Ab(a),a)})},url:function(a,c,d,e,g,f){bb(a,c,d,e,g,f);a=function(a){return qa(e,"url",e.$isEmpty(a)||Ye.test(a),a)};e.$formatters.push(a);e.$parsers.push(a)},email:function(a,c,d,e,g,f){bb(a,c,d,e,g,f); +a=function(a){return qa(e,"email",e.$isEmpty(a)||Ze.test(a),a)};e.$formatters.push(a);e.$parsers.push(a)},radio:function(a,c,d,e){D(d.name)&&c.attr("name",eb());c.on("click",function(){c[0].checked&&a.$apply(function(){e.$setViewValue(d.value)})});e.$render=function(){c[0].checked=d.value==e.$viewValue};d.$observe("value",e.$render)},checkbox:function(a,c,d,e){var g=d.ngTrueValue,f=d.ngFalseValue;t(g)||(g=!0);t(f)||(f=!1);c.on("click",function(){a.$apply(function(){e.$setViewValue(c[0].checked)})}); +e.$render=function(){c[0].checked=e.$viewValue};e.$isEmpty=function(a){return a!==g};e.$formatters.push(function(a){return a===g});e.$parsers.push(function(a){return a?g:f})},hidden:C,button:C,submit:C,reset:C,file:C},gc=["$browser","$sniffer","$filter",function(a,c,d){return{restrict:"E",require:"?ngModel",link:function(e,g,f,h){h&&($c[I(f.type)]||$c.text)(e,g,f,h,c,a,d)}}}],wb="ng-valid",vb="ng-invalid",Ma="ng-pristine",yb="ng-dirty",af=["$scope","$exceptionHandler","$attrs","$element","$parse", +"$animate",function(a,c,d,e,g,f){function h(a,c){c=c?"-"+ib(c,"-"):"";f.removeClass(e,(a?vb:wb)+c);f.addClass(e,(a?wb:vb)+c)}this.$modelValue=this.$viewValue=Number.NaN;this.$parsers=[];this.$formatters=[];this.$viewChangeListeners=[];this.$pristine=!0;this.$dirty=!1;this.$valid=!0;this.$invalid=!1;this.$name=d.name;var m=g(d.ngModel),k=m.assign;if(!k)throw v("ngModel")("nonassign",d.ngModel,ha(e));this.$render=C;this.$isEmpty=function(a){return D(a)||""===a||null===a||a!==a};var l=e.inheritedData("$formController")|| +xb,n=0,p=this.$error={};e.addClass(Ma);h(!0);this.$setValidity=function(a,c){p[a]!==!c&&(c?(p[a]&&n--,n||(h(!0),this.$valid=!0,this.$invalid=!1)):(h(!1),this.$invalid=!0,this.$valid=!1,n++),p[a]=!c,h(c,a),l.$setValidity(a,c,this))};this.$setPristine=function(){this.$dirty=!1;this.$pristine=!0;f.removeClass(e,yb);f.addClass(e,Ma)};this.$setViewValue=function(d){this.$viewValue=d;this.$pristine&&(this.$dirty=!0,this.$pristine=!1,f.removeClass(e,Ma),f.addClass(e,yb),l.$setDirty());q(this.$parsers,function(a){d= +a(d)});this.$modelValue!==d&&(this.$modelValue=d,k(a,d),q(this.$viewChangeListeners,function(a){try{a()}catch(d){c(d)}}))};var r=this;a.$watch(function(){var c=m(a);if(r.$modelValue!==c){var d=r.$formatters,e=d.length;for(r.$modelValue=c;e--;)c=d[e](c);r.$viewValue!==c&&(r.$viewValue=c,r.$render())}return c})}],Od=function(){return{require:["ngModel","^?form"],controller:af,link:function(a,c,d,e){var g=e[0],f=e[1]||xb;f.$addControl(g);a.$on("$destroy",function(){f.$removeControl(g)})}}},Qd=aa({require:"ngModel", +link:function(a,c,d,e){e.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),hc=function(){return{require:"?ngModel",link:function(a,c,d,e){if(e){d.required=!0;var g=function(a){if(d.required&&e.$isEmpty(a))e.$setValidity("required",!1);else return e.$setValidity("required",!0),a};e.$formatters.push(g);e.$parsers.unshift(g);d.$observe("required",function(){g(e.$viewValue)})}}}},Pd=function(){return{require:"ngModel",link:function(a,c,d,e){var g=(a=/\/(.*)\//.exec(d.ngList))&&RegExp(a[1])|| +d.ngList||",";e.$parsers.push(function(a){if(!D(a)){var c=[];a&&q(a.split(g),function(a){a&&c.push(ca(a))});return c}});e.$formatters.push(function(a){return M(a)?a.join(", "):s});e.$isEmpty=function(a){return!a||!a.length}}}},bf=/^(true|false|\d+)$/,Rd=function(){return{priority:100,compile:function(a,c){return bf.test(c.ngValue)?function(a,c,g){g.$set("value",a.$eval(g.ngValue))}:function(a,c,g){a.$watch(g.ngValue,function(a){g.$set("value",a)})}}}},rd=xa(function(a,c,d){c.addClass("ng-binding").data("$binding", +d.ngBind);a.$watch(d.ngBind,function(a){c.text(a==s?"":a)})}),td=["$interpolate",function(a){return function(c,d,e){c=a(d.attr(e.$attr.ngBindTemplate));d.addClass("ng-binding").data("$binding",c);e.$observe("ngBindTemplate",function(a){d.text(a)})}}],sd=["$sce","$parse",function(a,c){return function(d,e,g){e.addClass("ng-binding").data("$binding",g.ngBindHtml);var f=c(g.ngBindHtml);d.$watch(function(){return(f(d)||"").toString()},function(c){e.html(a.getTrustedHtml(f(d))||"")})}}],ud=Rb("",!0),wd= +Rb("Odd",0),vd=Rb("Even",1),xd=xa({compile:function(a,c){c.$set("ngCloak",s);a.removeClass("ng-cloak")}}),yd=[function(){return{scope:!0,controller:"@",priority:500}}],ic={};q("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var c=na("ng-"+a);ic[c]=["$parse",function(d){return{compile:function(e,g){var f=d(g[c]);return function(c,d,e){d.on(I(a),function(a){c.$apply(function(){f(c,{$event:a})})})}}}}]}); +var Bd=["$animate",function(a){return{transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(c,d,e,g,f){var h,m,k;c.$watch(e.ngIf,function(g){Pa(g)?m||(m=c.$new(),f(m,function(c){c[c.length++]=U.createComment(" end ngIf: "+e.ngIf+" ");h={clone:c};a.enter(c,d.parent(),d)})):(k&&(k.remove(),k=null),m&&(m.$destroy(),m=null),h&&(k=Db(h.clone),a.leave(k,function(){k=null}),h=null))})}}}],Cd=["$http","$templateCache","$anchorScroll","$animate","$sce",function(a,c,d,e,g){return{restrict:"ECA", +priority:400,terminal:!0,transclude:"element",controller:Qa.noop,compile:function(f,h){var m=h.ngInclude||h.src,k=h.onload||"",l=h.autoscroll;return function(f,h,r,q,z){var s=0,w,y,G,x=function(){y&&(y.remove(),y=null);w&&(w.$destroy(),w=null);G&&(e.leave(G,function(){y=null}),y=G,G=null)};f.$watch(g.parseAsResourceUrl(m),function(g){var m=function(){!B(l)||l&&!f.$eval(l)||d()},r=++s;g?(a.get(g,{cache:c}).success(function(a){if(r===s){var c=f.$new();q.template=a;a=z(c,function(a){x();e.enter(a,null, +h,m)});w=c;G=a;w.$emit("$includeContentLoaded");f.$eval(k)}}).error(function(){r===s&&x()}),f.$emit("$includeContentRequested")):(x(),q.template=null)})}}}}],Sd=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(c,d,e,g){d.html(g.template);a(d.contents())(c)}}}],Dd=xa({priority:450,compile:function(){return{pre:function(a,c,d){a.$eval(d.ngInit)}}}}),Ed=xa({terminal:!0,priority:1E3}),Fd=["$locale","$interpolate",function(a,c){var d=/{}/g;return{restrict:"EA", +link:function(e,g,f){var h=f.count,m=f.$attr.when&&g.attr(f.$attr.when),k=f.offset||0,l=e.$eval(m)||{},n={},p=c.startSymbol(),r=c.endSymbol(),s=/^when(Minus)?(.+)$/;q(f,function(a,c){s.test(c)&&(l[I(c.replace("when","").replace("Minus","-"))]=g.attr(f.$attr[c]))});q(l,function(a,e){n[e]=c(a.replace(d,p+h+"-"+k+r))});e.$watch(function(){var c=parseFloat(e.$eval(h));if(isNaN(c))return"";c in l||(c=a.pluralCat(c-k));return n[c](e,g,!0)},function(a){g.text(a)})}}}],Gd=["$parse","$animate",function(a, +c){var d=v("ngRepeat");return{transclude:"element",priority:1E3,terminal:!0,$$tlb:!0,link:function(e,g,f,h,m){var k=f.ngRepeat,l=k.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),n,p,r,s,z,B,w={$id:Ja};if(!l)throw d("iexp",k);f=l[1];h=l[2];(l=l[3])?(n=a(l),p=function(a,c,d){B&&(w[B]=a);w[z]=c;w.$index=d;return n(e,w)}):(r=function(a,c){return Ja(c)},s=function(a){return a});l=f.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);if(!l)throw d("iidexp",f);z=l[3]||l[1]; +B=l[2];var H={};e.$watchCollection(h,function(a){var f,h,l=g[0],n,w={},E,R,t,C,S,v,D=[];if(db(a))S=a,n=p||r;else{n=p||s;S=[];for(t in a)a.hasOwnProperty(t)&&"$"!=t.charAt(0)&&S.push(t);S.sort()}E=S.length;h=D.length=S.length;for(f=0;fA;)u.pop().element.remove()}for(;x.length>J;)x.pop()[0].element.remove()}var k;if(!(k= +t.match(d)))throw cf("iexp",t,ha(f));var m=c(k[2]||k[1]),l=k[4]||k[6],n=k[5],p=c(k[3]||""),q=c(k[2]?k[1]:l),y=c(k[7]),v=k[8]?c(k[8]):null,x=[[{element:f,label:""}]];z&&(a(z)(e),z.removeClass("ng-scope"),z.remove());f.empty();f.on("change",function(){e.$apply(function(){var a,c=y(e)||[],d={},h,k,m,p,t,w,u;if(r)for(k=[],p=0,w=x.length;p@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}'); +//# sourceMappingURL=angular.min.js.map diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/jwt-decode.min.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/jwt-decode.min.js new file mode 100644 index 0000000000..f56f96737a --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/jwt-decode.min.js @@ -0,0 +1 @@ +!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g>8-f%1*8)){if(e=a.charCodeAt(f+=.75),e>255)throw d;c=c<<8|e}return h}),a.atob||(a.atob=function(a){if(a=a.replace(/=+$/,""),a.length%4==1)throw d;for(var c,e,f=0,g=0,h="";e=a.charAt(g++);~e&&(c=f%4?64*c+e:e,f++%4)?h+=String.fromCharCode(255&c>>(-2*f&6)):0)e=b.indexOf(e);return h})}()},{}],5:[function(a){var b="undefined"!=typeof self?self:"undefined"!=typeof window?window:{},c=a("./lib/index");"function"==typeof b.window.define&&b.window.define.amd?b.window.define("jwt_decode",function(){return c}):b.window&&(b.window.jwt_decode=c)},{"./lib/index":2}]},{},[5]); \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html new file mode 100644 index 0000000000..da78224f91 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html @@ -0,0 +1,19 @@ +

All Albums

+ + + + + + + + + + + +
{{key}}
+ +
\ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html new file mode 100644 index 0000000000..d9ddd25813 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html @@ -0,0 +1,7 @@ +

Create an Album

+ +
+ Name: + + +
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/detail.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/detail.html new file mode 100644 index 0000000000..cf32df1480 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/detail.html @@ -0,0 +1 @@ +

{{album.name}}

\ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html new file mode 100644 index 0000000000..bd5853ea8e --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html @@ -0,0 +1,22 @@ +

Welcome To Photoz, {{Identity.claims.name}} [Sign Out]

+
Administration: [All Albums]
+
+
+
+Create Album | My Profile +
+
+You don't have any albums, yet. + + + + + + + + + + + +
Your Albums
{{p.name}} - [X]
+
\ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/profile.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/profile.html new file mode 100644 index 0000000000..c6f6750a4c --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/profile.html @@ -0,0 +1,6 @@ +

My Profile

+ +
+

Name: {{profile.userName}}

+

Total of albums: {{profile.totalAlbums}}

+
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json new file mode 100644 index 0000000000..b3b2b8132a --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json @@ -0,0 +1,110 @@ +{ + "realm": "photoz", + "enabled": true, + "sslRequired": "external", + "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "requiredCredentials": [ + "password" + ], + "users": [ + { + "username": "alice", + "enabled": true, + "email": "alice@keycloak.org", + "firstName": "Alice", + "lastName": "In Chains", + "credentials": [ + { + "type": "password", + "value": "alice" + } + ], + "realmRoles": [ + "user", "uma_authorization" + ] + }, + { + "username": "jdoe", + "enabled": true, + "email": "jdoe@keycloak.org", + "firstName": "John", + "lastName": "Doe", + "credentials": [ + { + "type": "password", + "value": "jdoe" + } + ], + "realmRoles": [ + "user", "uma_authorization" + ] + }, + { + "username": "admin", + "enabled": true, + "email": "admin@admin.com", + "firstName": "Admin", + "lastName": "Istrator", + "credentials": [ + { + "type": "password", + "value": "admin" + } + ], + "realmRoles": [ + "user", "admin", "uma_authorization" + ], + "clientRoles": { + "realm-management": [ + "realm-admin" + ] + } + }, + { + "username": "service-account-photoz-restful-api", + "enabled": true, + "email": "service-account-photoz-restful-api@placeholder.org", + "serviceAccountClientId": "photoz-restful-api", + "clientRoles": { + "photoz-restful-api" : ["uma_protection"] + } + } + ], + "roles": { + "realm": [ + { + "name": "user", + "description": "User privileges" + }, + { + "name": "admin", + "description": "Administrator privileges" + } + ] + }, + "clients": [ + { + "clientId": "photoz-html5-client", + "enabled": true, + "adminUrl": "/photoz-html5-client", + "baseUrl": "/photoz-html5-client", + "publicClient": true, + "redirectUris": [ + "/photoz-html5-client/*" + ], + "webOrigins": ["*"] + }, + { + "clientId": "photoz-restful-api", + "secret": "secret", + "enabled": true, + "baseUrl": "/photoz-restful-api", + "authorizationServicesEnabled" : true, + "redirectUris": [ + "/photoz-restful-api/*" + ], + "webOrigins" : ["*"] + } + ] +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json new file mode 100644 index 0000000000..91c3e0ea63 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json @@ -0,0 +1,183 @@ +{ + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "User Profile Resource", + "uri": "/profile", + "type": "http://photoz.com/profile", + "scopes": [ + { + "name": "urn:photoz.com:scopes:profile:view" + } + ] + }, + { + "name": "Album Resource", + "uri": "/album/*", + "type": "http://photoz.com/album", + "scopes": [ + { + "name": "urn:photoz.com:scopes:album:view" + }, + { + "name": "urn:photoz.com:scopes:album:create" + }, + { + "name": "urn:photoz.com:scopes:album:delete" + } + ] + }, + { + "name": "Admin Resources", + "uri": "/admin/*", + "type": "http://photoz.com/admin", + "scopes": [ + { + "name": "urn:photoz.com:scopes:album:admin:manage" + } + ] + } + ], + "policies": [ + { + "name": "Only Owner Policy", + "description": "Defines that only the resource owner is allowed to do something", + "type": "drools", + "config": { + "mavenArtifactVersion": "2.0.0.CR1-SNAPSHOT", + "mavenArtifactId": "photoz-authz-policy", + "sessionName": "MainOwnerSession", + "mavenArtifactGroupId": "org.keycloak.testsuite", + "moduleName": "PhotozAuthzOwnerPolicy", + "scannerPeriod": "1", + "scannerPeriodUnit": "Hours" + } + }, + { + "name": "Any Admin Policy", + "description": "Defines that adminsitrators can do something", + "type": "role", + "config": { + "roles": "[\"admin\"]" + } + }, + { + "name": "Any User Policy", + "description": "Defines that any user can do something", + "type": "role", + "config": { + "roles": "[\"user\"]" + } + }, + { + "name": "Only From a Specific Client Address", + "description": "Defines that only clients from a specific address can do something", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "code": "var contextAttributes = $evaluation.getContext().getAttributes();\n\nif (contextAttributes.containsValue('kc.client.network.ip_address', '127.0.0.1')) {\n $evaluation.grant();\n}" + } + }, + { + "name": "Administration Policy", + "description": "Defines that only administrators from a specific network address can do something.", + "type": "aggregate", + "config": { + "applyPolicies": "[\"Any Admin Policy\",\"Only From a Specific Client Address\"]" + } + }, + { + "name": "Only Owner and Administrators Policy", + "description": "Defines that only the resource owner and administrators can do something", + "type": "aggregate", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "applyPolicies": "[\"Administration Policy\",\"Only Owner Policy\"]" + } + }, + { + "name": "Only From @keycloak.org or Admin", + "description": "Defines that only users from @keycloak.org", + "type": "js", + "config": { + "applyPolicies": "[]", + "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}" + } + }, + { + "name": "Only in the Period", + "description": "Access granted only during the morning", + "type": "time", + "config": { + "noa": "2016-01-03 23:59:59", + "expirationUnit": "Minutes", + "nbf": "2016-01-01 00:00:00", + "expirationTime": "1" + } + }, + { + "name": "Album Resource Permission", + "description": "General policies that apply to all album resources.", + "type": "resource", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "defaultResourceType": "http://photoz.com/album", + "default": "true", + "applyPolicies": "[\"Any User Policy\",\"Administration Policy\"]" + } + }, + { + "name": "Admin Resource Permission", + "description": "General policy for any administrative resource.", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "defaultResourceType": "http://photoz.com/admin", + "default": "true", + "applyPolicies": "[\"Administration Policy\"]" + } + }, + { + "name": "View User Permission", + "description": "Defines who is allowed to view an user profile", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "applyPolicies": "[\"Only From @keycloak.org or Admin\"]", + "scopes": "[\"urn:photoz.com:scopes:profile:view\"]" + } + }, + { + "name": "Delete Album Permission", + "description": "A policy that only allows the owner to delete his albums.", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "applyPolicies": "[\"Only Owner and Administrators Policy\"]", + "scopes": "[\"urn:photoz.com:scopes:album:delete\"]" + } + } + ], + "scopes": [ + { + "name": "urn:photoz.com:scopes:profile:view" + }, + { + "name": "urn:photoz.com:scopes:album:view" + }, + { + "name": "urn:photoz.com:scopes:album:create" + }, + { + "name": "urn:photoz.com:scopes:album:delete" + }, + { + "name": "urn:photoz.com:scopes:album:admin:manage" + } + ] +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml new file mode 100755 index 0000000000..eb33fb5d8f --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + org.keycloak.testsuite + integration-arquillian-test-apps-photoz-parent + 2.0.0.CR1-SNAPSHOT + ../pom.xml + + + photoz-restful-api + war + + Keycloak Authz Test: Photoz RESTful API + Photoz RESTful API + + + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_2.0_spec + provided + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + javax.persistence + persistence-api + 1.0.2 + provided + + + org.jboss.spec.javax.ejb + jboss-ejb-api_3.2_spec + 1.0.0.Final + provided + + + org.keycloak + keycloak-authz-client + ${project.version} + provided + + + + + ${project.artifactId} + + + org.jboss.as.plugins + jboss-as-maven-plugin + + false + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + + diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java new file mode 100644 index 0000000000..51755d899e --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java @@ -0,0 +1,32 @@ +package org.keycloak.example.photoz; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Pedro Igor + */ +public class ErrorResponse extends WebApplicationException { + + private final Response.Status status; + + public ErrorResponse(String message) { + this(message, Response.Status.INTERNAL_SERVER_ERROR); + } + + public ErrorResponse(String message, Response.Status status) { + super(message, status); + this.status = status; + } + + @Override + public Response getResponse() { + Map errorResponse = new HashMap<>(); + + errorResponse.put("message", getMessage()); + + return Response.status(status).entity(errorResponse).build(); + } +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java new file mode 100644 index 0000000000..5b8377ced6 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java @@ -0,0 +1,12 @@ +package org.keycloak.example.photoz; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Basic auth app. + */ +@ApplicationPath("/") +public class PhotozApplication extends Application { + +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java new file mode 100644 index 0000000000..b349e026d4 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java @@ -0,0 +1,62 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.example.photoz.admin; + +import org.keycloak.example.photoz.entity.Album; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * @author Pedro Igor + */ +@Path("/admin/album") +@Stateless +public class AdminAlbumService { + + public static final String SCOPE_ADMIN_ALBUM_MANAGE = "urn:photoz.com:scopes:album:admin:manage"; + + @PersistenceContext + private EntityManager entityManager; + + @Context + private HttpHeaders headers; + + @GET + @Produces("application/json") + public Response findAll() { + HashMap> albums = new HashMap<>(); + List result = this.entityManager.createQuery("from Album").getResultList(); + + for (Album album : result) { + albums.computeIfAbsent(album.getUserId(), key -> new ArrayList<>()).add(album); + } + + return Response.ok(albums).build(); + } +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java new file mode 100644 index 0000000000..388c9e4c8b --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java @@ -0,0 +1,159 @@ +package org.keycloak.example.photoz.album; + +import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.authorization.client.Configuration; +import org.keycloak.authorization.client.representation.ResourceRepresentation; +import org.keycloak.authorization.client.representation.ScopeRepresentation; +import org.keycloak.authorization.client.resource.ProtectionResource; +import org.keycloak.example.photoz.ErrorResponse; +import org.keycloak.example.photoz.entity.Album; +import org.keycloak.representations.adapters.config.AdapterConfig; +import org.keycloak.util.JsonSerialization; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.Query; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.security.Principal; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Path("/album") +@Stateless +public class AlbumService { + + public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view"; + public static final String SCOPE_ALBUM_CREATE = "urn:photoz.com:scopes:album:create"; + public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete"; + + @PersistenceContext + private EntityManager entityManager; + + @Context + private HttpServletRequest request; + + private AuthzClient authzClient; + + public AlbumService() { + + } + + @POST + @Consumes("application/json") + public Response create(Album newAlbum) { + Principal userPrincipal = request.getUserPrincipal(); + + newAlbum.setUserId(userPrincipal.getName()); + + Query queryDuplicatedAlbum = this.entityManager.createQuery("from Album where name = :name and userId = :userId"); + + queryDuplicatedAlbum.setParameter("name", newAlbum.getName()); + queryDuplicatedAlbum.setParameter("userId", userPrincipal.getName()); + + if (!queryDuplicatedAlbum.getResultList().isEmpty()) { + throw new ErrorResponse("Name [" + newAlbum.getName() + "] already taken. Choose another one.", Status.CONFLICT); + } + + this.entityManager.persist(newAlbum); + + createProtectedResource(newAlbum); + + return Response.ok(newAlbum).build(); + } + + @Path("{id}") + @DELETE + public Response delete(@PathParam("id") String id) { + Album album = this.entityManager.find(Album.class, Long.valueOf(id)); + + try { + deleteProtectedResource(album); + this.entityManager.remove(album); + } catch (Exception e) { + throw new RuntimeException("Could not delete album.", e); + } + + return Response.ok().build(); + } + + @GET + @Produces("application/json") + public Response findAll() { + return Response.ok(this.entityManager.createQuery("from Album where userId = '" + request.getUserPrincipal().getName() + "'").getResultList()).build(); + } + + @GET + @Path("{id}") + @Produces("application/json") + public Response findById(@PathParam("id") String id) { + List result = this.entityManager.createQuery("from Album where id = " + id).getResultList(); + + if (result.isEmpty()) { + return Response.status(Status.NOT_FOUND).build(); + } + + return Response.ok(result.get(0)).build(); + } + + private void createProtectedResource(Album album) { + try { + HashSet scopes = new HashSet<>(); + + scopes.add(new ScopeRepresentation(SCOPE_ALBUM_VIEW)); + scopes.add(new ScopeRepresentation(SCOPE_ALBUM_CREATE)); + scopes.add(new ScopeRepresentation(SCOPE_ALBUM_DELETE)); + + ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album"); + + albumResource.setOwner(album.getUserId()); + + getAuthzClient().protection().resource().create(albumResource); + } catch (Exception e) { + throw new RuntimeException("Could not register protected resource.", e); + } + } + + private void deleteProtectedResource(Album album) { + String uri = "/album/" + album.getId(); + + try { + ProtectionResource protection = getAuthzClient().protection(); + Set search = protection.resource().findByFilter("uri=" + uri); + + if (search.isEmpty()) { + throw new RuntimeException("Could not find protected resource with URI [" + uri + "]"); + } + + protection.resource().delete(search.iterator().next()); + } catch (Exception e) { + throw new RuntimeException("Could not search protected resource.", e); + } + } + + private AuthzClient getAuthzClient() { + if (this.authzClient == null) { + try { + AdapterConfig adapterConfig = JsonSerialization.readValue(this.request.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json"), AdapterConfig.class); + Configuration configuration = new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), null); + + this.authzClient = AuthzClient.create(configuration); + } catch (Exception e) { + throw new RuntimeException("Could not create authorization client.", e); + } + } + + return this.authzClient; + } +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java new file mode 100644 index 0000000000..be638b602e --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java @@ -0,0 +1,70 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.example.photoz.album; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import java.security.Principal; +import java.util.List; + +/** + * @author Pedro Igor + */ +@Path("/profile") +@Stateless +public class ProfileService { + + private static final String PROFILE_VIEW = "urn:photoz.com:scopes:profile:view"; + + @PersistenceContext + private EntityManager entityManager; + + @GET + @Produces("application/json") + public Response view(@Context HttpServletRequest request) { + Principal userPrincipal = request.getUserPrincipal(); + List albums = this.entityManager.createQuery("from Album where userId = '" + userPrincipal.getName() + "'").getResultList(); + return Response.ok(new Profile(userPrincipal.getName(), albums.size())).build(); + } + + public static class Profile { + private String userName; + private int totalAlbums; + + public Profile(String name, int totalAlbums) { + this.userName = name; + this.totalAlbums = totalAlbums; + } + + public String getUserName() { + return userName; + } + + public int getTotalAlbums() { + return totalAlbums; + } + } +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java new file mode 100644 index 0000000000..978bdeabb5 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java @@ -0,0 +1,79 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.example.photoz.entity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Pedro Igor + */ +@Entity +public class Album { + + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private String name; + + @OneToMany(mappedBy = "album", fetch = FetchType.EAGER) + private List photos = new ArrayList<>(); + + @Column(nullable = false) + private String userId; + + public Long getId() { + return this.id; + } + + public void setId(final Long id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(final String name) { + this.name = name; + } + + public List getPhotos() { + return this.photos; + } + + public void setPhotos(final List photos) { + this.photos = photos; + } + + public void setUserId(final String userId) { + this.userId = userId; + } + + public String getUserId() { + return this.userId; + } +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java new file mode 100644 index 0000000000..08b7495f73 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java @@ -0,0 +1,81 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2015 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.example.photoz.entity; + +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; + +/** + * @author Pedro Igor + */ +@Entity +public class Photo { + + @Id + @GeneratedValue + private Long id; + + @Column + private String name; + + @ManyToOne + private Album album; + + @Lob + @Column + @Basic(fetch = FetchType.LAZY) + private byte[] image; + + public Long getId() { + return this.id; + } + + public void setId(final Long id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(final String name) { + this.name = name; + } + + public Album getAlbum() { + return this.album; + } + + public void setAlbum(final Album album) { + this.album = album; + } + + public byte[] getImage() { + return this.image; + } + + public void setImage(final byte[] image) { + this.image = image; + } +} diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000..957dc8ac43 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml @@ -0,0 +1,7 @@ + + + + diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000000..9323182405 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,19 @@ + + + + java:jboss/datasources/PhotozDS + + org.keycloak.example.photoz.entity.Album + org.keycloak.example.photoz.entity.Photo + + + + + + + + diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml new file mode 100644 index 0000000000..4b23be62d7 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json new file mode 100644 index 0000000000..95fb58bf70 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json @@ -0,0 +1,45 @@ +{ + "realm": "photoz", + "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "http://localhost:8080/auth", + "ssl-required": "external", + "resource": "photoz-restful-api", + "bearer-only" : true, + "credentials": { + "secret": "secret" + }, + "policy-enforcer": { + "paths": [ + { + "path" : "/album/*", + "methods" : [ + { + "method": "GET", + "scopes" : ["urn:photoz.com:scopes:album:view"] + }, + { + "method": "POST", + "scopes" : ["urn:photoz.com:scopes:album:create"] + } + ] + }, + { + "name" : "Album Resource", + "path" : "/album/{id}", + "methods" : [ + { + "method": "DELETE", + "scopes" : ["urn:photoz.com:scopes:album:delete"] + } + ] + }, + { + "path" : "/profile" + }, + { + "name" : "Admin Resources", + "path" : "/admin/*" + } + ] + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml new file mode 100644 index 0000000000..247448f2ae --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml @@ -0,0 +1,12 @@ + + + jdbc:h2:${jboss.server.data.dir}/kc-authz-photo;AUTO_SERVER=TRUE + h2 + + sa + sa + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..34cf6bd6a1 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,41 @@ + + + + photoz-restful-api + + + + All Resources + /* + + + user + + + + + + All Resources + /* + + + admin + + + + + KEYCLOAK + photoz + + + + admin + + + + user + + diff --git a/testsuite/integration-arquillian/test-apps/photoz/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/pom.xml new file mode 100755 index 0000000000..fdf7946544 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/photoz/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + + org.keycloak.testsuite + integration-arquillian-test-apps + 2.0.0.CR1-SNAPSHOT + + + integration-arquillian-test-apps-photoz-parent + pom + + Keycloak Authz: PhotoZ Test Parent + PhotoZ Test Application + + + photoz-restful-api + photoz-html5-client + photoz-authz-policy + + diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml index 5f13130e21..21c917913d 100644 --- a/testsuite/integration-arquillian/test-apps/pom.xml +++ b/testsuite/integration-arquillian/test-apps/pom.xml @@ -18,5 +18,7 @@ js-console test-apps-dist js-database + photoz + hello-world-authz-service \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml index e48e088698..9621ae53ef 100755 --- a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml +++ b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml @@ -27,5 +27,21 @@ + + + + + + + + + + + + + + + + diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java new file mode 100644 index 0000000000..09bf6a92a4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.adapter.page; + +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.keycloak.testsuite.auth.page.login.OIDCLogin; +import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; +import org.keycloak.testsuite.page.Form; +import org.keycloak.testsuite.util.WaitUtils; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; + +import java.net.URL; + +import static org.keycloak.testsuite.util.WaitUtils.pause; + +/** + * @author Pedro Igor + */ +public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl { + + public static final String DEPLOYMENT_NAME = "photoz-html5-client"; + + @ArquillianResource + @OperateOnDeployment(DEPLOYMENT_NAME) + private URL url; + + @Page + protected OIDCLogin loginPage; + + public void createAlbum(String name) { + this.driver.findElement(By.id("create-album")).click(); + Form.setInputValue(this.driver.findElement(By.id("album.name")), name); + this.driver.findElement(By.id("save-album")).click(); + pause(500); + } + + @Override + public URL getInjectedUrl() { + return this.url; + } + + public void deleteAlbum(String name) { + By id = By.id("delete-" + name); + WaitUtils.waitUntilElement(id); + this.driver.findElements(id).forEach(WebElement::click); + pause(500); + } + + public void navigateToAdminAlbum() { + this.driver.navigate().to(this.getInjectedUrl().toString() + "/#/admin/album"); + pause(500); + } + + public void logOut() { + navigateTo(); + By by = By.xpath("//a[text() = 'Sign Out']"); + WaitUtils.waitUntilElement(by); + this.driver.findElement(by).click(); + pause(500); + } + + public void login(String username, String password) { + navigateTo(); + + if (this.driver.getCurrentUrl().startsWith(getInjectedUrl().toString())) { + logOut(); + } + + this.loginPage.form().login(username, password); + } + + public boolean wasDenied() { + return this.driver.findElement(By.id("output")).getText().contains("You can not access"); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java new file mode 100644 index 0000000000..82b3ec4133 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.adapter.example.authorization; + +import org.jboss.arquillian.container.test.api.Deployer; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.util.IOUtil.loadRealm; + +/** + * @author Pedro Igor + */ +public abstract class AbstractDefaultAuthzConfigAdapterTest extends AbstractExampleAdapterTest { + + private static final String REALM_NAME = "hello-world-authz"; + private static final String RESOURCE_SERVER_ID = "hello-world-authz-service"; + + @ArquillianResource + private Deployer deployer; + + @Override + public void addAdapterTestRealms(List testRealms) { + testRealms.add( + loadRealm(new File(TEST_APPS_HOME_DIR + "/hello-world-authz-service/hello-world-authz-realm.json"))); + } + + @Deployment(name = RESOURCE_SERVER_ID, managed = false) + public static WebArchive deployment() throws IOException { + return exampleDeployment(RESOURCE_SERVER_ID); + } + + @Test + public void testDefaultAuthzConfig() throws Exception { + configureAuthorizationServices(); + deploy(); + navigateToResourceServer(); + login(); + + assertTrue(this.driver.getPageSource().contains("Your permissions are")); + assertTrue(this.driver.getPageSource().contains("Default Resource")); + } + + private void login() { + this.loginPage.form().login("alice", "alice"); + } + + private void navigateToResourceServer() throws MalformedURLException { + this.driver.navigate().to(getResourceServerUrl()); + } + + private URL getResourceServerUrl() throws MalformedURLException { + return this.appServerContextRootPage.getUriBuilder().path(RESOURCE_SERVER_ID).build().toURL(); + } + + private void deploy() { + this.deployer.deploy(RESOURCE_SERVER_ID); + } + + private void configureAuthorizationServices() { + ClientsResource clients = realmsResouce().realm(REALM_NAME).clients(); + ClientRepresentation client = clients.findByClientId(RESOURCE_SERVER_ID).get(0); + + client.setAuthorizationServicesEnabled(false); + + // disables authorization services and remove authorization configuration from the client app + clients.get(client.getId()).update(client); + + client.setAuthorizationServicesEnabled(true); + + // enable authorization services in order to generate the default config and continue with tests + clients.get(client.getId()).update(client); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java new file mode 100644 index 0000000000..b2a0354a81 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java @@ -0,0 +1,207 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.adapter.example.authorization; + +import org.apache.commons.io.IOUtils; +import org.jboss.arquillian.container.test.api.Deployer; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.Test; +import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; +import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest; +import org.keycloak.testsuite.adapter.page.PhotozClientAuthzTestApp; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.util.IOUtil.loadJson; +import static org.keycloak.testsuite.util.IOUtil.loadRealm; + +/** + * @author Pedro Igor + */ +public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAdapterTest { + + private static final String REALM_NAME = "photoz"; + private static final String RESOURCE_SERVER_ID = "photoz-restful-api"; + private static int TOKEN_LIFESPAN_LEEWAY = 3; // seconds + + @ArquillianResource + private Deployer deployer; + + @Page + private PhotozClientAuthzTestApp clientPage; + + @Override + public void addAdapterTestRealms(List testRealms) { + RealmRepresentation realm = loadRealm(new File(TEST_APPS_HOME_DIR + "/photoz/photoz-realm.json")); + + realm.setAccessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY); // seconds + + testRealms.add(realm); + } + + @Deployment(name = PhotozClientAuthzTestApp.DEPLOYMENT_NAME) + public static WebArchive deploymentClient() throws IOException { + return exampleDeployment(PhotozClientAuthzTestApp.DEPLOYMENT_NAME); + } + + @Deployment(name = RESOURCE_SERVER_ID, managed = false) + public static WebArchive deploymentResourceServer() throws IOException { + return exampleDeployment(RESOURCE_SERVER_ID); + } + + @Override + public void beforeAbstractKeycloakTest() throws Exception { + super.beforeAbstractKeycloakTest(); + importResourceServerSettings(); + } + + @Test + public void testCreateDeleteAlbum() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + + this.clientPage.login("alice", "alice"); + this.clientPage.createAlbum("Alice Family Album"); + + List resources = getAuthorizationResource().resources().resources(); + + assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + + this.clientPage.deleteAlbum("Alice Family Album"); + + resources = getAuthorizationResource().resources().resources(); + + assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + @Test + public void testOnlyOwnerCanDeleteAlbum() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); + this.clientPage.createAlbum("Alice Family Album"); + this.clientPage.login("admin", "admin"); + this.clientPage.navigateToAdminAlbum(); + + List resources = getAuthorizationResource().resources().resources(); + + assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Delete Album Permission".equals(policy.getName())) { + policy.getConfig().put("applyPolicies", "[\"Only Owner Policy\"]"); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + this.clientPage.login("admin", "admin"); + this.clientPage.navigateToAdminAlbum(); + this.clientPage.deleteAlbum("Alice Family Album"); + + resources = getAuthorizationResource().resources().resources(); + + assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Delete Album Permission".equals(policy.getName())) { + policy.getConfig().put("applyPolicies", "[\"Only Owner and Administrators Policy\"]"); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + this.clientPage.login("admin", "admin"); + this.clientPage.navigateToAdminAlbum(); + this.clientPage.deleteAlbum("Alice Family Album"); + + resources = getAuthorizationResource().resources().resources(); + + assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + @Test + public void testRegularUserCanNotAccessAdminResources() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("alice", "alice"); + this.clientPage.navigateToAdminAlbum(); + + assertTrue(this.clientPage.wasDenied()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + @Test + public void testAdminOnlyFromSpecificAddress() throws Exception { + try { + this.deployer.deploy(RESOURCE_SERVER_ID); + this.clientPage.login("admin", "admin"); + this.clientPage.navigateToAdminAlbum(); + + assertFalse(this.clientPage.wasDenied()); + + for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) { + if ("Only From a Specific Client Address".equals(policy.getName())) { + String code = policy.getConfig().get("code"); + policy.getConfig().put("code", code.replaceAll("127.0.0.1", "127.3.3.3")); + getAuthorizationResource().policies().policy(policy.getId()).update(policy); + } + } + + this.clientPage.login("admin", "admin"); + this.clientPage.navigateToAdminAlbum(); + + assertTrue(this.clientPage.wasDenied()); + } finally { + this.deployer.undeploy(RESOURCE_SERVER_ID); + } + } + + private void importResourceServerSettings() throws FileNotFoundException { + getAuthorizationResource().importSettings(loadJson(new FileInputStream(new File(TEST_APPS_HOME_DIR + "/photoz/photoz-restful-api-authz-service.json")), ResourceServerRepresentation.class)); + } + + private AuthorizationResource getAuthorizationResource() throws FileNotFoundException { + ClientsResource clients = this.realmsResouce().realm(REALM_NAME).clients(); + ClientRepresentation resourceServer = clients.findByClientId(RESOURCE_SERVER_ID).get(0); + return clients.get(resourceServer.getId()).authorization(); + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java new file mode 100644 index 0000000000..c4979e0f67 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java @@ -0,0 +1,93 @@ +/* + Copyright 2016 Red Hat, Inc. and/or its affiliates + and other contributors as indicated by the @author tags. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.keycloak.testsuite.admin.client.authorization; + +import org.junit.After; +import org.junit.Before; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ResourceScopeResource; +import org.keycloak.admin.client.resource.ResourceScopesResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.keycloak.testsuite.admin.client.AbstractClientTest; + +import javax.ws.rs.core.Response; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +/** + * @author Pedro Igor + */ +public abstract class AbstractAuthorizationTest extends AbstractClientTest { + + protected static final String RESOURCE_SERVER_CLIENT_ID = "test-resource-server"; + + @Before + public void onBeforeAuthzTests() { + createOidcClient(RESOURCE_SERVER_CLIENT_ID); + + ClientRepresentation resourceServer = getResourceServer(); + + assertEquals(RESOURCE_SERVER_CLIENT_ID, resourceServer.getName()); + assertFalse(resourceServer.getAuthorizationServicesEnabled()); + } + + @After + public void onAfterAuthzTests() { + getClientResource().remove(); + } + + protected ClientResource getClientResource() { + return findClientResource(RESOURCE_SERVER_CLIENT_ID); + } + + protected ClientRepresentation getResourceServer() { + return findClientRepresentation(RESOURCE_SERVER_CLIENT_ID); + } + + protected void enableAuthorizationServices() { + ClientRepresentation resourceServer = getResourceServer(); + + resourceServer.setAuthorizationServicesEnabled(true); + resourceServer.setServiceAccountsEnabled(true); + + getClientResource().update(resourceServer); + } + + protected ResourceScopeResource createDefaultScope() { + return createScope("Test Scope", "Scope Icon"); + } + + protected ResourceScopeResource createScope(String name, String iconUri) { + ScopeRepresentation newScope = new ScopeRepresentation(); + + newScope.setName(name); + newScope.setIconUri(iconUri); + + ResourceScopesResource resources = getClientResource().authorization().scopes(); + + Response response = resources.create(newScope); + + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + + ScopeRepresentation stored = response.readEntity(ScopeRepresentation.class); + + return resources.scope(stored.getId()); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AuthorizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AuthorizationTest.java new file mode 100644 index 0000000000..55936b26fc --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AuthorizationTest.java @@ -0,0 +1,58 @@ +/* + Copyright 2016 Red Hat, Inc. and/or its affiliates + and other contributors as indicated by the @author tags. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +package org.keycloak.testsuite.admin.client.authorization; + +import org.junit.Test; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * + * @author Pedro Igor + */ +public class AuthorizationTest extends AbstractAuthorizationTest { + + @Test + public void testEnableAuthorizationServices() { + ClientResource clientResource = getClientResource(); + ClientRepresentation resourceServer = getResourceServer(); + + enableAuthorizationServices(); + + ResourceServerRepresentation settings = clientResource.authorization().getSettings(); + + assertEquals(PolicyEnforcerConfig.EnforcementMode.ENFORCING.name(), settings.getPolicyEnforcementMode().name()); + assertEquals(resourceServer.getId(), settings.getClientId()); + List defaultResources = clientResource.authorization().resources().resources(); + + assertEquals(1, defaultResources.size()); + + List defaultPolicies = clientResource.authorization().policies().policies(); + + assertEquals(2, defaultPolicies.size()); + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java new file mode 100644 index 0000000000..e6f83d8af0 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java @@ -0,0 +1,290 @@ +/* + Copyright 2016 Red Hat, Inc. and/or its affiliates + and other contributors as indicated by the @author tags. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ +package org.keycloak.testsuite.admin.client.authorization; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.PoliciesResource; +import org.keycloak.admin.client.resource.PolicyResource; +import org.keycloak.admin.client.resource.ResourceResource; +import org.keycloak.admin.client.resource.ResourceScopeResource; +import org.keycloak.admin.client.resource.ResourceScopesResource; +import org.keycloak.admin.client.resource.ResourcesResource; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.Logic; +import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; + +import javax.ws.rs.core.Response; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Pedro Igor + */ +public class GenericPolicyManagementTest extends AbstractAuthorizationTest { + + private static final String[] EXPECTED_BUILTIN_POLICY_PROVIDERS = {"test", "user", "role", "drools", "js", "time", "aggregate", "scope", "resource"}; + + @Before + @Override + public void onBeforeAuthzTests() { + super.onBeforeAuthzTests(); + enableAuthorizationServices(); + } + + @After + @Override + public void onAfterAuthzTests() { + super.onAfterAuthzTests(); + } + + @Test + public void testCreate() { + PolicyRepresentation newPolicy = createTestingPolicy().toRepresentation(); + + assertEquals("Test Generic Policy", newPolicy.getName()); + assertEquals("test", newPolicy.getType()); + assertEquals(Logic.POSITIVE, newPolicy.getLogic()); + assertEquals(DecisionStrategy.UNANIMOUS, newPolicy.getDecisionStrategy()); + assertEquals("configuration for A", newPolicy.getConfig().get("configA")); + assertEquals("configuration for B", newPolicy.getConfig().get("configB")); + assertEquals("configuration for C", newPolicy.getConfig().get("configC")); + + List policies = getClientResource().authorization().policies().policies(); + + assertEquals(6, policies.size()); + + assertAssociatedPolicy("Test Associated A", newPolicy); + assertAssociatedPolicy("Test Associated B", newPolicy); + assertAssociatedPolicy("Test Associated C", newPolicy); + + assertAssociatedResource("Test Resource A", newPolicy); + assertAssociatedResource("Test Resource B", newPolicy); + assertAssociatedResource("Test Resource C", newPolicy); + + assertAssociatedScope("Test Scope A", newPolicy); + assertAssociatedScope("Test Scope B", newPolicy); + assertAssociatedScope("Test Scope C", newPolicy); + } + + @Test + public void testUpdate() { + PolicyResource policyResource = createTestingPolicy(); + PolicyRepresentation resource = policyResource.toRepresentation(); + + resource.setName("changed"); + resource.setLogic(Logic.NEGATIVE); + resource.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE); + resource.getConfig().put("configA", "changed configuration for A"); + resource.getConfig().remove("configB"); + resource.getConfig().put("configC", "changed configuration for C"); + + policyResource.update(resource); + + resource = policyResource.toRepresentation(); + + assertEquals("changed", resource.getName()); + assertEquals(Logic.NEGATIVE, resource.getLogic()); + + assertEquals(DecisionStrategy.AFFIRMATIVE, resource.getDecisionStrategy()); + assertEquals("changed configuration for A", resource.getConfig().get("configA")); + assertNull(resource.getConfig().get("configB")); + assertEquals("changed configuration for C", resource.getConfig().get("configC")); + + Map config = resource.getConfig(); + + config.put("applyPolicies", buildConfigOption(findPolicyByName("Test Associated C").getId())); + + config.put("resources", buildConfigOption(findResourceByName("Test Resource B").getId())); + + config.put("scopes", buildConfigOption(findScopeByName("Test Scope A").getId())); + + policyResource.update(resource); + + resource = policyResource.toRepresentation(); + config = resource.getConfig(); + + assertAssociatedPolicy("Test Associated C", resource); + assertFalse(config.get("applyPolicies").contains(findPolicyByName("Test Associated A").getId())); + assertFalse(config.get("applyPolicies").contains(findPolicyByName("Test Associated B").getId())); + + assertAssociatedResource("Test Resource B", resource); + assertFalse(config.get("resources").contains(findResourceByName("Test Resource A").getId())); + assertFalse(config.get("resources").contains(findResourceByName("Test Resource C").getId())); + + assertAssociatedScope("Test Scope A", resource); + assertFalse(config.get("scopes").contains(findScopeByName("Test Scope B").getId())); + assertFalse(config.get("scopes").contains(findScopeByName("Test Scope C").getId())); + } + + @Test + public void testDefaultPolicyProviders() { + List providers = getClientResource().authorization().policies() + .policyProviders().stream().map(PolicyProviderRepresentation::getType).collect(Collectors.toList()); + + assertFalse(providers.isEmpty()); + assertTrue(providers.containsAll(Arrays.asList(EXPECTED_BUILTIN_POLICY_PROVIDERS))); + } + + private PolicyResource createTestingPolicy() { + Map config = new HashMap<>(); + + config.put("configA", "configuration for A"); + config.put("configB", "configuration for B"); + config.put("configC", "configuration for C"); + + config.put("applyPolicies", buildConfigOption( + createPolicy("Test Associated A", new HashMap<>()).toRepresentation().getId(), + createPolicy("Test Associated B", new HashMap<>()).toRepresentation().getId(), + createPolicy("Test Associated C", new HashMap<>()).toRepresentation().getId() + )); + + config.put("resources", buildConfigOption( + createResource("Test Resource A").toRepresentation().getId(), + createResource("Test Resource B").toRepresentation().getId(), + createResource("Test Resource C").toRepresentation().getId() + )); + + config.put("scopes", buildConfigOption( + createScope("Test Scope A").toRepresentation().getId(), + createScope("Test Scope B").toRepresentation().getId(), + createScope("Test Scope C").toRepresentation().getId() + )); + + return createPolicy("Test Generic Policy", config); + } + + private PolicyResource createPolicy(String name, Map config) { + PolicyRepresentation newPolicy = new PolicyRepresentation(); + + newPolicy.setName(name); + newPolicy.setType("test"); + newPolicy.setConfig(config); + + PoliciesResource policies = getClientResource().authorization().policies(); + Response response = policies.create(newPolicy); + + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + + PolicyRepresentation stored = response.readEntity(PolicyRepresentation.class); + + return policies.policy(stored.getId()); + } + + private ResourceResource createResource(String name) { + ResourceRepresentation newResource = new ResourceRepresentation(); + + newResource.setName(name); + + ResourcesResource resources = getClientResource().authorization().resources(); + + Response response = resources.create(newResource); + + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + + ResourceRepresentation stored = response.readEntity(ResourceRepresentation.class); + + return resources.resource(stored.getId()); + } + + private ResourceScopeResource createScope(String name) { + ScopeRepresentation newScope = new ScopeRepresentation(); + + newScope.setName(name); + + ResourceScopesResource scopes = getClientResource().authorization().scopes(); + + Response response = scopes.create(newScope); + + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + + ScopeRepresentation stored = response.readEntity(ScopeRepresentation.class); + + return scopes.scope(stored.getId()); + } + + private String buildConfigOption(String... values) { + StringBuilder builder = new StringBuilder(); + + for (String value : values) { + if (builder.length() > 0) { + builder.append(","); + } + builder.append("\"" + value + "\""); + } + + return builder.insert(0, "[").append("]").toString(); + } + + private PolicyRepresentation findPolicyByName(String name) { + return getClientResource().authorization().policies().policies() + .stream().filter(policyRepresentation -> policyRepresentation.getName().equals(name)) + .findFirst().orElse(null); + } + + private ResourceRepresentation findResourceByName(String name) { + return getClientResource().authorization().resources().resources() + .stream().filter(resource -> resource.getName().equals(name)) + .findFirst().orElse(null); + } + + private ScopeRepresentation findScopeByName(String name) { + return getClientResource().authorization().scopes().scopes() + .stream().filter(scope -> scope.getName().equals(name)) + .findFirst().orElse(null); + } + + private void assertAssociatedPolicy(String associatedPolicyName, PolicyRepresentation dependentPolicy) { + PolicyRepresentation associatedPolicy = findPolicyByName(associatedPolicyName); + assertNotNull(associatedPolicy); + assertTrue(dependentPolicy.getConfig().get("applyPolicies").contains(associatedPolicy.getId())); + assertEquals(1, associatedPolicy.getDependentPolicies().size()); + assertEquals(dependentPolicy.getId(), associatedPolicy.getDependentPolicies().get(0).getId()); + } + + private void assertAssociatedResource(String resourceName, PolicyRepresentation policy) { + ResourceRepresentation resource = findResourceByName(resourceName); + assertNotNull(resource); + assertTrue(policy.getConfig().get("resources").contains(resource.getId())); + assertEquals(1, resource.getPolicies().size()); + assertTrue(resource.getPolicies().stream().map(PolicyRepresentation::getId).collect(Collectors.toList()) + .contains(policy.getId())); + } + + private void assertAssociatedScope(String scopeName, PolicyRepresentation policy) { + ScopeRepresentation scope = findScopeByName(scopeName); + assertNotNull(scope); + assertTrue(policy.getConfig().get("scopes").contains(scope.getId())); + assertEquals(1, scope.getPolicies().size()); + assertTrue(scope.getPolicies().stream().map(PolicyRepresentation::getId).collect(Collectors.toList()) + .contains(policy.getId())); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java new file mode 100644 index 0000000000..9907472a6a --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java @@ -0,0 +1,185 @@ +/* + Copyright 2016 Red Hat, Inc. and/or its affiliates + and other contributors as indicated by the @author tags. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +package org.keycloak.testsuite.admin.client.authorization; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.ResourceResource; +import org.keycloak.admin.client.resource.ResourcesResource; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * + * @author Pedro Igor + */ +public class ResourceManagementTest extends AbstractAuthorizationTest { + + @Before + @Override + public void onBeforeAuthzTests() { + super.onBeforeAuthzTests(); + enableAuthorizationServices(); + } + + @Test + public void testCreate() { + ResourceRepresentation newResource = createResource().toRepresentation(); + + assertEquals("Test Resource", newResource.getName()); + assertEquals("/test/*", newResource.getUri()); + assertEquals("test-resource", newResource.getType()); + assertEquals("icon-test-resource", newResource.getIconUri()); + } + + @Test + public void testUpdate() { + ResourceResource resourceResource = createResource(); + ResourceRepresentation resource = resourceResource.toRepresentation(); + + resource.setType("changed"); + resource.setIconUri("changed"); + resource.setUri("changed"); + + resourceResource.update(resource); + + resource = resourceResource.toRepresentation(); + + assertEquals("changed", resource.getIconUri()); + assertEquals("changed", resource.getType()); + assertEquals("changed", resource.getUri()); + } + + @Test(expected = NotFoundException.class) + public void testDelete() { + ResourceResource resourceResource = createResource(); + + resourceResource.remove(); + + resourceResource.toRepresentation(); + } + + @Test + public void testAssociateScopes() { + ResourceResource resourceResource = createResourceWithDefaultScopes(); + ResourceRepresentation updated = resourceResource.toRepresentation(); + + assertEquals(3, updated.getScopes().size()); + + assertTrue(containsScope("Scope A", updated)); + assertTrue(containsScope("Scope B", updated)); + assertTrue(containsScope("Scope C", updated)); + } + + @Test + public void testUpdateScopes() { + ResourceResource resourceResource = createResourceWithDefaultScopes(); + ResourceRepresentation resource = resourceResource.toRepresentation(); + Set scopes = new HashSet<>(resource.getScopes()); + + assertEquals(3, scopes.size()); + assertTrue(scopes.removeIf(scopeRepresentation -> scopeRepresentation.getName().equals("Scope B"))); + + resource.setScopes(scopes); + + resourceResource.update(resource); + + ResourceRepresentation updated = resourceResource.toRepresentation(); + + assertEquals(2, resource.getScopes().size()); + + assertFalse(containsScope("Scope B", updated)); + assertTrue(containsScope("Scope A", updated)); + assertTrue(containsScope("Scope C", updated)); + + scopes = new HashSet<>(updated.getScopes()); + + assertTrue(scopes.removeIf(scopeRepresentation -> scopeRepresentation.getName().equals("Scope A"))); + assertTrue(scopes.removeIf(scopeRepresentation -> scopeRepresentation.getName().equals("Scope C"))); + + updated.setScopes(scopes); + + resourceResource.update(updated); + + updated = resourceResource.toRepresentation(); + + assertEquals(0, updated.getScopes().size()); + } + + private ResourceResource createResourceWithDefaultScopes() { + ResourceResource resourceResource = createResource(); + ResourceRepresentation resource = resourceResource.toRepresentation(); + + assertEquals(0, resource.getScopes().size()); + + HashSet scopes = new HashSet<>(); + + scopes.add(createScope("Scope A", "").toRepresentation()); + scopes.add(createScope("Scope B", "").toRepresentation()); + scopes.add(createScope("Scope C", "").toRepresentation()); + + resource.setScopes(scopes); + + resourceResource.update(resource); + + return resourceResource; + } + + private boolean containsScope(String scopeName, ResourceRepresentation resource) { + Set scopes = resource.getScopes(); + + if (scopes != null) { + for (ScopeRepresentation scope : scopes) { + if (scope.getName().equals(scopeName)) { + return true; + } + } + } + + return false; + } + + private ResourceResource createResource() { + ResourceRepresentation newResource = new ResourceRepresentation(); + + newResource.setName("Test Resource"); + newResource.setUri("/test/*"); + newResource.setType("test-resource"); + newResource.setIconUri("icon-test-resource"); + + ResourcesResource resources = getClientResource().authorization().resources(); + + Response response = resources.create(newResource); + + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + + ResourceRepresentation stored = response.readEntity(ResourceRepresentation.class); + + return resources.resource(stored.getId()); + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ScopeManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ScopeManagementTest.java new file mode 100644 index 0000000000..bcb8b1b636 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ScopeManagementTest.java @@ -0,0 +1,75 @@ +/* + Copyright 2016 Red Hat, Inc. and/or its affiliates + and other contributors as indicated by the @author tags. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +package org.keycloak.testsuite.admin.client.authorization; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.ResourceScopeResource; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; + +import javax.ws.rs.NotFoundException; + +import static org.junit.Assert.assertEquals; + +/** + * + * @author Pedro Igor + */ +public class ScopeManagementTest extends AbstractAuthorizationTest { + + @Before + @Override + public void onBeforeAuthzTests() { + super.onBeforeAuthzTests(); + enableAuthorizationServices(); + } + + @Test + public void testCreate() { + ScopeRepresentation newScope = createDefaultScope().toRepresentation(); + + assertEquals("Test Scope", newScope.getName()); + assertEquals("Scope Icon", newScope.getIconUri()); + } + + @Test + public void testUpdate() { + ResourceScopeResource scopeResource = createDefaultScope(); + ScopeRepresentation scope = scopeResource.toRepresentation(); + + scope.setName("changed"); + scope.setIconUri("changed"); + + scopeResource.update(scope); + + scope = scopeResource.toRepresentation(); + + assertEquals("changed", scope.getName()); + assertEquals("changed", scope.getIconUri()); + } + + @Test(expected = NotFoundException.class) + public void testDelete() { + ResourceScopeResource scopeResource = createDefaultScope(); + + scopeResource.remove(); + + scopeResource.toRepresentation(); + } +} \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java new file mode 100644 index 0000000000..712daa06f5 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.adapter.example; + +import org.keycloak.testsuite.adapter.example.authorization.AbstractDefaultAuthzConfigAdapterTest; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +/** + * + * @author tkyjovsk + */ +@AppServerContainer("app-server-wildfly") +//@AdapterLibsLocationProperty("adapter.libs.wildfly") +public class WildflyDefaultAuthzConfigAdapterTest extends AbstractDefaultAuthzConfigAdapterTest { + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java new file mode 100644 index 0000000000..d9e2c343fa --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.testsuite.adapter.example; + +import org.keycloak.testsuite.adapter.example.authorization.AbstractPhotozExampleAdapterTest; +import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; + +/** + * + * @author tkyjovsk + */ +@AppServerContainer("app-server-wildfly") +//@AdapterLibsLocationProperty("adapter.libs.wildfly") +public class WildflyPhotozExampleAdapterTest extends AbstractPhotozExampleAdapterTest { + +} diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml index b53c73902a..1038d28cad 100644 --- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml +++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml @@ -284,6 +284,24 @@ ${project.version} war + + org.keycloak.testsuite + hello-world-authz-service + ${project.version} + war + + + org.keycloak.testsuite + photoz-html5-client + ${project.version} + war + + + org.keycloak.testsuite + photoz-restful-api + ${project.version} + war + ${examples.home} true @@ -309,7 +327,7 @@ integration-arquillian-test-apps-dist ${project.version} zip - **/*realm.json,**/testsaml.json + **/*realm.json,**/*authz-service.json,**/testsaml.json ${examples.home} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java index 0786eabec5..31b221b4a1 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java @@ -1,13 +1,12 @@ /* - * JBoss, Home of Professional Open Source. - * Copyright 2016 Red Hat, Inc., and individual contributors - * as indicated by the @author tags. + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.keycloak.testsuite.authorization; import org.apache.commons.collections.map.HashedMap; @@ -23,8 +21,6 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.junit.Before; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.Decision; -import org.keycloak.authorization.admin.representation.ResourceRepresentation; -import org.keycloak.authorization.admin.representation.ScopeRepresentation; import org.keycloak.authorization.common.KeycloakEvaluationContext; import org.keycloak.authorization.common.KeycloakIdentity; import org.keycloak.authorization.model.Policy; @@ -42,6 +38,8 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.util.JsonSerialization; import javax.ws.rs.client.Invocation; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java index f323265a50..4a6f9b61b7 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java @@ -19,8 +19,8 @@ package org.keycloak.testsuite.authorization; import org.junit.Test; -import org.keycloak.authorization.admin.representation.ResourceRepresentation; import org.keycloak.authorization.model.Resource; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation.Builder; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java index a4cc5513be..50ab943b1d 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java @@ -21,12 +21,13 @@ package org.keycloak.testsuite.authorization; import org.apache.commons.collections.map.HashedMap; import org.junit.Test; import org.keycloak.authorization.Decision.Effect; -import org.keycloak.authorization.admin.representation.PolicyRepresentation; -import org.keycloak.authorization.admin.representation.ResourceRepresentation; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.permission.ResourcePermission; import org.keycloak.authorization.policy.evaluation.DefaultEvaluation; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.util.JsonSerialization; import javax.ws.rs.client.Entity; @@ -329,7 +330,7 @@ public class ResourcePermissionManagementTest extends AbstractPhotozAdminTest { newPermission.setName("Album Resource Policy"); newPermission.setType("resource"); - newPermission.setDecisionStrategy(Policy.DecisionStrategy.AFFIRMATIVE); + newPermission.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE); HashedMap config = new HashedMap(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java index 839a813f2e..4566fe6b68 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java @@ -19,8 +19,8 @@ package org.keycloak.testsuite.authorization; import org.junit.Test; -import org.keycloak.authorization.admin.representation.ScopeRepresentation; import org.keycloak.authorization.model.Scope; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation.Builder; diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js index ecbe9d533e..216a5a9681 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js @@ -6,7 +6,7 @@ module.controller('ResourceServerCtrl', function($scope, realm, ResourceServer) }); }); -module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $location, $upload, realm, ResourceServer, client, AuthzDialog, Notifications) { +module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $location, $upload, $modal, realm, ResourceServer, client, AuthzDialog, Notifications) { $scope.realm = realm; $scope.client = client; @@ -31,8 +31,7 @@ module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $l } $scope.reset = function() { - $scope.server = angular.copy(data); - $scope.changed = false; + $route.reload(); } $scope.export = function() { @@ -54,38 +53,29 @@ module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $l delete $scope.settings } - $scope.onFileSelect = function($files) { - $scope.files = $files; + $scope.onFileSelect = function($fileContent) { + $scope.server = angular.copy(JSON.parse($fileContent)); + $scope.importing = true; }; - $scope.clearFileSelect = function() { - $scope.files = null; + $scope.viewImportDetails = function() { + $modal.open({ + templateUrl: resourceUrl + '/partials/modal/view-object.html', + controller: 'ObjectModalCtrl', + resolve: { + object: function () { + return $scope.server; + } + } + }) + }; + + $scope.import = function () { + ResourceServer.import({realm : realm.realm, client : client.id}, $scope.server, function() { + $route.reload(); + Notifications.success("The resource server has been updated."); + }); } - - $scope.uploadFile = function() { - //$files: an array of files selected, each file has name, size, and type. - for (var i = 0; i < $scope.files.length; i++) { - var $file = $scope.files[i]; - $scope.upload = $upload.upload({ - url: authUrl + '/admin/realms/' + $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server', //upload.php script, node.js route, or servlet url - // method: POST or PUT, - // headers: {'headerKey': 'headerValue'}, withCredential: true, - data: {myObj: ""}, - file: $file - /* set file formData name for 'Content-Desposition' header. Default: 'file' */ - //fileFormDataName: myFile, - /* customize how data is added to formData. See #40#issuecomment-28612000 for example */ - //formDataAppender: function(formData, key, val){} - }).progress(function(evt) { - console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total)); - }).success(function(data, status, headers) { - $route.reload(); - Notifications.success("The resource server has been updated."); - }).error(function() { - Notifications.error("The resource server can not be uploaded. Please verify the file."); - }); - } - }; }); }); diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js index c74db28386..e6114309d6 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js @@ -4,6 +4,7 @@ module.factory('ResourceServer', function($resource) { client: '@client' }, { 'update' : {method : 'PUT'}, + 'import' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/import', method : 'POST'}, 'settings' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/settings', method : 'GET'} }); }); diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html index 35d471d515..fcd9f67c1b 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html @@ -3,7 +3,6 @@ - + @@ -41,6 +41,14 @@ + diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html index 918cadf8ea..2b3cd70c60 100644 --- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html @@ -7,22 +7,24 @@
-
+
- + +
+
+
- {{files[0].name}}
Import a JSON file containing all settings for this resource server.
-
- - +
+ +
-
+
@@ -54,7 +56,7 @@
-
+
Export Settings Here you can export all settings for this resource server.
@@ -34,6 +33,7 @@ Permission Name Description TypeAssociated Policies
{{policy.name}} {{policy.description}} {{policy.type}} + No policies assigned. + + + {{policy.name}}{{$last ? '' : ', '}} + + +
No results