+
+
+ +

+
+
+
diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/js/app.js b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/js/app.js
new file mode 100755
index 00000000000..8913ead302e
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/js/app.js
@@ -0,0 +1,207 @@
+var module = angular.module('photoz-uma', ['ngRoute', 'ngResource']);
+
+var Identity = {};
+
+angular.element(document).ready(function ($http) {
+    var keycloakAuth = new Keycloak('keycloak.json');
+    Identity.loggedIn = false;
+    keycloakAuth.init({onLoad: 'login-required'}).success(function () {
+        Identity.loggedIn = true;
+        Identity.authz = keycloakAuth;
+        Identity.logout = function () {
+            Identity.loggedIn = false;
+            Identity.claim = {};
+            Identity.authc = null;
+            window.location = this.authz.authServerUrl + "/realms/photoz-uma/protocol/openid-connect/logout?redirect_uri=http://localhost:8080/photoz-uma-html5-client/index.html";
+            Identity.authz = null;
+        };
+        Identity.claim = {};
+        Identity.claim.name = Identity.authz.idTokenParsed.name;
+        Identity.hasRole = function (name) {
+            if (Identity.authz && Identity.authz.hasRealmRole(name)) {
+                return true;
+            }
+            return false;
+        };
+        Identity.isAdmin = function () {
+            return this.hasRole("admin");
+        };
+        Identity.authc = {};
+        Identity.authc.token = Identity.authz.token;
+        module.factory('Identity', function () {
+            return Identity;
+        });
+        angular.bootstrap(document, ["photoz-uma"]);
+    }).error(function () {
+        window.location.reload();
+    });
+});
+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.uma.rpt.rpt), null, '  ');
+    }
+
+    $scope.showAccessToken = function () {
+        document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.authc.token), null, '  ');
+    }
+
+    $scope.requestEntitlements = function () {
+        var request = new XMLHttpRequest();
+
+        request.open("GET", "http://localhost:8080/auth/realms/photoz-uma/authz/entitlement/photoz-uma-restful-api", true);
+        request.setRequestHeader("Authorization", "Bearer " + Identity.authc.token);
+        request.onreadystatechange = function () {
+            if (request.readyState == 4 && request.status == 200) {
+                Identity.uma.rpt = JSON.parse(request.responseText);
+            }
+        }
+
+        request.send(null);
+    }
+});
+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('/photoz-uma-restful-api/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('http://localhost:8080/photoz-uma-restful-api/album/:id');
+}]);
+module.factory('Profile', ['$resource', function ($resource) {
+    return $resource('http://localhost:8080/photoz-uma-restful-api/profile');
+}]);
+module.factory('AdminAlbum', ['$resource', function ($resource) {
+    return $resource('http://localhost:8080/photoz-uma-restful-api/admin/album/:id');
+}]);
+module.factory('authInterceptor', function ($q, $injector, $timeout, Identity) {
+    return {
+        request: function (request) {
+            document.getElementById("output").innerHTML = '';
+            if (Identity.uma && Identity.uma.rpt && request.url.indexOf('/authorize') == -1) {
+                retries = 0;
+                request.headers.Authorization = 'Bearer ' + Identity.uma.rpt.rpt;
+            } else {
+                request.headers.Authorization = 'Bearer ' + Identity.authc.token;
+            }
+            return request;
+        },
+        responseError: function (rejection) {
+            if (rejection.status == 403 || rejection.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) {
+                    if (rejection.status == 401) {
+                        console.log("Here");
+                        var authenticateHeader = rejection.headers('WWW-Authenticate');
+
+                        if (authenticateHeader.startsWith('UMA')) {
+                            var params = authenticateHeader.split(',');
+
+                            for (i = 0; i < params.length; i++) {
+                                var param = params[i].split('=');
+
+                                if (param[0] == 'ticket') {
+                                    var ticket = param[1].substring(1, param[1].length - 1).trim();
+
+                                    var data = JSON.stringify({
+                                        ticket: ticket,
+                                        rpt: Identity.uma ? Identity.uma.rpt.rpt : ""
+                                    });
+
+                                    var $http = $injector.get("$http");
+
+                                    var deferred = $q.defer();
+
+                                    $http.post('http://localhost:8080/auth/realms/photoz-uma/authz/authorize', data, {headers: {"Authorization": "Bearer " + Identity.authc.token}})
+                                        .then(function (authzResponse) {
+                                            if (authzResponse.data) {
+                                                Identity.uma = {};
+                                                Identity.uma.rpt = authzResponse.data;
+                                            }
+                                            deferred.resolve(rejection);
+                                        }, function (authzResponse) {
+                                            document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
+                                        });
+
+                                    var promise = deferred.promise;
+
+                                    return promise.then(function (res) {
+                                        if (!res.config.retry) {
+                                            res.config.retry = 1;
+                                        } else {
+                                            res.config.retry++;
+                                        }
+                                        return $http(res.config).then(function (response) {
+                                            return response;
+                                        });
+                                    });
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            return $q.reject(rejection);
+        }
+    };
+});
+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',
+    });
+});
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/keycloak.json b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/keycloak.json
new file mode 100644
index 00000000000..3e732417634
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/keycloak.json
@@ -0,0 +1,12 @@
+{
+  "realm": "photoz-uma",
+  "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "auth-server-url" : "http://localhost:8080/auth",
+  "ssl-required" : "external",
+  "resource" : "photoz-uma-html5-client",
+  "public-client" : true,
+  "use-resource-role-mappings": "false",
+  "scope" : {
+    "realm" : [ "user" ]
+  }
+}
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular-resource.min.js b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular-resource.min.js
new file mode 100644
index 00000000000..3f196c35383
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/jwt-decode.min.js b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/jwt-decode.min.js new file mode 100644 index 00000000000..f56f96737a5 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/admin/albums.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/admin/albums.html new file mode 100644 index 00000000000..bb381b9dd71 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/create.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/create.html new file mode 100644 index 00000000000..556693c7cec --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/create.html @@ -0,0 +1,7 @@ +

Create an Album

+ +
+ Name: + + +
diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/detail.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/detail.html new file mode 100644 index 00000000000..cf32df1480c --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/detail.html @@ -0,0 +1 @@ +

{{album.name}}

\ No newline at end of file diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/home.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/home.html new file mode 100644 index 00000000000..dc10c92b6e1 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/home.html @@ -0,0 +1,22 @@ +

Welcome To Photoz, {{Identity.claim.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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/profile.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/profile.html new file mode 100644 index 00000000000..c6f6750a4cb --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-realm.json b/examples/authz/photoz-uma/photoz-uma-realm.json new file mode 100644 index 00000000000..0230b616ac4 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-realm.json @@ -0,0 +1,111 @@ +{ + "realm": "photoz-uma", + "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-uma-restful-api", + "enabled": true, + "email": "service-account-photoz-uma-restful-api@placeholder.org", + "serviceAccountClientId": "photoz-uma-restful-api", + "clientRoles": { + "photoz-uma-restful-api" : ["uma_protection"] + } + } + ], + "roles": { + "realm": [ + { + "name": "user", + "description": "User privileges" + }, + { + "name": "admin", + "description": "Administrator privileges" + } + ] + }, + "clients": [ + { + "clientId": "photoz-uma-html5-client", + "enabled": true, + "adminUrl": "/photoz-uma-html5-client", + "baseUrl": "/photoz-uma-html5-client", + "publicClient": true, + "redirectUris": [ + "/photoz-uma-html5-client/*" + ], + "webOrigins": [ + "" + ] + }, + { + "clientId": "photoz-uma-restful-api", + "enabled": true, + "baseUrl": "/photoz-uma-restful-api", + "authorizationServicesEnabled" : true, + "redirectUris": [ + "/photoz-uma-restful-api/*" + ], + "secret": "secret" + } + ] +} diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api-authz-service.json b/examples/authz/photoz-uma/photoz-uma-restful-api-authz-service.json new file mode 100644 index 00000000000..60a4cd5c9af --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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-uma-authz-policy", + "sessionName": "MainOwnerSession", + "mavenArtifactGroupId": "org.keycloak", + "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.authz.context.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 Policy", + "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/examples/authz/photoz-uma/photoz-uma-restful-api/pom.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/pom.xml new file mode 100755 index 00000000000..0f04d950b93 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-restful-api/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + org.keycloak + keycloak-authz-photoz-uma-parent + 2.0.0.CR1-SNAPSHOT + ../pom.xml + + + photoz-uma-restful-api + war + + Keycloak Authz: Photoz UMA RESTful API + Photoz UMA 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 + + + 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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java new file mode 100644 index 00000000000..51755d899ed --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java new file mode 100644 index 00000000000..5b8377ced66 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java new file mode 100644 index 00000000000..b349e026d4a --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java new file mode 100644 index 00000000000..be2f1eb28c2 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java @@ -0,0 +1,132 @@ +package org.keycloak.example.photoz.album; + +import org.keycloak.authorization.client.AuthzClient; +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 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; + + @POST + @Consumes("application/json") + public Response create(@Context HttpServletRequest request, 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(@Context HttpServletRequest request) { + 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()); + + AuthzClient.create().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 = AuthzClient.create().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); + } + } +} diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java new file mode 100644 index 00000000000..be638b602e2 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java new file mode 100644 index 00000000000..978bdeabb5b --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java new file mode 100644 index 00000000000..08b7495f735 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/beans.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/beans.xml new file mode 100644 index 00000000000..957dc8ac430 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/beans.xml @@ -0,0 +1,7 @@ + + + + diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/persistence.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/persistence.xml new file mode 100644 index 00000000000..93231824053 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/keycloak.json b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/keycloak.json new file mode 100644 index 00000000000..528b1783550 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/keycloak.json @@ -0,0 +1,8 @@ +{ + "realm": "photoz-uma", + "auth-server-url": "http://localhost:8080/auth", + "resource": "photoz-uma-restful-api", + "credentials": { + "secret": "secret" + } +} \ No newline at end of file diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml new file mode 100644 index 00000000000..4b23be62d7d --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/keycloak.json new file mode 100644 index 00000000000..8b21c95c953 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/keycloak.json @@ -0,0 +1,50 @@ +{ + "realm": "photoz-uma", + "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "http://localhost:8080/auth", + "ssl-required": "external", + "resource": "photoz-uma-restful-api", + "bearer-only" : true, + "credentials": { + "secret": "secret" + }, + "policy-enforcer": { + "user-managed-access" : {}, + "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"] + }, + { + "method": "GET", + "scopes" : ["urn:photoz.com:scopes:album:view"] + } + ] + }, + { + "path" : "/profile" + }, + { + "name" : "Admin Resources", + "path" : "/admin/*" + } + ] + } +} \ No newline at end of file diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml new file mode 100644 index 00000000000..247448f2aea --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/web.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..92fd000d02c --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,41 @@ + + + + photoz-uma-restful-api + + + + All Resources + /* + + + user + + + + + + All Resources + /* + + + admin + + + + + KEYCLOAK + photoz-uma + + + + admin + + + + user + + diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/index.html b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/index.html new file mode 100644 index 00000000000..8318c86b357 --- /dev/null +++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/index.html @@ -0,0 +1 @@ +Test \ No newline at end of file diff --git a/examples/authz/photoz-uma/pom.xml b/examples/authz/photoz-uma/pom.xml new file mode 100755 index 00000000000..f0482e460f0 --- /dev/null +++ b/examples/authz/photoz-uma/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + + org.keycloak + keycloak-authz-example-parent + 2.0.0.CR1-SNAPSHOT + ../pom.xml + + + keycloak-authz-photoz-uma-parent + pom + + Keycloak Authz: PhotoZ UMA Example Application Parent + PhotoZ Example Application + + + photoz-uma-restful-api + photoz-uma-html5-client + photoz-uma-authz-policy + + diff --git a/examples/authz/pom.xml b/examples/authz/pom.xml new file mode 100755 index 00000000000..318d01eabfd --- /dev/null +++ b/examples/authz/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + + keycloak-examples-parent + org.keycloak + 2.0.0.CR1-SNAPSHOT + ../pom.xml + + + keycloak-authz-example-parent + pom + + Keycloak Authz: Examples Parent + + + + 1.8 + 1.8 + + + + photoz-uma + servlet-authz + hello-world + hello-world-authz-service + + diff --git a/examples/authz/servlet-authz/README.md b/examples/authz/servlet-authz/README.md new file mode 100644 index 00000000000..df528704dc6 --- /dev/null +++ b/examples/authz/servlet-authz/README.md @@ -0,0 +1,50 @@ +# About the Example Application + +This is a simple Servlet-based application that will introduce you to some of the main concepts around Keycloak Authorization Services. + +For this application, users can be regular users, premium users or administrators, where: + +* Regular users have very limited access. +* Premium users have access to the *premium area* +* Administrators have access to the *administration area* + +In Keycloak, all the paths being protected are resources on the server. + +This application will also show you how to create a dynamic menu with the permissions granted to an user. + +## Create the Example Realm and a Resource Server + +Considering that your AuthZ Server is up and running, log in to the Keycloak Administration Console. + +Now, create a new realm based on the following configuration file: + + examples/authz/servlet-authz/servlet-authz-realm.json + +That will import a pre-configured realm with everything you need to run this example. For more details about how to import a realm +into Keycloak, check the Keycloak's reference documentation. + +After importing that file, you'll have a new realm called ``servlet-authz``. + +Now, let's import another configuration using the Administration Console in order to configure the ``servlet-authz-app`` client application as a resource server with all resources, scopes, permissions and policies. + +Click on ``Authorization`` on the left side menu. Click on the ``Create`` button on the top of the resource server table. This will +open the page that allows you to create a new resource server. + +Click on the ``Select file`` button, which means you want to import a resource server configuration. Now select the file that is located at: + + examples/authz/servlet-authz/servlet-authz-app-config.json + +Now click ``Upload`` and a new resource server will be created based on the ``servlet-authz-app`` client application. + +## Deploy and Run the Example Applications + +To deploy the example applications, follow these steps: + + cd examples/authz/servlet-authz + mvn wildfly:deploy + +If everything is correct, you will be redirect to Keycloak login page. You can login to the application with the following credentials: + +* username: jdoe / password: jdoe (premium user) +* username: alice / password: alice (regular user) +* username: admin / password: admin (administrator) \ No newline at end of file diff --git a/examples/authz/servlet-authz/pom.xml b/examples/authz/servlet-authz/pom.xml new file mode 100755 index 00000000000..cfe82bc2d84 --- /dev/null +++ b/examples/authz/servlet-authz/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + + org.keycloak + keycloak-authz-example-parent + 2.0.0.CR1-SNAPSHOT + ../pom.xml + + + servlet-authz + war + + Keycloak Authz: Examples - Servlet Authorization + Servlet Authorization + + + + + org.keycloak + keycloak-authz-client + ${project.version} + provided + + + org.keycloak + keycloak-core + ${project.version} + provided + + + + + ${project.artifactId} + + + org.jboss.as.plugins + jboss-as-maven-plugin + + false + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + + diff --git a/examples/authz/servlet-authz/servlet-authz-app-config.json b/examples/authz/servlet-authz/servlet-authz-app-config.json new file mode 100644 index 00000000000..d5fb1cb8110 --- /dev/null +++ b/examples/authz/servlet-authz/servlet-authz-app-config.json @@ -0,0 +1,147 @@ +{ + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Admin Resource", + "uri": "/protected/admin/*", + "type": "http://servlet-authz/protected/admin", + "scopes": [ + { + "name": "urn:servlet-authz:protected:admin:access" + } + ] + }, + { + "name": "Protected Resource", + "uri": "/*", + "type": "http://servlet-authz/protected/resource", + "scopes": [ + { + "name": "urn:servlet-authz:protected:resource:access" + } + ] + }, + { + "name": "Premium Resource", + "uri": "/protected/premium/*", + "type": "urn:servlet-authz:protected:resource", + "scopes": [ + { + "name": "urn:servlet-authz:protected:premium:access" + } + ] + }, + { + "name": "Main Page", + "type": "urn:servlet-authz:protected:resource", + "scopes": [ + { + "name": "urn:servlet-authz:page:main:actionForAdmin" + }, + { + "name": "urn:servlet-authz:page:main:actionForUser" + }, + { + "name": "urn:servlet-authz:page:main:actionForPremiumUser" + } + ] + } + ], + "policies": [ + { + "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 Premium User Policy", + "description": "Defines that only premium users can do something", + "type": "role", + "logic": "POSITIVE", + "config": { + "roles": "[\"user_premium\"]" + } + }, + { + "name": "All Users Policy", + "description": "Defines that all users can do something", + "type": "aggregate", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "applyPolicies": "[\"Any User Policy\",\"Any Admin Policy\",\"Only Premium User Policy\"]" + } + }, + { + "name": "Premium Resource Permission", + "description": "A policy that defines access to premium resources", + "type": "resource", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Premium Resource\"]", + "applyPolicies": "[\"Only Premium User Policy\"]" + } + }, + { + "name": "Administrative Resource Permission", + "description": "A policy that defines access to administrative resources", + "type": "resource", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Admin Resource\"]", + "applyPolicies": "[\"Any Admin Policy\"]" + } + }, + { + "name": "Protected Resource Permission", + "description": "A policy that defines access to any protected resource", + "type": "resource", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "resources": "[\"Protected Resource\"]", + "applyPolicies": "[\"All Users Policy\"]" + } + }, + { + "name": "Action 1 on Main Page Resource Permission", + "description": "A policy that defines access to action 1 on the main page", + "type": "scope", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "scopes": "[\"urn:servlet-authz:page:main:actionForAdmin\"]", + "applyPolicies": "[\"Any Admin Policy\"]" + } + }, + { + "name": "Action 2 on Main Page Resource Permission", + "description": "A policy that defines access to action 2 on the main page", + "type": "scope", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "scopes": "[\"urn:servlet-authz:page:main:actionForUser\"]", + "applyPolicies": "[\"Any User Policy\"]" + } + }, + { + "name": "Action 3 on Main Page Resource Permission", + "description": "A policy that defines access to action 3 on the main page", + "type": "scope", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "scopes": "[\"urn:servlet-authz:page:main:actionForPremiumUser\"]", + "applyPolicies": "[\"Only Premium User Policy\"]" + } + } + ] +} \ No newline at end of file diff --git a/examples/authz/servlet-authz/servlet-authz-realm.json b/examples/authz/servlet-authz/servlet-authz-realm.json new file mode 100644 index 00000000000..371e4510f58 --- /dev/null +++ b/examples/authz/servlet-authz/servlet-authz-realm.json @@ -0,0 +1,95 @@ +{ + "realm": "servlet-authz", + "enabled": true, + "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, + "credentials": [ + { + "type": "password", + "value": "alice" + } + ], + "realmRoles": [ + "user" + ] + }, + { + "username": "jdoe", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "jdoe" + } + ], + "realmRoles": [ + "user", + "user_premium" + ] + }, + { + "username": "admin", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "admin" + } + ], + "realmRoles": [ + "user", + "admin" + ], + "clientRoles": { + "realm-management": [ + "realm-admin" + ] + } + }, + { + "username": "service-account-servlet-authz-app", + "enabled": true, + "serviceAccountClientId": "servlet-authz-app", + "clientRoles": { + "servlet-authz-app" : ["uma_protection"] + } + } + ], + "roles": { + "realm": [ + { + "name": "user", + "description": "User privileges" + }, + { + "name": "admin", + "description": "Administrator privileges" + }, + { + "name": "user_premium", + "description": "User Premium privileges" + } + ] + }, + "clients": [ + { + "clientId": "servlet-authz-app", + "enabled": true, + "baseUrl": "/servlet-authz-app", + "adminUrl": "/servlet-authz-app", + "bearerOnly": false, + "authorizationServicesEnabled": true, + "redirectUris": [ + "/servlet-authz-app/*" + ], + "secret": "secret" + } + ] +} diff --git a/examples/authz/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml b/examples/authz/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml new file mode 100644 index 00000000000..515ffa5c73d --- /dev/null +++ b/examples/authz/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json new file mode 100644 index 00000000000..7f375974176 --- /dev/null +++ b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json @@ -0,0 +1,12 @@ +{ + "realm": "servlet-authz", + "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url" : "http://localhost:8080/auth", + "ssl-required" : "external", + "resource" : "servlet-authz-app", + "public-client" : false, + "credentials": { + "secret": "secret" + }, + "policy-enforcer": {} +} \ No newline at end of file diff --git a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..14d0615978c --- /dev/null +++ b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,47 @@ + + + + servlet-authz-app + + + + All Resources + /* + + + user + + + + + + All Resources + /* + + + admin + + + + + KEYCLOAK + servlet-authz + + + + admin + + + + user + + + + 403 + /accessDenied.jsp + + + diff --git a/examples/authz/servlet-authz/src/main/webapp/accessDenied.jsp b/examples/authz/servlet-authz/src/main/webapp/accessDenied.jsp new file mode 100644 index 00000000000..be85c220042 --- /dev/null +++ b/examples/authz/servlet-authz/src/main/webapp/accessDenied.jsp @@ -0,0 +1,8 @@ +<%@ page import="org.keycloak.constants.ServiceUrlConstants" %> +<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %> + + +

You can not access this resource. Click ">here to log in as a different user.

+ + \ No newline at end of file diff --git a/examples/authz/servlet-authz/src/main/webapp/index.jsp b/examples/authz/servlet-authz/src/main/webapp/index.jsp new file mode 100755 index 00000000000..118f142c598 --- /dev/null +++ b/examples/authz/servlet-authz/src/main/webapp/index.jsp @@ -0,0 +1,38 @@ +<%@page import="org.keycloak.AuthorizationContext" %> +<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %> +<%@ page import="org.keycloak.constants.ServiceUrlConstants" %> +<%@ page import="org.keycloak.KeycloakSecurityContext" %> +<%@ page import="org.keycloak.representations.authorization.Permission" %> + +<% + KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); + AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext(); +%> + + + +

Click ">here to logout.

+

This is a public resource. Try to access one of these protected resources:

+ +

Dynamic Menu

+

User Premium

+

Administration

+ +

Your permissions are:

+ +
    + <% + for (Permission permission : authzContext.getPermissions()) { + %> +
  • +

    Resource: <%= permission.getResourceSetName() %>

    +

    ID: <%= permission.getResourceSetId() %>

    +

    Scopes: <%= permission.getScopes() %>

    +
  • + <% + } + %> +
+ + diff --git a/examples/authz/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp b/examples/authz/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp new file mode 100644 index 00000000000..554b250467a --- /dev/null +++ b/examples/authz/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp @@ -0,0 +1,8 @@ +<%@ page import="org.keycloak.constants.ServiceUrlConstants" %> +<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %> + + +

Only Administrators can access this page. Click ">here to logout.

+ + \ No newline at end of file diff --git a/examples/authz/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp b/examples/authz/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp new file mode 100644 index 00000000000..7240a98be2e --- /dev/null +++ b/examples/authz/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp @@ -0,0 +1,50 @@ +<%@page import="org.keycloak.AuthorizationContext" %> +<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %> +<%@ page import="org.keycloak.constants.ServiceUrlConstants" %> +<%@ page import="org.keycloak.KeycloakSecurityContext" %> + +<% + KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName()); + AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext(); +%> + + + +

Any authenticated user can access this page. Click ">here to logout.

+ +

Here is a dynamic menu built from the permissions returned by the server:

+ +
    + <% + if (authzContext.hasResourcePermission("Protected Resource")) { + %> +
  • + Do user thing +
  • + <% + } + %> + + <% + if (authzContext.hasResourcePermission("Premium Resource")) { + %> +
  • + Do user premium thing +
  • + <% + } + %> + + <% + if (authzContext.hasPermission("Admin Resource", "urn:servlet-authz:protected:admin:access")) { + %> +
  • + Do administration thing +
  • + <% + } + %> +
+ + \ No newline at end of file diff --git a/examples/authz/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp b/examples/authz/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp new file mode 100644 index 00000000000..f1725730f1b --- /dev/null +++ b/examples/authz/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp @@ -0,0 +1,9 @@ +<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %> +<%@ page import="org.keycloak.constants.ServiceUrlConstants" %> + + +

Only for premium users. Click ">here to logout.

+ + + \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index 3d55a42b5ec..622699eabc0 100755 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -66,5 +66,6 @@ themes saml ldap + authz diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index ded6c3c9178..c9311bd7523 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -21,6 +21,7 @@ import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.Configuration; import org.infinispan.configuration.cache.ConfigurationBuilder; import org.infinispan.configuration.global.GlobalConfigurationBuilder; +import org.infinispan.eviction.EvictionType; import org.infinispan.manager.DefaultCacheManager; import org.infinispan.manager.EmbeddedCacheManager; import org.infinispan.transaction.LockingMode; @@ -164,6 +165,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon Configuration counterCacheConfiguration = counterConfigBuilder.build(); cacheManager.defineConfiguration(InfinispanConnectionProvider.VERSION_CACHE_NAME, counterCacheConfiguration); + + cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, + new ConfigurationBuilder().eviction().type(EvictionType.COUNT).size(100).simpleCache(true).build()); } } diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java index 8a21def7ec4..0fc2bc0b5e0 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java @@ -32,6 +32,7 @@ public interface InfinispanConnectionProvider extends Provider { static final String OFFLINE_SESSION_CACHE_NAME = "offlineSessions"; static final String LOGIN_FAILURE_CACHE_NAME = "loginFailures"; static final String WORK_CACHE_NAME = "work"; + String AUTHORIZATION_CACHE_NAME = "authorization"; Cache getCache(String name); diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java new file mode 100644 index 00000000000..f1855d334ed --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java @@ -0,0 +1,407 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan; + +import org.infinispan.Cache; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction; +import org.keycloak.models.authorization.infinispan.entities.CachedPolicy; +import org.keycloak.models.entities.AbstractIdentifiableEntity; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * @author Pedro Igor + */ +public class CachedPolicyStore implements PolicyStore { + + private static final String POLICY_ID_CACHE_PREFIX = "policy-id-"; + + private final Cache cache; + private final KeycloakSession session; + private final CacheTransaction transaction; + private StoreFactory storeFactory; + private PolicyStore delegate; + + public CachedPolicyStore(KeycloakSession session, CacheTransaction transaction) { + this.session = session; + this.transaction = transaction; + InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class); + this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME); + } + + @Override + public Policy create(String name, String type, ResourceServer resourceServer) { + Policy policy = getDelegate().create(name, type, getStoreFactory().getResourceServerStore().findById(resourceServer.getId())); + + return createAdapter(new CachedPolicy(policy)); + } + + @Override + public void delete(String id) { + getDelegate().delete(id); + this.transaction.whenComplete(() -> cache.remove(id)); + } + + @Override + public Policy findById(String id) { + String cacheKeyForPolicy = getCacheKeyForPolicy(id); + List cached = this.cache.get(cacheKeyForPolicy); + + if (cached == null) { + Policy policy = getDelegate().findById(id); + + if (policy != null) { + return createAdapter(updatePolicyCache(policy)); + } + + return null; + } + + return createAdapter(cached.get(0)); + } + + @Override + public Policy findByName(String name, String resourceServerId) { + return getDelegate().findByName(name, resourceServerId); + } + + @Override + public List findByResourceServer(String resourceServerId) { + return getDelegate().findByResourceServer(resourceServerId); + } + + @Override + public List findByResource(String resourceId) { + List cache = new ArrayList<>(); + + for (Entry entry : this.cache.entrySet()) { + String cacheKey = (String) entry.getKey(); + + if (cacheKey.startsWith(POLICY_ID_CACHE_PREFIX)) { + List value = (List) entry.getValue(); + CachedPolicy policy = value.get(0); + + if (policy.getResourcesIds().contains(resourceId)) { + cache.add(findById(policy.getId())); + } + } + } + + if (cache.isEmpty()) { + getDelegate().findByResource(resourceId).forEach(policy -> cache.add(findById(updatePolicyCache(policy).getId()))); + } + + return cache; + } + + @Override + public List findByResourceType(String resourceType, String resourceServerId) { + List cache = new ArrayList<>(); + + for (Entry entry : this.cache.entrySet()) { + String cacheKey = (String) entry.getKey(); + + if (cacheKey.startsWith(POLICY_ID_CACHE_PREFIX)) { + List value = (List) entry.getValue(); + CachedPolicy policy = value.get(0); + + if (policy.getResourceServerId().equals(resourceServerId) && policy.getConfig().getOrDefault("defaultResourceType", "").equals(resourceType)) { + cache.add(findById(policy.getId())); + } + } + } + + if (cache.isEmpty()) { + getDelegate().findByResourceType(resourceType, resourceServerId).forEach(policy -> cache.add(findById(updatePolicyCache(policy).getId()))); + } + + return cache; + } + + @Override + public List findByScopeIds(List scopeIds, String resourceServerId) { + List cache = new ArrayList<>(); + + for (Entry entry : this.cache.entrySet()) { + String cacheKey = (String) entry.getKey(); + + if (cacheKey.startsWith(POLICY_ID_CACHE_PREFIX)) { + List value = (List) entry.getValue(); + CachedPolicy policy = value.get(0); + + for (String scopeId : policy.getScopesIds()) { + if (scopeIds.contains(scopeId)) { + cache.add(findById(policy.getId())); + break; + } + } + } + } + + if (cache.isEmpty()) { + getDelegate().findByScopeIds(scopeIds, resourceServerId).forEach(policy -> cache.add(findById(updatePolicyCache(policy).getId()))); + } + + return cache; + } + + @Override + public List findByType(String type) { + return getDelegate().findByType(type); + } + + @Override + public List findDependentPolicies(String id) { + return getDelegate().findDependentPolicies(id); + } + + private String getCacheKeyForPolicy(String policyId) { + return POLICY_ID_CACHE_PREFIX + policyId; + } + + private StoreFactory getStoreFactory() { + if (this.storeFactory == null) { + this.storeFactory = this.session.getProvider(StoreFactory.class); + } + + return this.storeFactory; + } + + private PolicyStore getDelegate() { + if (this.delegate == null) { + this.delegate = getStoreFactory().getPolicyStore(); + } + + return this.delegate; + } + + private Policy createAdapter(CachedPolicy cached) { + return new Policy() { + + private Policy updated; + + @Override + public String getId() { + return cached.getId(); + } + + @Override + public String getType() { + return cached.getType(); + } + + @Override + public DecisionStrategy getDecisionStrategy() { + return cached.getDecisionStrategy(); + } + + @Override + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + getDelegateForUpdate().setDecisionStrategy(decisionStrategy); + cached.setDecisionStrategy(decisionStrategy); + } + + @Override + public Logic getLogic() { + return cached.getLogic(); + } + + @Override + public void setLogic(Logic logic) { + getDelegateForUpdate().setLogic(logic); + cached.setLogic(logic); + } + + @Override + public Map getConfig() { + return cached.getConfig(); + } + + @Override + public void setConfig(Map config) { + getDelegateForUpdate().setConfig(config); + cached.setConfig(config); + } + + @Override + public String getName() { + return cached.getName(); + } + + @Override + public void setName(String name) { + getDelegateForUpdate().setName(name); + cached.setName(name); + } + + @Override + public String getDescription() { + return cached.getDescription(); + } + + @Override + public void setDescription(String description) { + getDelegateForUpdate().setDescription(description); + cached.setDescription(description); + } + + @Override + public ResourceServer getResourceServer() { + return getStoreFactory().getResourceServerStore().findById(cached.getResourceServerId()); + } + + @Override + public void addScope(Scope scope) { + getDelegateForUpdate().addScope(getStoreFactory().getScopeStore().findById(scope.getId())); + cached.addScope(scope); + } + + @Override + public void removeScope(Scope scope) { + getDelegateForUpdate().removeScope(getStoreFactory().getScopeStore().findById(scope.getId())); + cached.removeScope(scope); + } + + @Override + public void addAssociatedPolicy(Policy associatedPolicy) { + getDelegateForUpdate().addAssociatedPolicy(getStoreFactory().getPolicyStore().findById(associatedPolicy.getId())); + cached.addAssociatedPolicy(associatedPolicy); + } + + @Override + public void removeAssociatedPolicy(Policy associatedPolicy) { + getDelegateForUpdate().removeAssociatedPolicy(getStoreFactory().getPolicyStore().findById(associatedPolicy.getId())); + cached.removeAssociatedPolicy(associatedPolicy); + } + + @Override + public void addResource(Resource resource) { + getDelegateForUpdate().addResource(getStoreFactory().getResourceStore().findById(resource.getId())); + cached.addResource(resource); + } + + @Override + public void removeResource(Resource resource) { + getDelegateForUpdate().removeResource(getStoreFactory().getResourceStore().findById(resource.getId())); + cached.removeResource(resource); + } + + @Override + public Set getAssociatedPolicies() { + Set associatedPolicies = new HashSet<>(); + + for (String id : cached.getAssociatedPoliciesIds()) { + Policy cached = findById(id); + + if (cached != null) { + associatedPolicies.add(cached); + } + } + + return associatedPolicies; + } + + @Override + public Set getResources() { + Set resources = new HashSet<>(); + + for (String id : cached.getResourcesIds()) { + Resource cached = getStoreFactory().getResourceStore().findById(id); + + if (cached != null) { + resources.add(cached); + } + } + + return resources; + } + + @Override + public Set getScopes() { + Set scopes = new HashSet<>(); + + for (String id : cached.getScopesIds()) { + Scope cached = getStoreFactory().getScopeStore().findById(id); + + if (cached != null) { + scopes.add(cached); + } + } + + return scopes; + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + + if (getId() == null) return false; + + if (o == null || getClass() != o.getClass()) return false; + + Policy that = (Policy) o; + + if (!getId().equals(that.getId())) return false; + + return true; + + } + + @Override + public int hashCode() { + return getId()!=null ? getId().hashCode() : super.hashCode(); + } + + private Policy getDelegateForUpdate() { + if (this.updated == null) { + this.updated = getDelegate().findById(getId()); + if (this.updated == null) throw new IllegalStateException("Not found in database"); + transaction.whenComplete(() -> cache.evict(getCacheKeyForPolicy(getId()))); + } + + return this.updated; + } + }; + } + + private CachedPolicy updatePolicyCache(Policy policy) { + CachedPolicy cached = new CachedPolicy(policy); + List cache = new ArrayList<>(); + + cache.add(cached); + + this.cache.put(getCacheKeyForPolicy(policy.getId()), cache); + + return cached; + } + +} \ No newline at end of file diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java new file mode 100644 index 00000000000..5779ae1bda6 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java @@ -0,0 +1,187 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan; + +import org.infinispan.Cache; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction; +import org.keycloak.models.authorization.infinispan.entities.CachedResourceServer; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * @author Pedro Igor + */ +public class CachedResourceServerStore implements ResourceServerStore { + + private static final String RS_ID_CACHE_PREFIX = "rs-id-"; + + private final KeycloakSession session; + private final CacheTransaction transaction; + private StoreFactory storeFactory; + private ResourceServerStore delegate; + private final Cache cache; + + public CachedResourceServerStore(KeycloakSession session, CacheTransaction transaction) { + this.session = session; + this.transaction = transaction; + InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class); + this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME); + } + + @Override + public ResourceServer create(String clientId) { + ResourceServer resourceServer = getDelegate().create(clientId); + + return createAdapter(new CachedResourceServer(resourceServer)); + } + + @Override + public void delete(String id) { + getDelegate().delete(id); + this.transaction.whenComplete(() -> this.cache.remove(getCacheKeyForResourceServer(id))); + } + + @Override + public ResourceServer findById(String id) { + String cacheKeyForResourceServer = getCacheKeyForResourceServer(id); + List cached = this.cache.get(cacheKeyForResourceServer); + + if (cached == null) { + ResourceServer resourceServer = getDelegate().findById(id); + + if (resourceServer != null) { + return createAdapter(updateResourceServerCache(resourceServer)); + } + + return null; + } + + return createAdapter(cached.get(0)); + } + + @Override + public ResourceServer findByClient(String id) { + for (Map.Entry entry : this.cache.entrySet()) { + String cacheKey = (String) entry.getKey(); + + if (cacheKey.startsWith(RS_ID_CACHE_PREFIX)) { + List cache = (List) entry.getValue(); + ResourceServer resourceServer = cache.get(0); + + if (resourceServer.getClientId().equals(id)) { + return findById(resourceServer.getId()); + } + } + } + + ResourceServer resourceServer = getDelegate().findByClient(id); + + if (resourceServer != null) { + return findById(updateResourceServerCache(resourceServer).getId()); + } + + return null; + } + + private String getCacheKeyForResourceServer(String id) { + return RS_ID_CACHE_PREFIX + id; + } + + private ResourceServerStore getDelegate() { + if (this.delegate == null) { + this.delegate = getStoreFactory().getResourceServerStore(); + } + + return this.delegate; + } + + private StoreFactory getStoreFactory() { + if (this.storeFactory == null) { + this.storeFactory = session.getProvider(StoreFactory.class); + } + + return this.storeFactory; + } + private ResourceServer createAdapter(ResourceServer cached) { + return new ResourceServer() { + + private ResourceServer updated; + + @Override + public String getId() { + return cached.getId(); + } + + @Override + public String getClientId() { + return cached.getClientId(); + } + + @Override + public boolean isAllowRemoteResourceManagement() { + return cached.isAllowRemoteResourceManagement(); + } + + @Override + public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) { + getDelegateForUpdate().setAllowRemoteResourceManagement(allowRemoteResourceManagement); + cached.setAllowRemoteResourceManagement(allowRemoteResourceManagement); + } + + @Override + public PolicyEnforcementMode getPolicyEnforcementMode() { + return cached.getPolicyEnforcementMode(); + } + + @Override + public void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode) { + getDelegateForUpdate().setPolicyEnforcementMode(enforcementMode); + cached.setPolicyEnforcementMode(enforcementMode); + } + + private ResourceServer getDelegateForUpdate() { + if (this.updated == null) { + this.updated = getDelegate().findById(getId()); + if (this.updated == null) throw new IllegalStateException("Not found in database"); + transaction.whenComplete(() -> cache.evict(getCacheKeyForResourceServer(getId()))); + } + + return this.updated; + } + }; + } + + private CachedResourceServer updateResourceServerCache(ResourceServer resourceServer) { + CachedResourceServer cached = new CachedResourceServer(resourceServer); + List cache = new ArrayList<>(); + + cache.add(cached); + + this.cache.put(getCacheKeyForResourceServer(resourceServer.getId()), cache); + + return cached; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java new file mode 100644 index 00000000000..9e1ea14fba7 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java @@ -0,0 +1,299 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan; + +import org.infinispan.Cache; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction; +import org.keycloak.models.authorization.infinispan.entities.CachedResource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class CachedResourceStore implements ResourceStore { + + private static final String RESOURCE_ID_CACHE_PREFIX = "rsc-id-"; + + private final KeycloakSession session; + private final CacheTransaction transaction; + private StoreFactory storeFactory; + private ResourceStore delegate; + private final Cache cache; + + public CachedResourceStore(KeycloakSession session, CacheTransaction transaction) { + this.session = session; + InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class); + this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME); + this.transaction = transaction; + } + + @Override + public Resource create(String name, ResourceServer resourceServer, String owner) { + Resource resource = getDelegate().create(name, getStoreFactory().getResourceServerStore().findById(resourceServer.getId()), owner); + + return createAdapter(new CachedResource(resource)); + } + + @Override + public void delete(String id) { + this.cache.evict(getCacheKeyForResource(id)); + getDelegate().delete(id); + } + + @Override + public Resource findById(String id) { + String cacheKeyForResource = getCacheKeyForResource(id); + List cached = this.cache.get(cacheKeyForResource); + + if (cached == null) { + Resource resource = getDelegate().findById(id); + + if (resource != null) { + return createAdapter(updateResourceCache(resource)); + } + + return null; + } + + return createAdapter(cached.get(0)); + } + + @Override + public List findByOwner(String ownerId) { + List cache = new ArrayList<>(); + + for (Entry entry : this.cache.entrySet()) { + String cacheKey = (String) entry.getKey(); + + if (cacheKey.startsWith(RESOURCE_ID_CACHE_PREFIX)) { + List value = (List) entry.getValue(); + Resource resource = value.get(0); + + if (resource.getOwner().equals(ownerId)) { + cache.add(findById(resource.getId())); + } + } + } + + if (cache.isEmpty()) { + getDelegate().findByOwner(ownerId).forEach(resource -> cache.add(findById(updateResourceCache(resource).getId()))); + } + + return cache; + } + + @Override + public List findByResourceServer(String resourceServerId) { + return getDelegate().findByResourceServer(resourceServerId); + } + + @Override + public List findByScope(String... id) { + return getDelegate().findByScope(id); + } + + @Override + public Resource findByName(String name, String resourceServerId) { + for (Entry entry : this.cache.entrySet()) { + String cacheKey = (String) entry.getKey(); + + if (cacheKey.startsWith(RESOURCE_ID_CACHE_PREFIX)) { + List value = (List) entry.getValue(); + CachedResource resource = value.get(0); + + if (resource.getResourceServerId().equals(resourceServerId) && resource.getName().equals(name)) { + return findById(resource.getId()); + } + } + } + + Resource resource = getDelegate().findByName(name, resourceServerId); + + if (resource != null) { + return findById(updateResourceCache(resource).getId()); + } + + return null; + } + + @Override + public List findByType(String type) { + List cache = new ArrayList<>(); + + for (Entry entry : this.cache.entrySet()) { + String cacheKey = (String) entry.getKey(); + + if (cacheKey.startsWith(RESOURCE_ID_CACHE_PREFIX)) { + List value = (List) entry.getValue(); + Resource resource = value.get(0); + + if (resource.getType().equals(type)) { + cache.add(findById(resource.getId())); + } + } + } + + if (cache.isEmpty()) { + getDelegate().findByType(type).forEach(resource -> cache.add(findById(updateResourceCache(resource).getId()))); + } + + return cache; + } + + private String getCacheKeyForResource(String id) { + return RESOURCE_ID_CACHE_PREFIX + id; + } + + private ResourceStore getDelegate() { + if (this.delegate == null) { + this.delegate = getStoreFactory().getResourceStore(); + } + + return this.delegate; + } + + private StoreFactory getStoreFactory() { + if (this.storeFactory == null) { + this.storeFactory = session.getProvider(StoreFactory.class); + } + + return this.storeFactory; + } + + private Resource createAdapter(CachedResource cached) { + return new Resource() { + + private List scopes; + private Resource updated; + + @Override + public String getId() { + return cached.getId(); + } + + @Override + public String getName() { + return cached.getName(); + } + + @Override + public void setName(String name) { + getDelegateForUpdate().setName(name); + cached.setName(name); + } + + @Override + public String getUri() { + return cached.getUri(); + } + + @Override + public void setUri(String uri) { + getDelegateForUpdate().setUri(uri); + cached.setUri(uri); + } + + @Override + public String getType() { + return cached.getType(); + } + + @Override + public void setType(String type) { + getDelegateForUpdate().setType(type); + cached.setType(type); + } + + @Override + public List getScopes() { + List scopes = new ArrayList<>(); + + for (String id : cached.getScopesIds()) { + Scope cached = getStoreFactory().getScopeStore().findById(id); + + if (cached != null) { + scopes.add(cached); + } + } + + return scopes; + } + + @Override + public String getIconUri() { + return cached.getIconUri(); + } + + @Override + public void setIconUri(String iconUri) { + getDelegateForUpdate().setIconUri(iconUri); + cached.setIconUri(iconUri); + } + + @Override + public ResourceServer getResourceServer() { + return getStoreFactory().getResourceServerStore().findById(cached.getResourceServerId()); + } + + @Override + public String getOwner() { + return cached.getOwner(); + } + + @Override + public void updateScopes(Set scopes) { + getDelegateForUpdate().updateScopes(scopes.stream().map(scope -> getStoreFactory().getScopeStore().findById(scope.getId())).collect(Collectors.toSet())); + cached.updateScopes(scopes); + } + + private Resource getDelegateForUpdate() { + if (this.updated == null) { + this.updated = getDelegate().findById(getId()); + if (this.updated == null) throw new IllegalStateException("Not found in database"); + transaction.whenComplete(() -> cache.evict(getCacheKeyForResource(getId()))); + } + + return this.updated; + } + }; + } + + private CachedResource updateResourceCache(Resource resource) { + CachedResource cached = new CachedResource(resource); + List cache = new ArrayList<>(); + + cache.add(cached); + + this.cache.put(getCacheKeyForResource(resource.getId()), cache); + + return cached; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java new file mode 100644 index 00000000000..59126451d62 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java @@ -0,0 +1,195 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan; + +import org.infinispan.Cache; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction; +import org.keycloak.models.authorization.infinispan.entities.CachedScope; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; + +/** + * @author Pedro Igor + */ +public class CachedScopeStore implements ScopeStore { + + private static final String SCOPE_ID_CACHE_PREFIX = "scp-id-"; + + private final Cache cache; + private final KeycloakSession session; + private final CacheTransaction transaction; + private ScopeStore delegate; + private StoreFactory storeFactory; + + public CachedScopeStore(KeycloakSession session, CacheTransaction transaction) { + this.session = session; + this.transaction = transaction; + InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class); + this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME); + } + + @Override + public Scope create(String name, ResourceServer resourceServer) { + Scope scope = getDelegate().create(name, getStoreFactory().getResourceServerStore().findById(resourceServer.getId())); + + return createAdapter(new CachedScope(scope)); + } + + @Override + public void delete(String id) { + getDelegate().delete(id); + this.transaction.whenComplete(() -> cache.remove(getCacheKeyForScope(id))); + } + + @Override + public Scope findById(String id) { + String cacheKeyForScope = getCacheKeyForScope(id); + List cached = this.cache.get(cacheKeyForScope); + + if (cached == null) { + Scope scope = getDelegate().findById(id); + + if (scope != null) { + return createAdapter(updateScopeCache(scope)); + } + + return null; + } + + return createAdapter(cached.get(0)); + } + + @Override + public Scope findByName(String name, String resourceServerId) { + for (Entry entry : this.cache.entrySet()) { + String cacheKey = (String) entry.getKey(); + + if (cacheKey.startsWith(SCOPE_ID_CACHE_PREFIX)) { + List cache = (List) entry.getValue(); + CachedScope scope = cache.get(0); + + if (scope.getResourceServerId().equals(resourceServerId) && scope.getName().equals(name)) { + return findById(scope.getId()); + } + } + } + + Scope scope = getDelegate().findByName(name, resourceServerId); + + if (scope != null) { + return findById(updateScopeCache(scope).getId()); + } + + return null; + } + + @Override + public List findByResourceServer(String id) { + return getDelegate().findByResourceServer(id); + } + + private String getCacheKeyForScope(String id) { + return SCOPE_ID_CACHE_PREFIX + id; + } + + private ScopeStore getDelegate() { + if (this.delegate == null) { + this.delegate = getStoreFactory().getScopeStore(); + } + + return this.delegate; + } + + private StoreFactory getStoreFactory() { + if (this.storeFactory == null) { + this.storeFactory = session.getProvider(StoreFactory.class); + } + + return this.storeFactory; + } + + private Scope createAdapter(CachedScope cached) { + return new Scope() { + + private Scope updated; + + @Override + public String getId() { + return cached.getId(); + } + + @Override + public String getName() { + return cached.getName(); + } + + @Override + public void setName(String name) { + getDelegateForUpdate().setName(name); + cached.setName(name); + } + + @Override + public String getIconUri() { + return cached.getIconUri(); + } + + @Override + public void setIconUri(String iconUri) { + getDelegateForUpdate().setIconUri(iconUri); + cached.setIconUri(iconUri); + } + + @Override + public ResourceServer getResourceServer() { + return getStoreFactory().getResourceServerStore().findById(cached.getResourceServerId()); + } + + private Scope getDelegateForUpdate() { + if (this.updated == null) { + this.updated = getDelegate().findById(getId()); + if (this.updated == null) throw new IllegalStateException("Not found in database"); + transaction.whenComplete(() -> cache.evict(getCacheKeyForScope(getId()))); + } + + return this.updated; + } + }; + } + + private CachedScope updateScopeCache(Scope scope) { + CachedScope cached = new CachedScope(scope); + + List cache = new ArrayList(); + + cache.add(cached); + + this.transaction.whenComplete(() -> this.cache.put(getCacheKeyForScope(scope.getId()), cache)); + + return cached; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java new file mode 100644 index 00000000000..df9f26203ff --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java @@ -0,0 +1,109 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan; + +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakTransaction; +import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class InfinispanStoreFactoryProvider implements CachedStoreFactoryProvider { + + private final KeycloakSession session; + private final CacheTransaction transaction; + + InfinispanStoreFactoryProvider(KeycloakSession delegate) { + this.session = delegate; + this.transaction = new CacheTransaction(); + this.session.getTransaction().enlistAfterCompletion(transaction); + } + + @Override + public ResourceStore getResourceStore() { + return new CachedResourceStore(this.session, this.transaction); + } + + @Override + public ResourceServerStore getResourceServerStore() { + return new CachedResourceServerStore(this.session, this.transaction); + } + + @Override + public ScopeStore getScopeStore() { + return new CachedScopeStore(this.session, this.transaction); + } + + @Override + public PolicyStore getPolicyStore() { + return new CachedPolicyStore(this.session, this.transaction); + } + + @Override + public void close() { + + } + + static class CacheTransaction implements KeycloakTransaction { + + private List completeTasks = new ArrayList<>(); + + @Override + public void begin() { + + } + + @Override + public void commit() { + this.completeTasks.forEach(task -> task.run()); + } + + @Override + public void rollback() { + this.completeTasks.forEach(task -> task.run()); + } + + @Override + public void setRollbackOnly() { + + } + + @Override + public boolean getRollbackOnly() { + return false; + } + + @Override + public boolean isActive() { + return false; + } + + protected void whenComplete(Runnable task) { + this.completeTasks.add(task); + } + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreProviderFactory.java new file mode 100644 index 00000000000..8015fc2e1dc --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreProviderFactory.java @@ -0,0 +1,56 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan; + +import org.keycloak.Config; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider; +import org.keycloak.models.cache.authorization.CachedStoreProviderFactory; + +/** + * @author Pedro Igor + */ +public class InfinispanStoreProviderFactory implements CachedStoreProviderFactory { + @Override + public CachedStoreFactoryProvider create(KeycloakSession session) { + return new InfinispanStoreFactoryProvider(session); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "infinispan-authz-store-factory"; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedPolicy.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedPolicy.java new file mode 100644 index 00000000000..6c6230bfc36 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedPolicy.java @@ -0,0 +1,213 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan.entities; + +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.models.entities.AbstractIdentifiableEntity; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class CachedPolicy implements Policy { + + private String id; + private String type; + private DecisionStrategy decisionStrategy; + private Logic logic; + private Map config; + private String name; + private String description; + private String resourceServerId; + private Set associatedPoliciesIds; + private Set resourcesIds; + private Set scopesIds; + + public CachedPolicy(Policy policy) { + this.id = policy.getId(); + this.type = policy.getType(); + this.decisionStrategy = policy.getDecisionStrategy(); + this.logic = policy.getLogic(); + this.config = new HashMap(policy.getConfig()); + this.name = policy.getName(); + this.description = policy.getDescription(); + this.resourceServerId = policy.getResourceServer().getId(); + this.associatedPoliciesIds = policy.getAssociatedPolicies().stream().map(Policy::getId).collect(Collectors.toSet()); + this.resourcesIds = policy.getResources().stream().map(Resource::getId).collect(Collectors.toSet()); + this.scopesIds = policy.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()); + } + + public CachedPolicy(String id) { + this.id = id; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getType() { + return this.type; + } + + @Override + public DecisionStrategy getDecisionStrategy() { + return this.decisionStrategy; + } + + @Override + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + this.decisionStrategy = decisionStrategy; + } + + @Override + public Logic getLogic() { + return this.logic; + } + + @Override + public void setLogic(Logic logic) { + this.logic = logic; + } + + @Override + public Map getConfig() { + return this.config; + } + + @Override + public void setConfig(Map config) { + this.config = config; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public void setDescription(String description) { + this.description = description; + } + + @Override + public ResourceServer getResourceServer() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void addScope(Scope scope) { + this.scopesIds.add(scope.getId()); + } + + @Override + public void removeScope(Scope scope) { + this.scopesIds.remove(scope.getId()); + } + + @Override + public void addAssociatedPolicy(Policy associatedPolicy) { + this.associatedPoliciesIds.add(associatedPolicy.getId()); + } + + @Override + public void removeAssociatedPolicy(Policy associatedPolicy) { + this.associatedPoliciesIds.remove(associatedPolicy.getId()); + } + + @Override + public void addResource(Resource resource) { + this.resourcesIds.add(resource.getId()); + } + + @Override + public void removeResource(Resource resource) { + this.resourcesIds.add(resource.getId()); + } + + @Override + public Set getAssociatedPolicies() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Set getResources() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Set getScopes() { + throw new RuntimeException("Not implemented"); + } + + public Set getAssociatedPoliciesIds() { + return this.associatedPoliciesIds; + } + + public Set getResourcesIds() { + return this.resourcesIds; + } + + public Set getScopesIds() { + return this.scopesIds; + } + + public String getResourceServerId() { + return this.resourceServerId; + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + + if (this.id == null) return false; + + if (o == null || getClass() != o.getClass()) return false; + + AbstractIdentifiableEntity that = (AbstractIdentifiableEntity) o; + + if (!getId().equals(that.getId())) return false; + + return true; + + } + + @Override + public int hashCode() { + return id!=null ? id.hashCode() : super.hashCode(); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResource.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResource.java new file mode 100644 index 00000000000..8af333b2348 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResource.java @@ -0,0 +1,131 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan.entities; + +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class CachedResource implements Resource { + + private final String id; + private String resourceServerId; + private String iconUri; + private String owner; + private String type; + private String name; + private String uri; + private Set scopesIds; + + public CachedResource(Resource resource) { + this.id = resource.getId(); + this.name = resource.getName(); + this.uri = resource.getUri(); + this.type = resource.getType(); + this.owner = resource.getOwner(); + this.iconUri = resource.getIconUri(); + this.resourceServerId = resource.getResourceServer().getId(); + this.scopesIds = resource.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()); + } + + public CachedResource(String id) { + this.id = id; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getUri() { + return this.uri; + } + + @Override + public void setUri(String uri) { + this.uri = uri; + } + + @Override + public String getType() { + return this.type; + } + + @Override + public void setType(String type) { + this.type = type; + } + + @Override + public List getScopes() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getIconUri() { + return this.iconUri; + } + + @Override + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + @Override + public ResourceServer getResourceServer() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getOwner() { + return this.owner; + } + + @Override + public void updateScopes(Set scopes) { + this.scopesIds.clear(); + this.scopesIds.addAll(scopes.stream().map(Scope::getId).collect(Collectors.toSet())); + } + + public String getResourceServerId() { + return this.resourceServerId; + } + + public Set getScopesIds() { + return this.scopesIds; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResourceServer.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResourceServer.java new file mode 100644 index 00000000000..fe595108f28 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResourceServer.java @@ -0,0 +1,73 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan.entities; + +import org.keycloak.authorization.model.ResourceServer; + +/** + * @author Pedro Igor + */ +public class CachedResourceServer implements ResourceServer { + + private final String id; + private String clientId; + private boolean allowRemoteResourceManagement; + private PolicyEnforcementMode policyEnforcementMode; + + public CachedResourceServer(ResourceServer resourceServer) { + this.id = resourceServer.getId(); + this.clientId = resourceServer.getClientId(); + this.allowRemoteResourceManagement = resourceServer.isAllowRemoteResourceManagement(); + this.policyEnforcementMode = resourceServer.getPolicyEnforcementMode(); + } + + public CachedResourceServer(String id) { + this.id = id; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getClientId() { + return this.clientId; + } + + @Override + public boolean isAllowRemoteResourceManagement() { + return this.allowRemoteResourceManagement; + } + + @Override + public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) { + this.allowRemoteResourceManagement = allowRemoteResourceManagement; + } + + @Override + public PolicyEnforcementMode getPolicyEnforcementMode() { + return this.policyEnforcementMode; + } + + @Override + public void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode) { + this.policyEnforcementMode = enforcementMode; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedScope.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedScope.java new file mode 100644 index 00000000000..1cba6a24877 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedScope.java @@ -0,0 +1,78 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.authorization.infinispan.entities; + +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; + +/** + * @author Pedro Igor + */ +public class CachedScope implements Scope { + + private final String id; + private String resourceServerId; + private String name; + private String iconUri; + + public CachedScope(Scope scope) { + this.id = scope.getId(); + this.name = scope.getName(); + this.iconUri = scope.getIconUri(); + this.resourceServerId = scope.getResourceServer().getId(); + } + + public CachedScope(String id) { + this.id = id; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getIconUri() { + return this.iconUri; + } + + @Override + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + @Override + public ResourceServer getResourceServer() { + throw new RuntimeException("Not implemented"); + } + + public String getResourceServerId() { + return this.resourceServerId; + } +} diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.authorization.CachedStoreProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.authorization.CachedStoreProviderFactory new file mode 100644 index 00000000000..0a46bb3ad2c --- /dev/null +++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.authorization.CachedStoreProviderFactory @@ -0,0 +1,19 @@ +# +# JBoss, Home of Professional Open Source. +# Copyright 2016 Red Hat, Inc., and individual 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. +# + +org.keycloak.models.authorization.infinispan.InfinispanStoreProviderFactory \ No newline at end of file diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml index c1176f3a362..47035c58aa2 100755 --- a/model/jpa/pom.xml +++ b/model/jpa/pom.xml @@ -30,6 +30,11 @@ Keycloak Model JPA + + 1.8 + 1.8 + + org.bouncycastle diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java new file mode 100644 index 00000000000..ddaf6371fac --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java @@ -0,0 +1,251 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.entities; + +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.Scope; +import org.keycloak.models.entities.AbstractIdentifiableEntity; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.MapKeyColumn; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Pedro Igor + */ +@Entity +@Table(name = "RESOURCE_SERVER_POLICY", uniqueConstraints = { + @UniqueConstraint(columnNames = {"NAME", "RESOURCE_SERVER_ID"}) +}) +public class PolicyEntity implements Policy { + + @Id + @Column(name="ID", length = 36) + @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL + private String id; + + @Column(name = "NAME") + private String name; + + @Column(name = "DESCRIPTION") + private String description; + + @Column(name = "TYPE") + private String type; + + @Column(name = "DECISION_STRATEGY") + private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; + + @Column(name = "LOGIC") + private Logic logic = Logic.POSITIVE; + + @ElementCollection + @MapKeyColumn(name="NAME") + @Column(name="VALUE", columnDefinition = "TEXT") + @CollectionTable(name="POLICY_CONFIG", joinColumns={ @JoinColumn(name="POLICY_ID") }) + private Map config = new HashMap(); + + @ManyToOne(optional = false) + @JoinColumn(name = "RESOURCE_SERVER_ID") + private ResourceServerEntity resourceServer; + + @ManyToMany(fetch = FetchType.LAZY, cascade = {}) + @JoinTable(name = "ASSOCIATED_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "ASSOCIATED_POLICY_ID")) + private Set associatedPolicies = new HashSet<>(); + + @ManyToMany(fetch = FetchType.LAZY, cascade = {}) + @JoinTable(name = "RESOURCE_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "RESOURCE_ID")) + private Set resources = new HashSet<>(); + + @ManyToMany(fetch = FetchType.EAGER, cascade = {}) + @JoinTable(name = "SCOPE_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "SCOPE_ID")) + private Set scopes = new HashSet<>(); + + @Override + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + @Override + public DecisionStrategy getDecisionStrategy() { + return this.decisionStrategy; + } + + @Override + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + this.decisionStrategy = decisionStrategy; + } + + @Override + public Logic getLogic() { + return this.logic; + } + + @Override + public void setLogic(Logic logic) { + this.logic = logic; + } + + @Override + public Map getConfig() { + return this.config; + } + + @Override + public void setConfig(Map config) { + this.config = config; + } + + @Override + public String getName() { + return this.name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public void setDescription(String description) { + this.description = description; + } + + @Override + public ResourceServerEntity getResourceServer() { + return this.resourceServer; + } + + public void setResourceServer(ResourceServerEntity resourceServer) { + this.resourceServer = resourceServer; + } + + @Override + public

Set

getAssociatedPolicies() { + return (Set

) this.associatedPolicies; + } + + public void setAssociatedPolicies(Set associatedPolicies) { + this.associatedPolicies = associatedPolicies; + } + + @Override + public Set getResources() { + return this.resources; + } + + public void setResources(Set resources) { + this.resources = resources; + } + + @Override + public Set getScopes() { + return this.scopes; + } + + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + @Override + public void addScope(Scope scope) { + getScopes().add((ScopeEntity) scope); + } + + @Override + public void removeScope(Scope scope) { + getScopes().remove(scope); + } + + @Override + public void addAssociatedPolicy(Policy associatedPolicy) { + getAssociatedPolicies().add(associatedPolicy); + } + + @Override + public void removeAssociatedPolicy(Policy associatedPolicy) { + getAssociatedPolicies().remove(associatedPolicy); + } + + @Override + public void addResource(Resource resource) { + getResources().add((ResourceEntity) resource); + } + + @Override + public void removeResource(Resource resource) { + getResources().remove(resource); + } + + @Override + public boolean equals(Object o) { + if (o == this) return true; + + if (this.id == null) return false; + + if (o == null || getClass() != o.getClass()) return false; + + AbstractIdentifiableEntity that = (AbstractIdentifiableEntity) o; + + if (!getId().equals(that.getId())) return false; + + return true; + + } + + @Override + public int hashCode() { + return id!=null ? id.hashCode() : super.hashCode(); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java new file mode 100644 index 00000000000..7cb1a6f565e --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java @@ -0,0 +1,190 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.entities; + +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.Scope; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Pedro Igor + */ +@Entity +@Table(name = "RESOURCE_SERVER_RESOURCE", uniqueConstraints = { + @UniqueConstraint(columnNames = {"NAME", "RESOURCE_SERVER_ID", "OWNER"}) +}) +public class ResourceEntity implements Resource { + + @Id + @Column(name="ID", length = 36) + @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL + private String id; + + @Column(name = "NAME") + private String name; + + @Column(name = "URI") + private String uri; + + @Column(name = "TYPE") + private String type; + + @Column(name = "ICON_URI") + private String iconUri; + + @Column(name = "OWNER") + private String owner; + + @ManyToOne(optional = false) + @JoinColumn(name = "RESOURCE_SERVER_ID") + private ResourceServerEntity resourceServer; + + @ManyToMany(fetch = FetchType.LAZY, cascade = {}) + @JoinTable(name = "RESOURCE_SCOPE", joinColumns = @JoinColumn(name = "RESOURCE_ID"), inverseJoinColumns = @JoinColumn(name = "SCOPE_ID")) + private List scopes = new ArrayList<>(); + + @ManyToMany(fetch = FetchType.LAZY, cascade = {}) + @JoinTable(name = "RESOURCE_POLICY", joinColumns = @JoinColumn(name = "RESOURCE_ID"), inverseJoinColumns = @JoinColumn(name = "POLICY_ID")) + private List policies = new ArrayList<>(); + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getUri() { + return uri; + } + + @Override + public void setUri(String uri) { + this.uri = uri; + } + + @Override + public String getType() { + return type; + } + + @Override + public void setType(String type) { + this.type = type; + } + + @Override + public List getScopes() { + return this.scopes; + } + + @Override + public String getIconUri() { + return iconUri; + } + + @Override + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + @Override + public ResourceServerEntity getResourceServer() { + return resourceServer; + } + + public void setResourceServer(ResourceServerEntity resourceServer) { + this.resourceServer = resourceServer; + } + + public String getOwner() { + return this.owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public List getPolicies() { + return this.policies; + } + + public void updateScopes(Set toUpdate) { + for (Scope scope : toUpdate) { + boolean hasScope = false; + + for (Scope existingScope : this.scopes) { + if (existingScope.equals(scope)) { + hasScope = true; + } + } + + if (!hasScope) { + this.scopes.add((ScopeEntity) scope); + } + } + + for (Scope scopeModel : new HashSet(this.scopes)) { + boolean hasScope = false; + + for (Scope scope : toUpdate) { + if (scopeModel.equals(scope)) { + hasScope = true; + } + } + + if (!hasScope) { + this.scopes.remove(scopeModel); + } + } + } + + public void setPolicies(List policies) { + this.policies = policies; + } +} diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceServerEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceServerEntity.java new file mode 100644 index 00000000000..b74b2310673 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceServerEntity.java @@ -0,0 +1,113 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.entities; + +import org.keycloak.authorization.model.ResourceServer; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import java.util.List; + +/** + * @author Pedro Igor + */ +@Entity +@Table(name = "RESOURCE_SERVER", uniqueConstraints = {@UniqueConstraint(columnNames = "CLIENT_ID")}) +public class ResourceServerEntity implements ResourceServer { + + @Id + @Column(name="ID", length = 36) + @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL + private String id; + + @Column(name = "CLIENT_ID") + private String clientId; + + @Column(name = "ALLOW_RS_REMOTE_MGMT") + private boolean allowRemoteResourceManagement; + + @Column(name = "POLICY_ENFORCE_MODE") + private PolicyEnforcementMode policyEnforcementMode = PolicyEnforcementMode.ENFORCING; + + @OneToMany(mappedBy = "resourceServer") + private List resources; + + @OneToMany (mappedBy = "resourceServer") + private List scopes; + + @Override + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getClientId() { + return this.clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + @Override + public boolean isAllowRemoteResourceManagement() { + return this.allowRemoteResourceManagement; + } + + @Override + public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) { + this.allowRemoteResourceManagement = allowRemoteResourceManagement; + } + + @Override + public PolicyEnforcementMode getPolicyEnforcementMode() { + return this.policyEnforcementMode; + } + + @Override + public void setPolicyEnforcementMode(PolicyEnforcementMode policyEnforcementMode) { + this.policyEnforcementMode = policyEnforcementMode; + } + + public List getResources() { + return this.resources; + } + + public void setResources(final List resources) { + this.resources = resources; + } + + public List getScopes() { + return this.scopes; + } + + public void setScopes(final List scopes) { + this.scopes = scopes; + } +} diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java new file mode 100644 index 00000000000..99f8b41edb8 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java @@ -0,0 +1,126 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.entities; + +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Scope; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @author Pedro Igor + */ +@Entity +@Table(name = "RESOURCE_SERVER_SCOPE", uniqueConstraints = { + @UniqueConstraint(columnNames = {"NAME", "RESOURCE_SERVER_ID"}) +}) +public class ScopeEntity implements Scope { + + @Id + @Column(name="ID", length = 36) + @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL + private String id; + + @Column(name = "NAME") + private String name; + + @Column(name = "ICON_URI") + private String iconUri; + + @ManyToOne(optional = false) + @JoinColumn(name = "RESOURCE_SERVER_ID") + private ResourceServerEntity resourceServer; + + @ManyToMany(fetch = FetchType.LAZY, cascade = {}) + @JoinTable(name = "SCOPE_POLICY", joinColumns = @JoinColumn(name = "SCOPE_ID"), inverseJoinColumns = @JoinColumn(name = "POLICY_ID")) + private List policies = new ArrayList<>(); + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + this.name = name; + } + + @Override + public String getIconUri() { + return iconUri; + } + + @Override + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + @Override + public ResourceServerEntity getResourceServer() { + return resourceServer; + } + + public List getPolicies() { + return this.policies; + } + + public void setPolicies(List policies) { + this.policies = policies; + } + + public void setResourceServer(final ResourceServerEntity resourceServer) { + this.resourceServer = resourceServer; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ScopeEntity that = (ScopeEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAAuthorizationStoreFactory.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAAuthorizationStoreFactory.java new file mode 100644 index 00000000000..87888848ee6 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAAuthorizationStoreFactory.java @@ -0,0 +1,56 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.store; + +import org.keycloak.Config; +import org.keycloak.authorization.store.AuthorizationStoreFactory; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.models.KeycloakSession; + +import javax.persistence.EntityManager; + +/** + * @author Pedro Igor + */ +public class JPAAuthorizationStoreFactory implements AuthorizationStoreFactory { + @Override + public StoreFactory create(KeycloakSession session) { + return new JPAStoreFactory(getEntityManager(session)); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "jpa"; + } + + private EntityManager getEntityManager(KeycloakSession session) { + return session.getProvider(JpaConnectionProvider.class).getEntityManager(); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java new file mode 100644 index 00000000000..b05f933e92a --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java @@ -0,0 +1,157 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.store; + +import org.keycloak.authorization.jpa.entities.PolicyEntity; +import org.keycloak.authorization.jpa.entities.ResourceServerEntity; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.models.utils.KeycloakModelUtils; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.Query; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class JPAPolicyStore implements PolicyStore { + + private final EntityManager entityManager; + + public JPAPolicyStore(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public Policy create(String name, String type, ResourceServer resourceServer) { + PolicyEntity entity = new PolicyEntity(); + + entity.setId(KeycloakModelUtils.generateId()); + entity.setName(name); + entity.setType(type); + entity.setResourceServer((ResourceServerEntity) resourceServer); + + this.entityManager.persist(entity); + + return entity; + } + + public EntityManager getEntityManager() { + return this.entityManager; + } + + @Override + public void delete(String id) { + Policy policy = findById(id); + + if (policy != null) { + getEntityManager().remove(policy); + } + } + + + @Override + public Policy findById(String id) { + return getEntityManager().find(PolicyEntity.class, id); + } + + @Override + public Policy findByName(String name, String resourceServerId) { + try { + Query query = getEntityManager().createQuery("from PolicyEntity where name = :name and resourceServer.id = :serverId"); + + query.setParameter("name", name); + query.setParameter("serverId", resourceServerId); + + return (Policy) query.getSingleResult(); + } catch (NoResultException nre) { + return null; + } + } + + @Override + public List findByResourceServer(final String resourceServerId) { + Query query = getEntityManager().createQuery("from PolicyEntity where resourceServer.id = :serverId"); + + query.setParameter("serverId", resourceServerId); + + return query.getResultList(); + } + + @Override + public List findByResource(final String resourceId) { + Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.resources r where r.id = :resourceId"); + + query.setParameter("resourceId", resourceId); + + return query.getResultList(); + } + + @Override + public List findByResourceType(final String resourceType, String resourceServerId) { + List policies = new ArrayList<>(); + Query query = getEntityManager().createQuery("from PolicyEntity where resourceServer.id = :serverId"); + + query.setParameter("serverId", resourceServerId); + + List models = query.getResultList(); + + for (Policy policy : models) { + String defaultType = policy.getConfig().get("defaultResourceType"); + + if (defaultType != null && defaultType.equals(resourceType) && policy.getResources().isEmpty()) { + policies.add(policy); + } + } + + return policies; + } + + @Override + public List findByScopeIds(List scopeIds, String resourceServerId) { + Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.scopes s where p.resourceServer.id = :serverId and s.id in (:scopeIds) and p.resources is empty group by p.id order by p.name"); + + query.setParameter("serverId", resourceServerId); + query.setParameter("scopeIds", scopeIds); + + return query.getResultList(); + } + + @Override + public List findByType(String type) { + Query query = getEntityManager().createQuery("select p from PolicyEntity p where p.type = :type"); + + query.setParameter("type", type); + + return query.getResultList(); + } + + @Override + public List findDependentPolicies(String policyId) { + Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.associatedPolicies ap where ap.id in (:policyId)"); + + query.setParameter("policyId", Arrays.asList(policyId)); + + return query.getResultList(); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java new file mode 100644 index 00000000000..51d0369245f --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java @@ -0,0 +1,75 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.store; + +import org.keycloak.authorization.jpa.entities.ResourceServerEntity; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.models.utils.KeycloakModelUtils; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class JPAResourceServerStore implements ResourceServerStore { + + private final EntityManager entityManager; + + public JPAResourceServerStore(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public ResourceServer create(String clientId) { + ResourceServerEntity entity = new ResourceServerEntity(); + + entity.setId(KeycloakModelUtils.generateId()); + entity.setClientId(clientId); + + this.entityManager.persist(entity); + + return entity; + } + + @Override + public void delete(String id) { + this.entityManager.remove(findById(id)); + } + + @Override + public ResourceServer findById(String id) { + return entityManager.find(ResourceServerEntity.class, id); + } + + @Override + public ResourceServer findByClient(final String clientId) { + Query query = entityManager.createQuery("from ResourceServerEntity where clientId = :clientId"); + + query.setParameter("clientId", clientId); + List result = query.getResultList(); + + if (result.isEmpty()) { + return null; + } + + return (ResourceServer) result.get(0); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java new file mode 100644 index 00000000000..802989c9735 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java @@ -0,0 +1,132 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.store; + +import org.keycloak.authorization.jpa.entities.ResourceEntity; +import org.keycloak.authorization.jpa.entities.ResourceServerEntity; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.models.utils.KeycloakModelUtils; + +import javax.persistence.EntityManager; +import javax.persistence.Query; +import java.util.Arrays; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class JPAResourceStore implements ResourceStore { + + private final EntityManager entityManager; + + public JPAResourceStore(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public Resource create(String name, ResourceServer resourceServer, String owner) { + if (!(resourceServer instanceof ResourceServerEntity)) { + throw new RuntimeException("Unexpected type [" + resourceServer.getClass() + "]."); + } + + ResourceEntity entity = new ResourceEntity(); + + entity.setId(KeycloakModelUtils.generateId()); + entity.setName(name); + entity.setResourceServer((ResourceServerEntity) resourceServer); + entity.setOwner(owner); + + this.entityManager.persist(entity); + + return entity; + } + + @Override + public void delete(String id) { + Resource resource = findById(id); + + resource.getScopes().clear(); + + if (resource != null) { + this.entityManager.remove(resource); + } + } + + @Override + public Resource findById(String id) { + if (id == null) { + return null; + } + + return entityManager.find(ResourceEntity.class, id); + } + + @Override + public List findByOwner(String ownerId) { + Query query = entityManager.createQuery("from ResourceEntity where owner = :ownerId"); + + query.setParameter("ownerId", ownerId); + + return query.getResultList(); + } + + @Override + public List findByResourceServer(String resourceServerId) { + Query query = entityManager.createQuery("from ResourceEntity where resourceServer.id = :serverId"); + + query.setParameter("serverId", resourceServerId); + + return query.getResultList(); + } + + @Override + public List findByScope(String... id) { + Query query = entityManager.createQuery("from ResourceEntity r inner join r.scopes s where s.id in (:scopeIds)"); + + query.setParameter("scopeIds", Arrays.asList(id)); + + return query.getResultList(); + } + + @Override + public Resource findByName(String name, String resourceServerId) { + Query query = entityManager.createQuery("from ResourceEntity where resourceServer.id = :serverId and name = :name"); + + query.setParameter("serverId", resourceServerId); + query.setParameter("name", name); + + List result = query.getResultList(); + + if (!result.isEmpty()) { + return result.get(0); + } + + return null; + } + + @Override + public List findByType(String type) { + Query query = entityManager.createQuery("from ResourceEntity where type = :type"); + + query.setParameter("type", type); + + return query.getResultList(); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java new file mode 100644 index 00000000000..cc9a956498b --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java @@ -0,0 +1,88 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.store; + +import org.keycloak.authorization.jpa.entities.ResourceServerEntity; +import org.keycloak.authorization.jpa.entities.ScopeEntity; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.models.utils.KeycloakModelUtils; + +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.Query; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class JPAScopeStore implements ScopeStore { + + private final EntityManager entityManager; + + public JPAScopeStore(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public Scope create(final String name, final ResourceServer resourceServer) { + ScopeEntity entity = new ScopeEntity(); + + entity.setId(KeycloakModelUtils.generateId()); + entity.setName(name); + entity.setResourceServer((ResourceServerEntity) resourceServer); + + this.entityManager.persist(entity); + + return entity; + } + + @Override + public void delete(String id) { + this.entityManager.remove(findById(id)); + } + + @Override + public Scope findById(String id) { + return entityManager.find(ScopeEntity.class, id); + } + + @Override + public Scope findByName(String name, String resourceServerId) { + try { + Query query = entityManager.createQuery("select s from ScopeEntity s inner join s.resourceServer rs where rs.id = :resourceServerId and name = :name"); + + query.setParameter("name", name); + query.setParameter("resourceServerId", resourceServerId); + + return (Scope) query.getSingleResult(); + } catch (NoResultException nre) { + return null; + } + } + + @Override + public List findByResourceServer(final String serverId) { + Query query = entityManager.createQuery("from ScopeEntity where resourceServer.id = :serverId"); + + query.setParameter("serverId", serverId); + + return query.getResultList(); + } +} diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java new file mode 100644 index 00000000000..5dad6af60b6 --- /dev/null +++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.jpa.store; + +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.authorization.store.StoreFactory; + +import javax.persistence.EntityManager; + +/** + * @author Pedro Igor + */ +public class JPAStoreFactory implements StoreFactory { + + private final EntityManager entityManager; + + public JPAStoreFactory(EntityManager entityManager) { + this.entityManager = entityManager; + } + + @Override + public PolicyStore getPolicyStore() { + return new JPAPolicyStore(this.entityManager); + } + + @Override + public ResourceServerStore getResourceServerStore() { + return new JPAResourceServerStore(this.entityManager); + } + + @Override + public ResourceStore getResourceStore() { + return new JPAResourceStore(this.entityManager); + } + + @Override + public ScopeStore getScopeStore() { + return new JPAScopeStore(this.entityManager); + } + + @Override + public void close() { + + } +} diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index e504f2bcc76..5ceab0bbaad 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -39,11 +39,9 @@ import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; /** @@ -127,7 +125,7 @@ public class JpaRealmProvider implements RealmProvider { return false; } em.refresh(realm); - RealmAdapter adapter = new RealmAdapter(session, em, realm); + final RealmAdapter adapter = new RealmAdapter(session, em, realm); session.users().preRemove(adapter); realm.getDefaultGroups().clear(); @@ -159,6 +157,19 @@ public class JpaRealmProvider implements RealmProvider { em.flush(); em.clear(); + + session.getKeycloakSessionFactory().publish(new RealmModel.RealmRemovedEvent() { + @Override + public RealmModel getRealm() { + return adapter; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); + return true; } @@ -268,6 +279,19 @@ public class JpaRealmProvider implements RealmProvider { int val = em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate(); em.remove(roleEntity); + + session.getKeycloakSessionFactory().publish(new RoleContainerModel.RoleRemovedEvent() { + @Override + public RoleModel getRole() { + return role; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); + em.flush(); return true; @@ -451,7 +475,7 @@ public class JpaRealmProvider implements RealmProvider { @Override public boolean removeClient(String id, RealmModel realm) { - ClientModel client = getClientById(id, realm); + final ClientModel client = getClientById(id, realm); if (client == null) return false; session.users().preRemove(realm, client); @@ -460,17 +484,32 @@ public class JpaRealmProvider implements RealmProvider { client.removeRole(role); } - ClientEntity clientEntity = ((ClientAdapter)client).getEntity(); + em.createNamedQuery("deleteScopeMappingByClient").setParameter("client", clientEntity).executeUpdate(); em.flush(); + + session.getKeycloakSessionFactory().publish(new RealmModel.ClientRemovedEvent() { + @Override + public ClientModel getClient() { + return client; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); + em.remove(clientEntity); // i have no idea why, but this needs to come before deleteScopeMapping + try { em.flush(); } catch (RuntimeException e) { logger.errorv("Unable to delete client entity: {0} from realm {1}", client.getClientId(), realm.getName()); throw e; } + return true; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 040d6eb38e4..593bae5dda9 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -25,6 +25,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredActionProviderModel; +import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserFederationProviderModel; @@ -115,6 +116,17 @@ public class JpaUserProvider implements UserProvider { UserEntity userEntity = em.find(UserEntity.class, user.getId()); if (userEntity == null) return false; removeUser(userEntity); + session.getKeycloakSessionFactory().publish(new UserModel.UserRemovedEvent() { + @Override + public UserModel getUser() { + return user; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); return true; } @@ -134,6 +146,7 @@ public class JpaUserProvider implements UserProvider { if (user != null) { em.remove(user); } + em.flush(); } diff --git a/model/jpa/src/main/resources/META-INF/jpa-authz-changelog-2.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-authz-changelog-2.0.0.xml new file mode 100755 index 00000000000..6e5e30c3d34 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-authz-changelog-2.0.0.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.0.0.xml new file mode 100755 index 00000000000..72af496995a --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.0.0.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml index fd1f0f6e72b..131ce056724 100755 --- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml @@ -32,4 +32,5 @@ + diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml index 7b6ee3760c6..b90c0fc232b 100755 --- a/model/jpa/src/main/resources/META-INF/persistence.xml +++ b/model/jpa/src/main/resources/META-INF/persistence.xml @@ -58,6 +58,12 @@ org.keycloak.events.jpa.EventEntity org.keycloak.events.jpa.AdminEventEntity + + + org.keycloak.authorization.jpa.entities.ResourceServerEntity + org.keycloak.authorization.jpa.entities.ResourceEntity + org.keycloak.authorization.jpa.entities.ScopeEntity + org.keycloak.authorization.jpa.entities.PolicyEntity true diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory new file mode 100644 index 00000000000..46463ee5c88 --- /dev/null +++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory @@ -0,0 +1,19 @@ +# +# JBoss, Home of Professional Open Source. +# Copyright 2016 Red Hat, Inc., and individual 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. +# + +org.keycloak.authorization.jpa.store.JPAAuthorizationStoreFactory \ No newline at end of file diff --git a/model/mongo/pom.xml b/model/mongo/pom.xml index db244732eed..90e2afaedb3 100755 --- a/model/mongo/pom.xml +++ b/model/mongo/pom.xml @@ -31,6 +31,11 @@ Keycloak Model Mongo + + 1.8 + 1.8 + + org.bouncycastle diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java new file mode 100644 index 00000000000..a993c94cf82 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java @@ -0,0 +1,168 @@ +package org.keycloak.authorization.mongo.adapter; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.mongo.entities.PolicyEntity; +import org.keycloak.authorization.mongo.entities.ResourceEntity; +import org.keycloak.authorization.mongo.entities.ScopeEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.adapters.AbstractMongoAdapter; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static javafx.scene.input.KeyCode.R; + +/** + * @author Pedro Igor + */ +public class PolicyAdapter extends AbstractMongoAdapter implements Policy { + + private final PolicyEntity entity; + private final AuthorizationProvider authorizationProvider; + + public PolicyAdapter(PolicyEntity entity, MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) { + super(invocationContext); + this.entity = entity; + this.authorizationProvider = authorizationProvider; + } + + @Override + protected PolicyEntity getMongoEntity() { + return entity; + } + + @Override + public String getId() { + return getMongoEntity().getId(); + } + + @Override + public String getType() { + return getMongoEntity().getType(); + } + + @Override + public DecisionStrategy getDecisionStrategy() { + return getMongoEntity().getDecisionStrategy(); + } + + @Override + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + getMongoEntity().setDecisionStrategy(decisionStrategy); + updateMongoEntity(); + } + + @Override + public Logic getLogic() { + return getMongoEntity().getLogic(); + } + + @Override + public void setLogic(Logic logic) { + getMongoEntity().setLogic(logic); + updateMongoEntity(); + } + + @Override + public Map getConfig() { + return getMongoEntity().getConfig(); + } + + @Override + public void setConfig(Map config) { + getMongoEntity().setConfig(config); + updateMongoEntity(); + } + + @Override + public String getName() { + return getMongoEntity().getName(); + } + + @Override + public void setName(String name) { + getMongoEntity().setName(name); + updateMongoEntity(); + } + + @Override + public String getDescription() { + return getMongoEntity().getDescription(); + } + + @Override + public void setDescription(String description) { + getMongoEntity().setDescription(description); + updateMongoEntity(); + } + + @Override + public ResourceServer getResourceServer() { + return this.authorizationProvider.getStoreFactory().getResourceServerStore().findById(getMongoEntity().getResourceServerId()); + } + + @Override + public Set getAssociatedPolicies() { + return getMongoEntity().getAssociatedPolicies().stream() + .map((Function) id -> authorizationProvider.getStoreFactory().getPolicyStore().findById(id)) + .collect(Collectors.toSet()); + } + + @Override + public Set getResources() { + return getMongoEntity().getResources().stream() + .map((Function) id -> authorizationProvider.getStoreFactory().getResourceStore().findById(id)) + .collect(Collectors.toSet()); + } + + @Override + public Set getScopes() { + return getMongoEntity().getScopes().stream() + .map((Function) id -> authorizationProvider.getStoreFactory().getScopeStore().findById(id)) + .collect(Collectors.toSet()); + } + + @Override + public void addScope(Scope scope) { + getMongoEntity().addScope(scope.getId()); + updateMongoEntity(); + } + + @Override + public void removeScope(Scope scope) { + getMongoEntity().removeScope(scope.getId()); + updateMongoEntity(); + } + + @Override + public void addAssociatedPolicy(Policy associatedPolicy) { + getMongoEntity().addAssociatedPolicy(associatedPolicy.getId()); + updateMongoEntity(); + } + + @Override + public void removeAssociatedPolicy(Policy associatedPolicy) { + getMongoEntity().removeAssociatedPolicy(associatedPolicy.getId()); + updateMongoEntity(); + } + + @Override + public void addResource(Resource resource) { + getMongoEntity().addResource(resource.getId()); + updateMongoEntity(); + } + + @Override + public void removeResource(Resource resource) { + getMongoEntity().removeResource(resource.getId()); + updateMongoEntity(); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java new file mode 100644 index 00000000000..7c67f6ea75c --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java @@ -0,0 +1,106 @@ +package org.keycloak.authorization.mongo.adapter; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.mongo.entities.ResourceEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.adapters.AbstractMongoAdapter; + +import java.util.List; +import java.util.Set; + +import static java.util.stream.Collectors.toList; + +/** + * @author Pedro Igor + */ +public class ResourceAdapter extends AbstractMongoAdapter implements Resource { + + private final ResourceEntity entity; + private final AuthorizationProvider authorizationProvider; + + public ResourceAdapter(ResourceEntity entity, MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) { + super(invocationContext); + this.entity = entity; + this.authorizationProvider = authorizationProvider; + } + + @Override + public String getId() { + return getMongoEntity().getId(); + } + + @Override + public String getName() { + return getMongoEntity().getName(); + } + + @Override + public void setName(String name) { + getMongoEntity().setName(name); + updateMongoEntity(); + } + + @Override + public String getUri() { + return getMongoEntity().getUri(); + } + + @Override + public void setUri(String uri) { + getMongoEntity().setUri(uri); + updateMongoEntity(); + } + + @Override + public String getType() { + return getMongoEntity().getType(); + } + + @Override + public void setType(String type) { + getMongoEntity().setType(type); + updateMongoEntity(); + } + + @Override + public List getScopes() { + return getMongoEntity().getScopes().stream() + .map(id -> authorizationProvider.getStoreFactory().getScopeStore().findById(id)) + .collect(toList()); + } + + @Override + public String getIconUri() { + return getMongoEntity().getIconUri(); + } + + @Override + public void setIconUri(String iconUri) { + getMongoEntity().setIconUri(iconUri); + updateMongoEntity(); + } + + @Override + public ResourceServer getResourceServer() { + return this.authorizationProvider.getStoreFactory().getResourceServerStore().findById(getMongoEntity().getResourceServerId()); + } + + @Override + public String getOwner() { + return getMongoEntity().getOwner(); + } + + @Override + public void updateScopes(Set scopes) { + getMongoEntity().updateScopes(scopes); + updateMongoEntity(); + } + + @Override + protected ResourceEntity getMongoEntity() { + return this.entity; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceServerAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceServerAdapter.java new file mode 100644 index 00000000000..72feedb82a2 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceServerAdapter.java @@ -0,0 +1,56 @@ +package org.keycloak.authorization.mongo.adapter; + +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.mongo.entities.ResourceServerEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.adapters.AbstractMongoAdapter; + +/** + * @author Pedro Igor + */ +public class ResourceServerAdapter extends AbstractMongoAdapter implements ResourceServer{ + + private final ResourceServerEntity entity; + + public ResourceServerAdapter(ResourceServerEntity entity, MongoStoreInvocationContext invocationContext) { + super(invocationContext); + this.entity = entity; + } + + @Override + public String getId() { + return getMongoEntity().getId(); + } + + @Override + public String getClientId() { + return getMongoEntity().getClientId(); + } + + @Override + public boolean isAllowRemoteResourceManagement() { + return getMongoEntity().isAllowRemoteResourceManagement(); + } + + @Override + public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) { + getMongoEntity().setAllowRemoteResourceManagement(allowRemoteResourceManagement); + updateMongoEntity(); + } + + @Override + public PolicyEnforcementMode getPolicyEnforcementMode() { + return getMongoEntity().getPolicyEnforcementMode(); + } + + @Override + public void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode) { + getMongoEntity().setPolicyEnforcementMode(enforcementMode); + updateMongoEntity(); + } + + @Override + protected ResourceServerEntity getMongoEntity() { + return this.entity; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ScopeAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ScopeAdapter.java new file mode 100644 index 00000000000..72196ca152d --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ScopeAdapter.java @@ -0,0 +1,60 @@ +package org.keycloak.authorization.mongo.adapter; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.mongo.entities.ScopeEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.mongo.keycloak.adapters.AbstractMongoAdapter; + +/** + * @author Pedro Igor + */ +public class ScopeAdapter extends AbstractMongoAdapter implements Scope { + + private final ScopeEntity entity; + private final AuthorizationProvider authorizationProvider; + + public ScopeAdapter(ScopeEntity entity, MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) { + super(invocationContext); + this.entity = entity; + this.authorizationProvider = authorizationProvider; + } + + @Override + public String getId() { + return getMongoEntity().getId(); + } + + @Override + public String getName() { + return getMongoEntity().getName(); + } + + @Override + public void setName(String name) { + getMongoEntity().setName(name); + updateMongoEntity(); + } + + @Override + public String getIconUri() { + return getMongoEntity().getIconUri(); + } + + @Override + public void setIconUri(String iconUri) { + getMongoEntity().setIconUri(iconUri); + updateMongoEntity(); + } + + @Override + public ResourceServer getResourceServer() { + return this.authorizationProvider.getStoreFactory().getResourceServerStore().findById(getMongoEntity().getResourceServerId()); + } + + @Override + protected ScopeEntity getMongoEntity() { + return this.entity; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/PolicyEntity.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/PolicyEntity.java new file mode 100644 index 00000000000..9230b88a14e --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/PolicyEntity.java @@ -0,0 +1,166 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.entities; + +import org.keycloak.authorization.model.Policy.DecisionStrategy; +import org.keycloak.authorization.model.Policy.Logic; +import org.keycloak.connections.mongo.api.MongoCollection; +import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.entities.AbstractIdentifiableEntity; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Pedro Igor + */ +@MongoCollection(collectionName = "policies") +public class PolicyEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity { + + private String name; + + private String description; + + private String type; + + private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; + + private Logic logic = Logic.POSITIVE; + + private Map config = new HashMap(); + + private String resourceServerId; + + private Set associatedPolicies = new HashSet<>(); + + private Set resources = new HashSet<>(); + + private Set scopes = new HashSet<>(); + + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + public DecisionStrategy getDecisionStrategy() { + return this.decisionStrategy; + } + + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + this.decisionStrategy = decisionStrategy; + } + + public Logic getLogic() { + return this.logic; + } + + public void setLogic(Logic logic) { + this.logic = logic; + } + + public Map getConfig() { + return this.config; + } + + public void setConfig(Map config) { + this.config = config; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getResourceServerId() { + return this.resourceServerId; + } + + public void setResourceServerId(String resourceServerId) { + this.resourceServerId = resourceServerId; + } + + public Set getAssociatedPolicies() { + return this.associatedPolicies; + } + + public void setAssociatedPolicies(Set associatedPolicies) { + this.associatedPolicies = associatedPolicies; + } + + public Set getResources() { + return this.resources; + } + + public void setResources(Set resources) { + this.resources = resources; + } + + public Set getScopes() { + return this.scopes; + } + + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + public void addScope(String scopeId) { + getScopes().add(scopeId); + } + + public void removeScope(String scopeId) { + getScopes().remove(scopeId); + } + + public void addAssociatedPolicy(String policyId) { + getAssociatedPolicies().add(policyId); + } + + public void removeAssociatedPolicy(String policyId) { + getAssociatedPolicies().remove(policyId); + } + + public void addResource(String resourceId) { + getResources().add(resourceId); + } + + public void removeResource(String resourceId) { + getResources().remove(resourceId); + } + + public void afterRemove(MongoStoreInvocationContext invocationContext) { + + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceEntity.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceEntity.java new file mode 100644 index 00000000000..b2e15da6300 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceEntity.java @@ -0,0 +1,142 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.entities; + +import org.keycloak.authorization.model.Scope; +import org.keycloak.connections.mongo.api.MongoCollection; +import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.entities.AbstractIdentifiableEntity; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Pedro Igor + */ +@MongoCollection(collectionName = "resources") +public class ResourceEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity { + + private String name; + + private String uri; + + private String type; + + private String iconUri; + + private String owner; + + private String resourceServerId; + + private List scopes = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getScopes() { + return this.scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public String getIconUri() { + return iconUri; + } + + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + public String getResourceServerId() { + return resourceServerId; + } + + public void setResourceServerId(String resourceServerId) { + this.resourceServerId = resourceServerId; + } + + public String getOwner() { + return this.owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public void updateScopes(Set toUpdate) { + for (Scope scope : toUpdate) { + boolean hasScope = false; + + for (String existingScope : this.scopes) { + if (existingScope.equals(scope.getId())) { + hasScope = true; + } + } + + if (!hasScope) { + this.scopes.add(scope.getId()); + } + } + + for (String scopeId : new HashSet(this.scopes)) { + boolean hasScope = false; + + for (Scope scope : toUpdate) { + if (scopeId.equals(scope.getId())) { + hasScope = true; + } + } + + if (!hasScope) { + this.scopes.remove(scopeId); + } + } + } + + @Override + public void afterRemove(MongoStoreInvocationContext invocationContext) { + + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceServerEntity.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceServerEntity.java new file mode 100644 index 00000000000..7013e1bfef5 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceServerEntity.java @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.entities; + +import org.keycloak.authorization.model.ResourceServer.PolicyEnforcementMode; +import org.keycloak.connections.mongo.api.MongoCollection; +import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.entities.AbstractIdentifiableEntity; + +/** + * @author Pedro Igor + */ +@MongoCollection(collectionName = "resource-servers") +public class ResourceServerEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity { + + private String clientId; + + private boolean allowRemoteResourceManagement; + + private PolicyEnforcementMode policyEnforcementMode = PolicyEnforcementMode.ENFORCING; + + public String getClientId() { + return this.clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public boolean isAllowRemoteResourceManagement() { + return this.allowRemoteResourceManagement; + } + + public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) { + this.allowRemoteResourceManagement = allowRemoteResourceManagement; + } + + public PolicyEnforcementMode getPolicyEnforcementMode() { + return this.policyEnforcementMode; + } + + public void setPolicyEnforcementMode(PolicyEnforcementMode policyEnforcementMode) { + this.policyEnforcementMode = policyEnforcementMode; + } + + @Override + public void afterRemove(MongoStoreInvocationContext invocationContext) { + + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ScopeEntity.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ScopeEntity.java new file mode 100644 index 00000000000..152127dcdd3 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ScopeEntity.java @@ -0,0 +1,66 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.entities; + +import org.keycloak.connections.mongo.api.MongoCollection; +import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.entities.AbstractIdentifiableEntity; + +/** + * @author Pedro Igor + */ +@MongoCollection(collectionName = "scopes") +public class ScopeEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity { + + private String name; + + private String iconUri; + + private String resourceServerId; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIconUri() { + return iconUri; + } + + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + public String getResourceServerId() { + return resourceServerId; + } + + public void setResourceServerId(String resourceServerId) { + this.resourceServerId = resourceServerId; + } + + @Override + public void afterRemove(MongoStoreInvocationContext invocationContext) { + + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java new file mode 100644 index 00000000000..9a484ad49da --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.store; + +import org.keycloak.Config; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.store.AuthorizationStoreFactory; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.connections.mongo.MongoConnectionProvider; +import org.keycloak.models.KeycloakSession; + +/** + * @author Pedro Igor + */ +public class MongoAuthorizationStoreFactory implements AuthorizationStoreFactory { + @Override + public StoreFactory create(KeycloakSession session) { + MongoConnectionProvider connection = session.getProvider(MongoConnectionProvider.class); + AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class); + return new MongoStoreFactory(connection.getInvocationContext(), provider); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "mongo"; + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java new file mode 100644 index 00000000000..6f0ba5dcd70 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java @@ -0,0 +1,171 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.store; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.mongo.adapter.PolicyAdapter; +import org.keycloak.authorization.mongo.entities.PolicyEntity; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.connections.mongo.api.MongoStore; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * @author Pedro Igor + */ +public class MongoPolicyStore implements PolicyStore { + + private final MongoStoreInvocationContext invocationContext; + private final AuthorizationProvider authorizationProvider; + + public MongoPolicyStore(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) { + this.invocationContext = invocationContext; + this.authorizationProvider = authorizationProvider; + } + + @Override + public Policy create(String name, String type, ResourceServer resourceServer) { + PolicyEntity entity = new PolicyEntity(); + + entity.setId(KeycloakModelUtils.generateId()); + entity.setName(name); + entity.setType(type); + entity.setResourceServerId(resourceServer.getId()); + + getMongoStore().insertEntity(entity, getInvocationContext()); + + return new PolicyAdapter(entity, getInvocationContext(), this.authorizationProvider) ; + } + + @Override + public void delete(String id) { + getMongoStore().removeEntity(PolicyEntity.class, id, getInvocationContext()); + } + + @Override + public Policy findById(String id) { + PolicyEntity entity = getMongoStore().loadEntity(PolicyEntity.class, id, getInvocationContext()); + + if (entity == null) { + return null; + } + + return new PolicyAdapter(entity, getInvocationContext(), this.authorizationProvider); + } + + + @Override + public Policy findByName(String name, String resourceServerId) { + DBObject query = new QueryBuilder() + .and("resourceServerId").is(resourceServerId) + .and("name").is(name) + .get(); + + return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())).findFirst().orElse(null); + } + + @Override + public List findByResourceServer(String resourceServerId) { + DBObject query = new QueryBuilder() + .and("resourceServerId").is(resourceServerId) + .get(); + + return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())) + .collect(toList()); + } + + @Override + public List findByResource(String resourceId) { + DBObject query = new QueryBuilder() + .and("resources").is(resourceId) + .get(); + + return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())) + .collect(toList()); + } + + @Override + public List findByResourceType(String resourceType, String resourceServerId) { + DBObject query = new QueryBuilder() + .and("resourceServerId").is(resourceServerId) + .get(); + + return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream() + .filter(policyEntity -> { + String defaultResourceType = policyEntity.getConfig().get("defaultResourceType"); + return defaultResourceType != null && defaultResourceType.equals(resourceType); + }) + .map(policyEntity -> findById(policyEntity.getId())) + .collect(toList()); + } + + @Override + public List findByScopeIds(List scopeIds, String resourceServerId) { + DBObject query = new QueryBuilder() + .and("resourceServerId").is(resourceServerId) + .and("scopes").in(scopeIds) + .get(); + + return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())) + .collect(toList()); + } + + @Override + public List findByType(String type) { + DBObject query = new QueryBuilder() + .and("type").is(type) + .get(); + + return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())) + .collect(toList()); + } + + @Override + public List findDependentPolicies(String policyId) { + DBObject query = new QueryBuilder() + .and("associatedPolicies").is(policyId) + .get(); + + return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())) + .collect(toList()); + } + + private MongoStoreInvocationContext getInvocationContext() { + return this.invocationContext; + } + + private MongoStore getMongoStore() { + return getInvocationContext().getMongoStore(); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceServerStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceServerStore.java new file mode 100644 index 00000000000..25e5f67301a --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceServerStore.java @@ -0,0 +1,90 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.store; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.mongo.adapter.ResourceServerAdapter; +import org.keycloak.authorization.mongo.entities.ResourceServerEntity; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.connections.mongo.api.MongoStore; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.utils.KeycloakModelUtils; + +/** + * @author Pedro Igor + */ +public class MongoResourceServerStore implements ResourceServerStore { + + private final MongoStoreInvocationContext invocationContext; + private final AuthorizationProvider authorizationProvider; + + public MongoResourceServerStore(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) { + this.invocationContext = invocationContext; + this.authorizationProvider = authorizationProvider; + } + + @Override + public ResourceServer create(String clientId) { + ResourceServerEntity entity = new ResourceServerEntity(); + + entity.setId(KeycloakModelUtils.generateId()); + entity.setClientId(clientId); + + getMongoStore().insertEntity(entity, getInvocationContext()); + + return new ResourceServerAdapter(entity, getInvocationContext()); + } + + @Override + public void delete(String id) { + getMongoStore().removeEntity(ResourceServerEntity.class, id, getInvocationContext()); + } + + @Override + public ResourceServer findById(String id) { + ResourceServerEntity entity = getMongoStore().loadEntity(ResourceServerEntity.class, id, getInvocationContext()); + + if (entity == null) { + return null; + } + + return new ResourceServerAdapter(entity, getInvocationContext()); + } + + @Override + public ResourceServer findByClient(String clientId) { + DBObject query = new QueryBuilder() + .and("clientId").is(clientId) + .get(); + + return getMongoStore().loadEntities(ResourceServerEntity.class, query, getInvocationContext()).stream() + .map(resourceServer -> findById(resourceServer.getId())).findFirst().orElse(null); + } + + private MongoStoreInvocationContext getInvocationContext() { + return this.invocationContext; + } + + private MongoStore getMongoStore() { + return getInvocationContext().getMongoStore(); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java new file mode 100644 index 00000000000..11b735ba6ce --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java @@ -0,0 +1,141 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.store; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.mongo.adapter.ResourceAdapter; +import org.keycloak.authorization.mongo.entities.ResourceEntity; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; +import org.keycloak.connections.mongo.api.MongoStore; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * @author Pedro Igor + */ +public class MongoResourceStore implements ResourceStore { + + private final MongoStoreInvocationContext invocationContext; + private final AuthorizationProvider authorizationProvider; + + public MongoResourceStore(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) { + this.invocationContext = invocationContext; + this.authorizationProvider = authorizationProvider; + } + + @Override + public Resource create(String name, ResourceServer resourceServer, String owner) { + ResourceEntity entity = new ResourceEntity(); + + entity.setId(KeycloakModelUtils.generateId()); + entity.setName(name); + entity.setResourceServerId(resourceServer.getId()); + entity.setOwner(owner); + + getMongoStore().insertEntity(entity, getInvocationContext()); + + return new ResourceAdapter(entity, getInvocationContext(), this.authorizationProvider); + } + + @Override + public void delete(String id) { + getMongoStore().removeEntity(ResourceEntity.class, id, getInvocationContext()); + } + + @Override + public Resource findById(String id) { + ResourceEntity entity = getMongoStore().loadEntity(ResourceEntity.class, id, getInvocationContext()); + + if (entity == null) { + return null; + } + + return new ResourceAdapter(entity, getInvocationContext(), this.authorizationProvider); + } + + @Override + public List findByOwner(String ownerId) { + DBObject query = new QueryBuilder() + .and("owner").is(ownerId) + .get(); + + return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream() + .map(scope -> findById(scope.getId())).collect(toList()); + } + + @Override + public List findByResourceServer(String resourceServerId) { + DBObject query = new QueryBuilder() + .and("resourceServerId").is(resourceServerId) + .get(); + + return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream() + .map(scope -> findById(scope.getId())).collect(toList()); + } + + @Override + public List findByScope(String... id) { + DBObject query = new QueryBuilder() + .and("scopes.id").in(id) + .get(); + + return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())) + .collect(toList()); + } + + @Override + public Resource findByName(String name, String resourceServerId) { + DBObject query = new QueryBuilder() + .and("name").is(name) + .and("resourceServerId").is(resourceServerId) + .get(); + + return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())).findFirst().orElse(null); + } + + @Override + public List findByType(String type) { + DBObject query = new QueryBuilder() + .and("type").is(type) + .get(); + + return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())) + .collect(toList()); + } + + private MongoStoreInvocationContext getInvocationContext() { + return this.invocationContext; + } + + private MongoStore getMongoStore() { + return getInvocationContext().getMongoStore(); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java new file mode 100644 index 00000000000..e57b69bf6ee --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java @@ -0,0 +1,108 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.store; + +import com.mongodb.DBObject; +import com.mongodb.QueryBuilder; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.mongo.adapter.ScopeAdapter; +import org.keycloak.authorization.mongo.entities.ScopeEntity; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.connections.mongo.api.MongoStore; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; +import org.keycloak.models.utils.KeycloakModelUtils; + +import java.util.List; + +import static java.util.stream.Collectors.toList; + +/** + * @author Pedro Igor + */ +public class MongoScopeStore implements ScopeStore { + + private final MongoStoreInvocationContext invocationContext; + private final AuthorizationProvider authorizationProvider; + + public MongoScopeStore(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) { + this.invocationContext = invocationContext; + this.authorizationProvider = authorizationProvider; + } + + @Override + public Scope create(final String name, final ResourceServer resourceServer) { + ScopeEntity entity = new ScopeEntity(); + + entity.setId(KeycloakModelUtils.generateId()); + entity.setName(name); + entity.setResourceServerId(resourceServer.getId()); + + getMongoStore().insertEntity(entity, getInvocationContext()); + + return new ScopeAdapter(entity, getInvocationContext(), this.authorizationProvider); + } + + @Override + public void delete(String id) { + getMongoStore().removeEntity(ScopeEntity.class, id, getInvocationContext()); + } + + @Override + public Scope findById(String id) { + ScopeEntity entity = getMongoStore().loadEntity(ScopeEntity.class, id, getInvocationContext()); + + if (entity == null) { + return null; + } + + return new ScopeAdapter(entity, getInvocationContext(), this.authorizationProvider); + } + + @Override + public Scope findByName(String name, String resourceServerId) { + DBObject query = new QueryBuilder() + .and("resourceServerId").is(resourceServerId) + .and("name").is(name) + .get(); + + return getMongoStore().loadEntities(ScopeEntity.class, query, getInvocationContext()).stream() + .map(scope -> findById(scope.getId())).findFirst().orElse(null); + } + + @Override + public List findByResourceServer(String resourceServerId) { + DBObject query = new QueryBuilder() + .and("resourceServerId").is(resourceServerId) + .get(); + + return getMongoStore().loadEntities(ScopeEntity.class, query, getInvocationContext()).stream() + .map(policyEntity -> findById(policyEntity.getId())) + .collect(toList()); + } + + private MongoStoreInvocationContext getInvocationContext() { + return this.invocationContext; + } + + private MongoStore getMongoStore() { + return getInvocationContext().getMongoStore(); + } +} diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java new file mode 100644 index 00000000000..7a94ba56873 --- /dev/null +++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java @@ -0,0 +1,66 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.mongo.store; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; + +/** + * @author Pedro Igor + */ +public class MongoStoreFactory implements StoreFactory { + + private final MongoStoreInvocationContext invocationContext; + private final AuthorizationProvider authorizationProvider; + + public MongoStoreFactory(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) { + this.invocationContext = invocationContext; + this.authorizationProvider = authorizationProvider; + } + + @Override + public PolicyStore getPolicyStore() { + return new MongoPolicyStore(this.invocationContext, this.authorizationProvider); + } + + @Override + public ResourceServerStore getResourceServerStore() { + return new MongoResourceServerStore(this.invocationContext, this.authorizationProvider); + } + + @Override + public ResourceStore getResourceStore() { + return new MongoResourceStore(this.invocationContext, this.authorizationProvider); + } + + @Override + public ScopeStore getScopeStore() { + return new MongoScopeStore(this.invocationContext, this.authorizationProvider); + } + + @Override + public void close() { + + } +} diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index 3bc9c7a6a99..491ad716c0f 100755 --- a/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -74,6 +74,10 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro "org.keycloak.models.entities.RequiredActionProviderEntity", "org.keycloak.models.entities.PersistentUserSessionEntity", "org.keycloak.models.entities.PersistentClientSessionEntity", + "org.keycloak.authorization.mongo.entities.PolicyEntity", + "org.keycloak.authorization.mongo.entities.ResourceEntity", + "org.keycloak.authorization.mongo.entities.ResourceServerEntity", + "org.keycloak.authorization.mongo.entities.ScopeEntity" }; private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java index cef8a5a139b..14e5b033a03 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java @@ -129,10 +129,26 @@ public class MongoRealmProvider implements RealmProvider { @Override public boolean removeRealm(String id) { - RealmModel realm = getRealm(id); + final RealmModel realm = getRealm(id); if (realm == null) return false; session.users().preRemove(realm); - return getMongoStore().removeEntity(MongoRealmEntity.class, id, invocationContext); + boolean removed = getMongoStore().removeEntity(MongoRealmEntity.class, id, invocationContext); + + if (removed) { + session.getKeycloakSessionFactory().publish(new RealmModel.RealmRemovedEvent() { + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); + } + + return removed; } protected MongoStore getMongoStore() { @@ -408,12 +424,27 @@ public class MongoRealmProvider implements RealmProvider { @Override public boolean removeClient(String id, RealmModel realm) { if (id == null) return false; - ClientModel client = getClientById(id, realm); + final ClientModel client = getClientById(id, realm); if (client == null) return false; session.users().preRemove(realm, client); + boolean removed = getMongoStore().removeEntity(MongoClientEntity.class, id, invocationContext); - return getMongoStore().removeEntity(MongoClientEntity.class, id, invocationContext); + if (removed) { + session.getKeycloakSessionFactory().publish(new RealmModel.ClientRemovedEvent() { + @Override + public ClientModel getClient() { + return client; + } + + @Override + public KeycloakSession getKeycloakSession() { + return session; + } + }); + } + + return removed; } @Override diff --git a/model/mongo/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory b/model/mongo/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory new file mode 100644 index 00000000000..e1d801cc483 --- /dev/null +++ b/model/mongo/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory @@ -0,0 +1,37 @@ +# +# JBoss, Home of Professional Open Source. +# Copyright 2016 Red Hat, Inc., and individual 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. +# + +# +# JBoss, Home of Professional Open Source. +# Copyright 2016 Red Hat, Inc., and individual 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. +# + +org.keycloak.authorization.mongo.store.MongoAuthorizationStoreFactory \ No newline at end of file diff --git a/pom.xml b/pom.xml index 38efdcffa9d..082891b4de8 100755 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,9 @@ 1.1.0.Final 2.0.5 + + 6.4.0.Final + 2.0.0-M17 1.0.0-M23 @@ -176,6 +179,7 @@ wildfly integration adapters + authz examples testsuite @@ -412,6 +416,15 @@ ${google.zxing.version} + + + org.drools + drools-bom + pom + ${version.org.drools} + import + + com.icegreen @@ -976,6 +989,49 @@ keycloak-services ${project.version} + + + + org.keycloak + keycloak-authz-client + ${project.version} + + + org.keycloak + keycloak-authz-policy-drools + ${project.version} + + + org.keycloak + keycloak-authz-policy-resource + ${project.version} + + + org.keycloak + keycloak-authz-policy-scope + ${project.version} + + + org.keycloak + keycloak-authz-policy-identity + ${project.version} + + + org.keycloak + keycloak-authz-policy-js + ${project.version} + + + org.keycloak + keycloak-authz-policy-time + ${project.version} + + + org.keycloak + keycloak-authz-policy-aggregate + ${project.version} + + org.keycloak keycloak-saml-as7-modules diff --git a/server-spi/pom.xml b/server-spi/pom.xml index 047a065cd3c..9b71e2d088a 100755 --- a/server-spi/pom.xml +++ b/server-spi/pom.xml @@ -30,6 +30,11 @@ Keycloak Server SPI + + 1.8 + 1.8 + + org.jboss.resteasy diff --git a/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProvider.java new file mode 100644 index 00000000000..ff646bbfe54 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProvider.java @@ -0,0 +1,137 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.keycloak.authorization.permission.evaluator.Evaluators; +import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator; +import org.keycloak.authorization.policy.provider.PolicyProvider; +import org.keycloak.authorization.policy.provider.PolicyProviderFactory; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; + +import java.util.List; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +/** + *

The main contract here is the creation of {@link org.keycloak.authorization.permission.evaluator.PermissionEvaluator} instances. Usually + * an application has a single {@link AuthorizationProvider} instance and threads servicing client requests obtain {@link org.keycloak.authorization.core.permission.evaluator.PermissionEvaluator} + * from the {@link #evaluators()} method. + * + *

The internal state of a {@link AuthorizationProvider} is immutable. This internal state includes all of the metadata + * used during the evaluation of policies. + * + *

Once created, {@link org.keycloak.authorization.permission.evaluator.PermissionEvaluator} instances can be obtained from the {@link #evaluators()} method: + * + *

+ *     List permissionsToEvaluate = getPermissions(); // the permissions to evaluate
+ *     EvaluationContext evaluationContext = createEvaluationContext(); // the context with runtime environment information
+ *     PermissionEvaluator evaluator = authorization.evaluators().from(permissionsToEvaluate, context);
+ *
+ *     evaluator.evaluate(new Decision() {
+ *
+ *         public void onDecision(Evaluation evaluation) {
+ *              // do something on grant
+ *         }
+ *
+ *     });
+ * 
+ * + * @author Pedro Igor + */ +public final class AuthorizationProvider implements Provider { + + private final DefaultPolicyEvaluator policyEvaluator; + private final Executor scheduller; + private final StoreFactory storeFactory; + private final List policyProviderFactories; + private final KeycloakSession keycloakSession; + + public AuthorizationProvider(KeycloakSession session, StoreFactory storeFactory, Executor scheduller) { + this.keycloakSession = session; + this.storeFactory = storeFactory; + this.scheduller = scheduller; + this.policyProviderFactories = configurePolicyProviderFactories(session); + this.policyEvaluator = new DefaultPolicyEvaluator(this, this.policyProviderFactories); + } + + public AuthorizationProvider(KeycloakSession session, StoreFactory storeFactory) { + this(session, storeFactory, Runnable::run); + } + + /** + * Returns a {@link Evaluators} instance from where {@link org.keycloak.authorization.policy.evaluation.PolicyEvaluator} instances + * can be obtained. + * + * @return a {@link Evaluators} instance + */ + public Evaluators evaluators() { + return new Evaluators(this.policyProviderFactories, this.policyEvaluator, this.scheduller); + } + + /** + * Returns a {@link StoreFactory}. + * + * @return the {@link StoreFactory} + */ + public StoreFactory getStoreFactory() { + return this.storeFactory; + } + + /** + * Returns the registered {@link PolicyProviderFactory}. + * + * @return a {@link List} containing all registered {@link PolicyProviderFactory} + */ + public List getProviderFactories() { + return this.policyProviderFactories; + } + + /** + * Returns a {@link PolicyProviderFactory} given a type. + * + * @param type the type of the policy provider + * @param the expected type of the provider + * @return a {@link PolicyProviderFactory} with the given type + */ + public F getProviderFactory(String type) { + return (F) getProviderFactories().stream().filter(policyProviderFactory -> policyProviderFactory.getId().equals(type)).findFirst().orElse(null); + } + + public KeycloakSession getKeycloakSession() { + return this.keycloakSession; + } + + private List configurePolicyProviderFactories(KeycloakSession session) { + List providerFactories = session.getKeycloakSessionFactory().getProviderFactories(PolicyProvider.class); + + if (providerFactories.isEmpty()) { + throw new RuntimeException("Could not find any policy provider."); + } + + return providerFactories.stream().map(providerFactory -> (PolicyProviderFactory) providerFactory).collect(Collectors.toList()); + } + + @Override + public void close() { + + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProviderFactory.java b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProviderFactory.java new file mode 100644 index 00000000000..ae4dcc2fe91 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProviderFactory.java @@ -0,0 +1,27 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Pedro Igor + */ +public interface AuthorizationProviderFactory extends ProviderFactory { +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/AuthorizationSpi.java b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationSpi.java new file mode 100644 index 00000000000..65028b3c0c6 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationSpi.java @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Pedro Igor + */ +public class AuthorizationSpi implements Spi { + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "authorization"; + } + + @Override + public Class getProviderClass() { + return AuthorizationProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return AuthorizationProviderFactory.class; + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/Decision.java b/server-spi/src/main/java/org/keycloak/authorization/Decision.java new file mode 100644 index 00000000000..6ebd0862812 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/Decision.java @@ -0,0 +1,41 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.keycloak.authorization.policy.evaluation.Evaluation; + +/** + * @author Pedro Igor + */ +public interface Decision { + + enum Effect { + PERMIT, + DENY + } + + void onDecision(D evaluation); + + default void onError(Throwable cause) { + throw new RuntimeException("Not implemented.", cause); + } + + default void onComplete() { + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java new file mode 100644 index 00000000000..ce2bc51921a --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java @@ -0,0 +1,143 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.attribute; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.Map; + +import static java.util.Collections.emptyList; + +/** + *

Holds attributes, their values and provides utlity methods to manage them. + * + *

In the future, it may be useful to provide different implementations for this interface in order to plug or integrate with different + * Policy Information Point (PIP).

+ * + * @author Pedro Igor + */ +public interface Attributes { + + static Attributes from(Map> attributes) { + return () -> attributes; + } + + /** + * Converts to a {@link Map}. + * + * @return + */ + Map> toMap(); + + /** + * Checks if there is an attribute with the given name. + * + * @param name the attribute name + * @return true if any attribute with name exist. Otherwise, returns false. + */ + default boolean exists(String name) { + return toMap().containsKey(name); + } + + /** + * Checks if there is an attribute with the given name and value. + * + * @param name the attribute name + * @param value the attribute value + * @return true if any attribute with name and value exist. Otherwise, returns false. + */ + default boolean containsValue(String name, String value) { + return toMap().getOrDefault(name, emptyList()).stream().anyMatch(value::equals); + } + + /** + * Returns a {@link Entry} from where values can be obtained and parsed accordingly. + * + * @param name the attribute name + * @return an {@link Entry} holding the values for an attribute + */ + default Entry getValue(String name) { + Collection value = toMap().get(name); + + if (value != null) { + return new Entry(name, value); + } + + return null; + } + + /** + * Holds an attribute and its values, providing useful methods for obtaining and formatting values. Specially useful + * for writing rule-based policies. + */ + class Entry { + + private final String[] values; + private final String name; + + Entry(String name, Collection values) { + this.name = name; + this.values = values.toArray(new String[values.size()]); + } + + private String getName() { + return this.name; + } + + public int size() { + return values.length; + } + + public String asString(int idx) { + if (idx >= values.length) { + throw new IllegalArgumentException("Invalid index [" + idx + "]. Values are [" + values + "]."); + } + + return values[idx]; + } + + public int asInt(int idx) { + return Integer.parseInt(asString(idx)); + } + + public Date asDate(int idx, String pattern) { + try { + return new SimpleDateFormat(pattern).parse(asString(idx)); + } catch (ParseException e) { + throw new RuntimeException("Error parsing date.", e); + } + } + + public InetAddress asInetAddress(int idx) { + try { + return InetAddress.getByName(asString(idx)); + } catch (UnknownHostException e) { + throw new RuntimeException("Error parsing address.", e); + } + } + + public long asLong(int idx) { + return Long.parseLong(asString(idx)); + } + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/attribute/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/attribute/package-info.java new file mode 100644 index 00000000000..c75b520a2a1 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/attribute/package-info.java @@ -0,0 +1,24 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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. + */ + +/** + * Provides classes related with the representation of attributes and their manipulation. + * + * @author Pedro Igor + */ +package org.keycloak.authorization.attribute; \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java b/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java new file mode 100644 index 00000000000..ebc9679994c --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java @@ -0,0 +1,57 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.identity; + +import org.keycloak.authorization.attribute.Attributes; + +/** + *

Represents a security identity, which can be a person or non-person entity that was previously authenticated. + * + *

An {@link Identity} plays an important role during the evaluation of policies as they represent the entity to which one or more permissions + * should be granted or not, providing additional information and attributes that can be relevant to the different + * access control methods involved during the evaluation of policies. + * + * @author Pedro Igor + */ +public interface Identity { + + /** + * Returns the unique identifier of this identity. + * + * @return the unique identifier of this identity + */ + String getId(); + + /** + * Returns the attributes or claims associated with this identity. + * + * @return the attributes or claims associated with this identity + */ + Attributes getAttributes(); + + /** + * Indicates if this identity is granted with a role with the given roleName. + * + * @param roleName the name of the role + * + * @return true if the identity has the given role. Otherwise, it returns false. + */ + default boolean hasRole(String roleName) { + return getAttributes().containsValue("roles", roleName); + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/identity/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/identity/package-info.java new file mode 100644 index 00000000000..47a5746dfee --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/identity/package-info.java @@ -0,0 +1,24 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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. + */ + +/** + * Provides classes related with the representation and management of identities. + * + * @author Pedro Igor + */ +package org.keycloak.authorization.identity; \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/Policy.java b/server-spi/src/main/java/org/keycloak/authorization/model/Policy.java new file mode 100644 index 00000000000..1960d6abb44 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/model/Policy.java @@ -0,0 +1,193 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.model; + +import java.util.Map; +import java.util.Set; + +/** + * Represents an authorization policy and all the configuration associated with it. + * + * @author Pedro Igor + */ +public interface Policy { + + /** + * Returns the unique identifier for this instance. + * + * @return the unique identifier for this instance + */ + String getId(); + + /** + * Returns the type of this policy. + * + * @return the type of this policy + */ + String getType(); + + /** + * Returns the {@link DecisionStrategy} for this policy. + * + * @return the decision strategy defined for this policy + */ + DecisionStrategy getDecisionStrategy(); + + /** + * Sets the {DecisionStrategy} for this policy. + * + * @return the decision strategy for this policy + */ + void setDecisionStrategy(DecisionStrategy decisionStrategy); + + /** + * Returns the {@link Logic} for this policy. + * + * @return the decision strategy defined for this policy + */ + Logic getLogic(); + + /** + * Sets the {Logic} for this policy. + * + * @return the decision strategy for this policy + */ + void setLogic(Logic logic); + + /** + * Returns a {@link Map} holding string-based key/value pairs representing any additional configuration for this policy. + * + * @return a map with any additional configuration defined for this policy. + */ + Map getConfig(); + + /** + * Sets a {@link Map} with string-based key/value pairs representing any additional configuration for this policy. + * + * @return a map with any additional configuration for this policy. + */ + void setConfig(Map config); + + /** + * Returns the name of this policy. + * + * @return the name of this policy + */ + String getName(); + + /** + * Sets an unique name to this policy. + * + * @param name an unique name + */ + void setName(String name); + + /** + * Returns the description of this policy. + * + * @return a description or null of there is no description + */ + String getDescription(); + + /** + * Sets the description for this policy. + * + * @param description a description + */ + void setDescription(String description); + + /** + * Returns the {@link ResourceServer} where this policy belongs to. + * + * @return a resource server + */ + R getResourceServer(); + + /** + * Returns the {@link Policy} instances associated with this policy and used to evaluate authorization decisions when + * this policy applies. + * + * @return the associated policies or an empty set if no policy is associated with this policy + */ +

Set

getAssociatedPolicies(); + + /** + * Returns the {@link Resource} instances where this policy applies. + * + * @return a set with all resource instances where this policy applies. Or an empty set if there is no resource associated with this policy + */ + Set getResources(); + + /** + * Returns the {@link Scope} instances where this policy applies. + * + * @return a set with all scope instances where this policy applies. Or an empty set if there is no scope associated with this policy + */ + Set getScopes(); + + void addScope(Scope scope); + + void removeScope(Scope scope); + + void addAssociatedPolicy(Policy associatedPolicy); + + void removeAssociatedPolicy(Policy associatedPolicy); + + void addResource(Resource resource); + + void removeResource(Resource resource); + + /** + * The decision strategy dictates how the policies associated with a given policy are evaluated and how a final decision + * is obtained. + */ + enum DecisionStrategy { + /** + * Defines that at least one policy must evaluate to a positive decision in order to the overall decision be also positive. + */ + AFFIRMATIVE, + + /** + * Defines that all policies must evaluate to a positive decision in order to the overall decision be also positive. + */ + UNANIMOUS, + + /** + * Defines that the number of positive decisions must be greater than the number of negative decisions. If the number of positive and negative is the same, + * the final decision will be negative. + */ + CONSENSUS + } + + /** + * The decision strategy dictates how the policies associated with a given policy are evaluated and how a final decision + * is obtained. + */ + enum Logic { + /** + * Defines that this policy follows a positive logic. In other words, the final decision is the policy outcome. + */ + POSITIVE, + + /** + * Defines that this policy uses a logical negation. In other words, the final decision would be a negative of the policy outcome. + */ + NEGATIVE, + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/Resource.java b/server-spi/src/main/java/org/keycloak/authorization/model/Resource.java new file mode 100644 index 00000000000..2bf2c6fa3f3 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/model/Resource.java @@ -0,0 +1,116 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.model; + +import java.util.List; +import java.util.Set; + +/** + * Represents a resource, which is usually protected by a set of policies within a resource server. + * + * @author Pedro Igor + */ +public interface Resource { + + /** + * Returns the unique identifier for this instance. + * + * @return the unique identifier for this instance + */ + String getId(); + + /** + * Returns the resource's name. + * + * @return the name of this resource + */ + String getName(); + + /** + * Sets a name for this resource. The name must be unique. + * + * @param name the name of this resource + */ + void setName(String name); + + /** + * Returns a {@link java.net.URI} that uniquely identify this resource. + * + * @return an {@link java.net.URI} for this resource or null if not defined. + */ + String getUri(); + + /** + * Sets a {@link java.net.URI} that uniquely identify this resource. + * + * @param uri an {@link java.net.URI} for this resource + */ + void setUri(String uri); + + /** + * Returns a string representing the type of this resource. + * + * @return the type of this resource or null if not defined + */ + String getType(); + + /** + * Sets a string representing the type of this resource. + * + * @return the type of this resource or null if not defined + */ + void setType(String type); + + /** + * Returns a {@link List} containing all the {@link Scope} associated with this resource. + * + * @return a list with all scopes associated with this resource + */ + List getScopes(); + + /** + * Returns an icon {@link java.net.URI} for this resource. + * + * @return a uri for an icon + */ + String getIconUri(); + + /** + * Sets an icon {@link java.net.URI} for this resource. + * + * @return a uri for an icon + */ + void setIconUri(String iconUri); + + /** + * Returns the {@link ResourceServer} to where this resource belongs to. + * + * @return the resource server associated with this resource + */ + R getResourceServer(); + + /** + * Returns the resource's owner, which is usually an identifier that uniquely identifies the resource's owner. + * + * @return the owner of this resource + */ + String getOwner(); + + void updateScopes(Set scopes); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/ResourceServer.java b/server-spi/src/main/java/org/keycloak/authorization/model/ResourceServer.java new file mode 100644 index 00000000000..2424c8d22e7 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/model/ResourceServer.java @@ -0,0 +1,91 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.model; + +/** + * Represents a resource server, whose resources are managed and protected. A resource server is basically an existing + * client application in Keycloak that will also act as a resource server. + * + * @author Pedro Igor + */ +public interface ResourceServer { + + /** + * Returns the unique identifier for this instance. + * + * @return the unique identifier for this instance + */ + String getId(); + + /** + * Returns the identifier of the client application (which already exists in Keycloak) that is also acting as a resource + * server. + * + * @return the identifier of the client application associated with this instance. + */ + String getClientId(); + + /** + * Indicates if the resource server is allowed to manage its own resources remotely using the Protection API. + * + * {@code true} if the resource server is allowed to managed them remotely + */ + boolean isAllowRemoteResourceManagement(); + + /** + * Indicates if the resource server is allowed to manage its own resources remotely using the Protection API. + * + * @param allowRemoteResourceManagement {@code true} if the resource server is allowed to managed them remotely + */ + void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement); + + /** + * Returns the {@code PolicyEnforcementMode} configured for this instance. + * + * @return the {@code PolicyEnforcementMode} configured for this instance. + */ + PolicyEnforcementMode getPolicyEnforcementMode(); + + /** + * Defines a {@code PolicyEnforcementMode} for this instance. + * + * @param enforcementMode one of the available options in {@code PolicyEnforcementMode} + */ + void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode); + + /** + * The policy enforcement mode dictates how authorization requests are handled by the server. + */ + enum PolicyEnforcementMode { + /** + * Requests are denied by default even when there is no policy associated with a given resource. + */ + ENFORCING, + + /** + * Requests are allowed even when there is no policy associated with a given resource. + */ + PERMISSIVE, + + /** + * Completely disables the evaluation of policies and allow access to any resource. + */ + DISABLED + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/Scope.java b/server-spi/src/main/java/org/keycloak/authorization/model/Scope.java new file mode 100644 index 00000000000..e13a789d8ae --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/model/Scope.java @@ -0,0 +1,70 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.model; + +/** + * Represents a scope, which is usually associated with one or more resources in order to define the actions that can be performed + * or a specific access context. + * + * @author Pedro Igor + */ +public interface Scope { + + /** + * Returns the unique identifier for this instance. + * + * @return the unique identifier for this instance + */ + String getId(); + + /** + * Returns the name of this scope. + * + * @return the name of this scope + */ + String getName(); + + /** + * Sets a name for this scope. The name must be unique. + * + * @param name the name of this scope + */ + void setName(String name); + + /** + * Returns an icon {@link java.net.URI} for this scope. + * + * @return a uri for an icon + */ + String getIconUri(); + + /** + * Sets an icon {@link java.net.URI} for this scope. + * + * @return a uri for an icon + */ + void setIconUri(String iconUri); + + /** + * Returns the {@link ResourceServer} instance to where this scope belongs to. + * + * @return + */ + ResourceServer getResourceServer(); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/model/package-info.java new file mode 100644 index 00000000000..38f9a8fdc88 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/model/package-info.java @@ -0,0 +1,24 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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. + */ + +/** + * Provides the domain model and any other type related with it + * + * @author Pedro Igor + */ +package org.keycloak.authorization.model; \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/authorization/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/package-info.java new file mode 100644 index 00000000000..6ff51afbcb6 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/package-info.java @@ -0,0 +1,24 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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. + */ + +/** + * Fine-grained Authorization SPI. + * + * @author Pedro Igor + */ +package org.keycloak.authorization; \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java b/server-spi/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java new file mode 100644 index 00000000000..1eef22acdc6 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java @@ -0,0 +1,71 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.permission; + +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; + +import java.util.Collections; +import java.util.List; + +/** + * Represents a permission for a given resource. + * + * @author Pedro Igor + */ +public class ResourcePermission { + + private final Resource resource; + private final List scopes; + private ResourceServer resourceServer; + + public ResourcePermission(Resource resource, List scopes, ResourceServer resourceServer) { + this.resource = resource; + this.scopes = scopes; + this.resourceServer = resourceServer; + } + + /** + * Returns the resource to which this permission applies. + * + * @return the resource to which this permission applies + */ + public Resource getResource() { + return this.resource; + } + + /** + * Returns a list of permitted scopes associated with the resource + * + * @return a lit of permitted scopes + */ + public List getScopes() { + return Collections.unmodifiableList(this.scopes); + } + + /** + * Returns the resource server associated with this permission. + * + * @return the resource server + */ + public ResourceServer getResourceServer() { + return this.resourceServer; + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java new file mode 100644 index 00000000000..e26ad1c87ec --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.permission.evaluator; + +import org.keycloak.authorization.permission.ResourcePermission; +import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator; +import org.keycloak.authorization.policy.evaluation.EvaluationContext; +import org.keycloak.authorization.policy.provider.PolicyProviderFactory; + +import java.util.List; +import java.util.concurrent.Executor; + +/** + * A factory for the different {@link PermissionEvaluator} implementations. + * + * @author Pedro Igor + */ +public final class Evaluators { + + private final List policyProviderFactories; + private final DefaultPolicyEvaluator policyEvaluator; + private final Executor scheduler; + + public Evaluators(List policyProviderFactories, DefaultPolicyEvaluator policyEvaluator, Executor scheduler) { + this.policyProviderFactories = policyProviderFactories; + this.policyEvaluator = policyEvaluator; + this.scheduler = scheduler; + } + + public PermissionEvaluator from(List permissions, EvaluationContext evaluationContext) { + return schedule(permissions, evaluationContext); + } + + public PermissionEvaluator schedule(List permissions, EvaluationContext evaluationContext) { + return new ScheduledPermissionEvaluator(new IterablePermissionEvaluator(permissions.iterator(), evaluationContext, this.policyEvaluator), this.scheduler); + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java new file mode 100644 index 00000000000..dfda6a7bb6d --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.permission.evaluator; + +import org.keycloak.authorization.Decision; +import org.keycloak.authorization.permission.ResourcePermission; +import org.keycloak.authorization.policy.evaluation.EvaluationContext; +import org.keycloak.authorization.policy.evaluation.PolicyEvaluator; + +import java.util.Iterator; + +/** + * @author Pedro Igor + */ +class IterablePermissionEvaluator implements PermissionEvaluator { + + private final Iterator permissions; + private final EvaluationContext executionContext; + private final PolicyEvaluator policyEvaluator; + + IterablePermissionEvaluator(Iterator permissions, EvaluationContext executionContext, PolicyEvaluator policyEvaluator) { + this.permissions = permissions; + this.executionContext = executionContext; + this.policyEvaluator = policyEvaluator; + } + + @Override + public void evaluate(Decision decision) { + try { + while (this.permissions.hasNext()) { + this.policyEvaluator.evaluate(this.permissions.next(), this.executionContext, decision); + } + decision.onComplete(); + } catch (Throwable cause) { + decision.onError(cause); + } + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java new file mode 100644 index 00000000000..c129caf02dd --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java @@ -0,0 +1,31 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.permission.evaluator; + +import org.keycloak.authorization.Decision; + +/** + * An {@link PermissionEvaluator} represents a source of {@link org.keycloak.authorization.permission.ResourcePermission}, responsible for emitting these permissions + * to a consumer in order to evaluate the authorization policies based on a {@link org.keycloak.authorization.policy.evaluation.EvaluationContext}. + * + * @author Pedro Igor + */ +public interface PermissionEvaluator { + + void evaluate(Decision decision); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java new file mode 100644 index 00000000000..13e08e4ebc4 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.permission.evaluator; + +import org.keycloak.authorization.Decision; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * @author Pedro Igor + * @see PermissionEvaluator + */ +class ScheduledPermissionEvaluator implements PermissionEvaluator { + + private final PermissionEvaluator publisher; + private final Executor scheduler; + + ScheduledPermissionEvaluator(PermissionEvaluator publisher, Executor scheduler) { + this.publisher = publisher; + this.scheduler = scheduler; + } + + @Override + public void evaluate(Decision decision) { + CompletableFuture.runAsync(() -> publisher.evaluate(decision), scheduler); + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java new file mode 100644 index 00000000000..f06eb3f4f26 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java @@ -0,0 +1,102 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.evaluation; + +import org.keycloak.authorization.Decision; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.permission.ResourcePermission; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public abstract class DecisionResultCollector implements Decision { + + private Map results = new HashMap(); + + @Override + public void onDecision(DefaultEvaluation evaluation) { + if (evaluation.getParentPolicy() != null) { + results.computeIfAbsent(evaluation.getPermission(), Result::new).policy(evaluation.getParentPolicy()).policy(evaluation.getPolicy()).setStatus(evaluation.getEffect()); + } else { + results.computeIfAbsent(evaluation.getPermission(), Result::new).setStatus(evaluation.getEffect()); + } + } + + @Override + public void onComplete() { + for (Result result : results.values()) { + for (Result.PolicyResult policyResult : result.getResults()) { + if (isGranted(policyResult)) { + policyResult.setStatus(Effect.PERMIT); + } else { + policyResult.setStatus(Effect.DENY); + } + } + + if (result.getResults().stream() + .filter(policyResult -> Effect.DENY.equals(policyResult.getStatus())).count() > 0) { + result.setStatus(Effect.DENY); + } else { + result.setStatus(Effect.PERMIT); + } + } + + onComplete(results.values().stream().collect(Collectors.toList())); + } + + protected abstract void onComplete(List results); + + private boolean isGranted(Result.PolicyResult policyResult) { + List values = policyResult.getAssociatedPolicies(); + + int grantCount = 0; + int denyCount = policyResult.getPolicy().getAssociatedPolicies().size(); + + for (Result.PolicyResult decision : values) { + if (decision.getStatus().equals(Effect.PERMIT)) { + grantCount++; + denyCount--; + } + } + + Policy policy = policyResult.getPolicy(); + Policy.DecisionStrategy decisionStrategy = policy.getDecisionStrategy(); + + if (decisionStrategy == null) { + decisionStrategy = Policy.DecisionStrategy.UNANIMOUS; + } + + if (Policy.DecisionStrategy.AFFIRMATIVE.equals(decisionStrategy) && grantCount > 0) { + return true; + } else if (Policy.DecisionStrategy.UNANIMOUS.equals(decisionStrategy) && denyCount == 0) { + return true; + } else if (Policy.DecisionStrategy.CONSENSUS.equals(decisionStrategy)) { + if (grantCount > denyCount) { + return true; + } + } + + return false; + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java new file mode 100644 index 00000000000..df379af30e2 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java @@ -0,0 +1,105 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.evaluation; + +import org.keycloak.authorization.Decision; +import org.keycloak.authorization.Decision.Effect; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Policy.Logic; +import org.keycloak.authorization.permission.ResourcePermission; + +/** + * @author Pedro Igor + */ +public class DefaultEvaluation implements Evaluation { + + private final ResourcePermission permission; + private final EvaluationContext executionContext; + private final Decision decision; + private final Policy policy; + private final Policy parentPolicy; + private Effect effect; + + public DefaultEvaluation(ResourcePermission permission, EvaluationContext executionContext, Policy parentPolicy, Policy policy, Decision decision) { + this.permission = permission; + this.executionContext = executionContext; + this.parentPolicy = parentPolicy; + this.policy = policy; + this.decision = decision; + } + + /** + * Returns the {@link ResourcePermission} to be evaluated. + * + * @return the permission to be evaluated + */ + public ResourcePermission getPermission() { + return this.permission; + } + + /** + * Returns the {@link org.keycloak.authorization.permission.evaluator.PermissionEvaluator}. Which provides access to the whole evaluation runtime context. + * + * @return the evaluation context + */ + public EvaluationContext getContext() { + return this.executionContext; + } + + /** + * Grants all the requested permissions to the caller. + */ + public void grant() { + if (policy != null && Logic.NEGATIVE.equals(policy.getLogic())) { + this.effect = Effect.DENY; + } else { + this.effect = Effect.PERMIT; + } + + this.decision.onDecision(this); + } + + public void deny() { + if (policy != null && Logic.NEGATIVE.equals(policy.getLogic())) { + this.effect = Effect.PERMIT; + } else { + this.effect = Effect.DENY; + } + + this.decision.onDecision(this); + } + + public Policy getPolicy() { + return this.policy; + } + + public Policy getParentPolicy() { + return this.parentPolicy; + } + + public Effect getEffect() { + return effect; + } + + void denyIfNoEffect() { + if (this.effect == null) { + deny(); + } + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java new file mode 100644 index 00000000000..8b12558a671 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java @@ -0,0 +1,156 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.evaluation; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.Decision; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.ResourceServer.PolicyEnforcementMode; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.permission.ResourcePermission; +import org.keycloak.authorization.policy.provider.PolicyProvider; +import org.keycloak.authorization.policy.provider.PolicyProviderFactory; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class DefaultPolicyEvaluator implements PolicyEvaluator { + + private final AuthorizationProvider authorization; + private Map policyProviders = new HashMap<>(); + + public DefaultPolicyEvaluator(AuthorizationProvider authorization, List policyProviderFactories) { + this.authorization = authorization; + + for (PolicyProviderFactory providerFactory : policyProviderFactories) { + this.policyProviders.put(providerFactory.getId(), providerFactory); + } + } + + @Override + public void evaluate(ResourcePermission permission, EvaluationContext executionContext, Decision decision) { + ResourceServer resourceServer = permission.getResourceServer(); + + if (PolicyEnforcementMode.DISABLED.equals(resourceServer.getPolicyEnforcementMode())) { + createEvaluation(permission, executionContext, decision, null, null).grant(); + return; + } + + StoreFactory storeFactory = this.authorization.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + AtomicInteger policiesCount = new AtomicInteger(0); + Consumer consumer = createDecisionConsumer(permission, executionContext, decision, policiesCount); + Resource resource = permission.getResource(); + + if (resource != null) { + List resourcePolicies = policyStore.findByResource(resource.getId()); + + if (!resourcePolicies.isEmpty()) { + resourcePolicies.forEach(consumer); + } + + if (resource.getType() != null) { + policyStore.findByResourceType(resource.getType(), resourceServer.getId()).forEach(consumer); + } + + if (permission.getScopes().isEmpty() && !resource.getScopes().isEmpty()) { + policyStore.findByScopeIds(resource.getScopes().stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()).forEach(consumer); + } + } + + if (!permission.getScopes().isEmpty()) { + policyStore.findByScopeIds(permission.getScopes().stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()).forEach(consumer); + } + + if (PolicyEnforcementMode.PERMISSIVE.equals(resourceServer.getPolicyEnforcementMode()) && policiesCount.get() == 0) { + createEvaluation(permission, executionContext, decision, null, null).grant(); + } + } + + private Consumer createDecisionConsumer(ResourcePermission permission, EvaluationContext executionContext, Decision decision, AtomicInteger policiesCount) { + return (parentPolicy) -> { + if (hasRequestedScopes(permission, parentPolicy)) { + for (Policy associatedPolicy : parentPolicy.getAssociatedPolicies()) { + PolicyProviderFactory providerFactory = policyProviders.get(associatedPolicy.getType()); + + if (providerFactory == null) { + throw new RuntimeException("Could not find a policy provider for policy type [" + associatedPolicy.getType() + "]."); + } + + PolicyProvider policyProvider = providerFactory.create(associatedPolicy, this.authorization); + + if (policyProvider == null) { + throw new RuntimeException("Unknown parentPolicy provider for type [" + associatedPolicy.getType() + "]."); + } + + DefaultEvaluation evaluation = createEvaluation(permission, executionContext, decision, parentPolicy, associatedPolicy); + + policyProvider.evaluate(evaluation); + evaluation.denyIfNoEffect(); + + policiesCount.incrementAndGet(); + } + } + }; + } + + private DefaultEvaluation createEvaluation(ResourcePermission permission, EvaluationContext executionContext, Decision decision, Policy parentPolicy, Policy associatedPolicy) { + return new DefaultEvaluation(permission, executionContext, parentPolicy, associatedPolicy, decision); + } + + private boolean hasRequestedScopes(final ResourcePermission permission, final Policy policy) { + if (permission.getScopes().isEmpty()) { + return true; + } + + if (policy.getScopes().isEmpty()) { + return true; + } + + boolean hasScope = true; + + for (Scope givenScope : policy.getScopes()) { + boolean hasGivenScope = false; + + for (Scope scope : permission.getScopes()) { + if (givenScope.getId().equals(scope.getId())) { + hasGivenScope = true; + break; + } + } + + if (!hasGivenScope) { + return false; + } + } + + return hasScope; + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java new file mode 100644 index 00000000000..f5b08682b70 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java @@ -0,0 +1,54 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.evaluation; + +import org.keycloak.authorization.permission.ResourcePermission; + +/** + *

An {@link Evaluation} is mainly used by {@link org.keycloak.authorization.policy.provider.PolicyProvider} in order to evaluate a single + * and specific {@link ResourcePermission} against the configured policies. + * + * @author Pedro Igor + */ +public interface Evaluation { + + /** + * Returns the {@link ResourcePermission} to be evaluated. + * + * @return the permission to be evaluated + */ + ResourcePermission getPermission(); + + /** + * Returns the {@link EvaluationContext}. Which provides access to the whole evaluation runtime context. + * + * @return the evaluation context + */ + EvaluationContext getContext(); + + /** + * Grants the requested permission to the caller. + */ + void grant(); + + /** + * Denies the requested permission. + */ + void deny(); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/EvaluationContext.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/EvaluationContext.java new file mode 100644 index 00000000000..db5ed0421cb --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/EvaluationContext.java @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.evaluation; + +import org.keycloak.authorization.attribute.Attributes; +import org.keycloak.authorization.identity.Identity; + +/** + * This interface serves as a bridge between the policy evaluation runtime and the environment in which it is running. When evaluating + * policies, this interface can be used to query information from the execution environment/context and enrich decisions. + * + * @author Pedro Igor + */ +public interface EvaluationContext { + + /** + * Returns the {@link Identity} that represents an entity (person or non-person) to which the permissions must be granted, or not. + * + * @return the identity to which the permissions must be granted, or not + */ + Identity getIdentity(); + + /** + * Returns all attributes within the current execution and runtime environment. + * + * @return the attributes within the current execution and runtime environment + */ + Attributes getAttributes(); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java new file mode 100644 index 00000000000..c380ba67afd --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.evaluation; + +import org.keycloak.authorization.Decision; +import org.keycloak.authorization.permission.ResourcePermission; + +/** + *

A {@link PolicyEvaluator} evaluates authorization policies based on a given {@link ResourcePermission}, sending + * the results to a {@link Decision} point through the methods defined in that interface. + * + * @author Pedro Igor + */ +public interface PolicyEvaluator { + + /** + * Starts the evaluation of the configured authorization policies. + * + * @param decision a {@link Decision} point to where notifications events will be delivered during the evaluation + */ + void evaluate(ResourcePermission permission, EvaluationContext executionContext, Decision decision); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java new file mode 100644 index 00000000000..325af3d2496 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java @@ -0,0 +1,120 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.evaluation; + +import org.keycloak.authorization.Decision.Effect; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.permission.ResourcePermission; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class Result { + + private final ResourcePermission permission; + private List results = new ArrayList<>(); + private Effect status; + + public Result(ResourcePermission permission) { + this.permission = permission; + } + + public ResourcePermission getPermission() { + return permission; + } + + public List getResults() { + return results; + } + + public PolicyResult policy(Policy policy) { + for (PolicyResult result : this.results) { + if (result.getPolicy().equals(policy)) { + return result; + } + } + + PolicyResult policyResult = new PolicyResult(policy); + + this.results.add(policyResult); + + return policyResult; + } + + public void setStatus(final Effect status) { + this.status = status; + } + + public Effect getEffect() { + return status; + } + + public static class PolicyResult { + + private final Policy policy; + private List associatedPolicies = new ArrayList<>(); + private Effect status; + + public PolicyResult(Policy policy) { + this.policy = policy; + } + + public PolicyResult status(Effect status) { + this.status = status; + return this; + } + + public PolicyResult policy(Policy policy) { + return getPolicy(policy, this.associatedPolicies); + } + + private PolicyResult getPolicy(Policy policy, List results) { + for (PolicyResult result : results) { + if (result.getPolicy().equals(policy)) { + return result; + } + } + + PolicyResult policyResult = new PolicyResult(policy); + + results.add(policyResult); + + return policyResult; + } + + public Policy getPolicy() { + return policy; + } + + public List getAssociatedPolicies() { + return associatedPolicies; + } + + public Effect getStatus() { + return status; + } + + public void setStatus(final Effect status) { + this.status = status; + } + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/package-info.java new file mode 100644 index 00000000000..dcae2ed0e4b --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/package-info.java @@ -0,0 +1,24 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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. + */ + +/** + * Provides classes related with the evaluation of policies. + * + * @author Pedro Igor + */ +package org.keycloak.authorization.policy.evaluation; \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProvider.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProvider.java new file mode 100644 index 00000000000..2405c3b0b50 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProvider.java @@ -0,0 +1,29 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.provider; + +import org.keycloak.authorization.policy.evaluation.Evaluation; +import org.keycloak.provider.Provider; + +/** + * @author Pedro Igor + */ +public interface PolicyProvider extends Provider { + + void evaluate(Evaluation evaluation); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderAdminService.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderAdminService.java new file mode 100644 index 00000000000..d26208efff0 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderAdminService.java @@ -0,0 +1,33 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.provider; + +import org.keycloak.authorization.model.Policy; + +/** + * @author Pedro Igor + */ +public interface PolicyProviderAdminService { + + void onCreate(Policy policy); + + void onUpdate(Policy policy); + + void onRemove(Policy policy); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java new file mode 100644 index 00000000000..1beedd9e5f7 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java @@ -0,0 +1,39 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.provider; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.provider.ProviderEvent; +import org.keycloak.provider.ProviderFactory; + +/** + * @author Pedro Igor + */ +public interface PolicyProviderFactory extends ProviderFactory { + + String getName(); + + String getGroup(); + + PolicyProvider create(Policy policy, AuthorizationProvider authorization); + + PolicyProviderAdminService getAdminResource(ResourceServer resourceServer); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicySpi.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicySpi.java new file mode 100644 index 00000000000..422981d0c95 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicySpi.java @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.policy.provider; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Pedro Igor + */ +public class PolicySpi implements Spi { + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "policy"; + } + + @Override + public Class getProviderClass() { + return PolicyProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return PolicyProviderFactory.class; + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/package-info.java new file mode 100644 index 00000000000..6a66949d191 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/package-info.java @@ -0,0 +1,24 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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. + */ + +/** + * Provides classes and a SPI to plug different policy providers. + * + * @author Pedro Igor + */ +package org.keycloak.authorization.policy.provider; \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/AuthorizationStoreFactory.java b/server-spi/src/main/java/org/keycloak/authorization/store/AuthorizationStoreFactory.java new file mode 100644 index 00000000000..dac1b336953 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/AuthorizationStoreFactory.java @@ -0,0 +1,63 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store; + +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.authorization.store.syncronization.ClientApplicationSynchronizer; +import org.keycloak.authorization.store.syncronization.RealmSynchronizer; +import org.keycloak.authorization.store.syncronization.Synchronizer; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel.ClientRemovedEvent; +import org.keycloak.models.RealmModel.RealmRemovedEvent; +import org.keycloak.provider.ProviderEvent; +import org.keycloak.provider.ProviderFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Pedro Igor + */ +public interface AuthorizationStoreFactory extends ProviderFactory { + + @Override + default void postInit(KeycloakSessionFactory factory) { + registerSynchronizationListeners(factory); + } + + default void registerSynchronizationListeners(KeycloakSessionFactory factory) { + Map, Synchronizer> synchronizers = new HashMap<>(); + + synchronizers.put(ClientRemovedEvent.class, new ClientApplicationSynchronizer()); + synchronizers.put(RealmRemovedEvent.class, new RealmSynchronizer()); + + factory.register(event -> { + try { + synchronizers.forEach((eventType, synchronizer) -> { + if (eventType.isInstance(event)) { + synchronizer.synchronize(event, factory); + } + }); + } catch (Exception e) { + throw new RuntimeException("Error synchronizing authorization data.", e); + } + }); + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/PolicyStore.java b/server-spi/src/main/java/org/keycloak/authorization/store/PolicyStore.java new file mode 100644 index 00000000000..f55db9971ef --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/PolicyStore.java @@ -0,0 +1,119 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store; + + +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; + +import java.util.List; + +/** + * A {@link PolicyStore} is responsible to manage the persistence of {@link Policy} instances. + * + * @author Pedro Igor + */ +public interface PolicyStore { + + /** + * Creates a new {@link Policy} instance. The new instance is not necessarily persisted though, which may require + * a call to the {#save} method to actually make it persistent. + * + * @param name the name of the policy + * @param type the type of the policy + * @param resourceServer the resource server to which this policy belongs + * @return a new instance of {@link Policy} + */ + Policy create(String name, String type, ResourceServer resourceServer); + + /** + * Deletes a policy from the underlying persistence mechanism. + * + * @param id the id of the policy to delete + */ + void delete(String id); + + /** + * Returns a {@link Policy} with the given id + * + * @param id the identifier of the policy + * @return a policy with the given identifier. + */ + Policy findById(String id); + + /** + * Returns a {@link Policy} with the given name + * + * @param name the name of the policy + * @param resourceServerId the resource server id + * @return a policy with the given name. + */ + Policy findByName(String name, String resourceServerId); + + /** + * Returns a list of {@link Policy} associated with a {@link ResourceServer} with the given resourceServerId. + * + * @param resourceServerId the identifier of a resource server + * @return a list of policies that belong to the given resource server + */ + List findByResourceServer(String resourceServerId); + + /** + * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given resourceId. + * + * @param resourceId the identifier of a resource + * @return a list of policies associated with the given resource + */ + List findByResource(String resourceId); + + /** + * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given type. + * + * @param resourceType the type of a resource + * @param resourceServerId the resource server id + * @return a list of policies associated with the given resource type + */ + List findByResourceType(String resourceType, String resourceServerId); + + /** + * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Scope} with the given scopeIds. + * + * @param scopeIds the id of the scopes + * @param resourceServerId the resource server id + * @return a list of policies associated with the given scopes + */ + List findByScopeIds(List scopeIds, String resourceServerId); + + /** + * Returns a list of {@link Policy} with the given type. + * + * @param type the type of the policy + * @return a list of policies with the given type + */ + List findByType(String type); + + /** + * Returns a list of {@link Policy} that depends on another policy with the given id. + * + * @param id the id of the policy to query its dependents + * @return a list of policies that depends on the a policy with the given identifier + */ + List findDependentPolicies(String id); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/ResourceServerStore.java b/server-spi/src/main/java/org/keycloak/authorization/store/ResourceServerStore.java new file mode 100644 index 00000000000..742f98b299c --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/ResourceServerStore.java @@ -0,0 +1,63 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store; + +import org.keycloak.authorization.model.ResourceServer; + +/** + * A {@link ResourceServerStore} is responsible to manage the persistence of {@link ResourceServer} instances. + * + * @author Pedro Igor + */ +public interface ResourceServerStore { + + /** + *

Creates a {@link ResourceServer} instance backed by this persistent storage implementation. + * + * @param clientId the client id acting as a resource server + * + * @return an instance backed by the underlying storage implementation + */ + ResourceServer create(String clientId); + + /** + * Removes a {@link ResourceServer} instance, with the given {@code id} from the persistent storage. + * + * @param id the identifier of an existing resource server instance + */ + void delete(String id); + + /** + * Returns a {@link ResourceServer} instance based on its identifier. + * + * @param id the identifier of an existing resource server instance + * + * @return the resource server instance with the given identifier or null if no instance was found + */ + ResourceServer findById(String id); + + /** + * Returns a {@link ResourceServer} instance based on the identifier of a client application. + * + * @param id the identifier of an existing client application + * + * @return the resource server instance, with the given client id or null if no instance was found + */ + ResourceServer findByClient(String id); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/ResourceStore.java b/server-spi/src/main/java/org/keycloak/authorization/store/ResourceStore.java new file mode 100644 index 00000000000..5b92808b1e7 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/ResourceStore.java @@ -0,0 +1,99 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store; + +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; + +import java.util.List; +import java.util.Set; + +/** + * A {@link ResourceStore} is responsible to manage the persistence of {@link Resource} instances. + * + * @author Pedro Igor + */ +public interface ResourceStore { + + /** + *

Creates a {@link Resource} instance backed by this persistent storage implementation. + * + * @param name the name of this resource. It must be unique. + * @param resourceServer the resource server to where the given resource belongs to + * @param owner the owner of this resource or null if the resource server is the owner + * @return an instance backed by the underlying storage implementation + */ + Resource create(String name, ResourceServer resourceServer, String owner); + + /** + * Removes a {@link Resource} instance, with the given {@code id} from the persistent storage. + * + * @param id the identifier of an existing resource instance + */ + void delete(String id); + + /** + * Returns a {@link Resource} instance based on its identifier. + * + * @param id the identifier of an existing resource instance + * @return the resource instance with the given identifier or null if no instance was found + */ + Resource findById(String id); + + /** + * Finds all {@link Resource} instances with the given {@code ownerId}. + * + * @param ownerId the identifier of the owner + * @return a list with all resource instances owned by the given owner + */ + List findByOwner(String ownerId); + + /** + * Finds all {@link Resource} instances associated with a given resource server. + * + * @param resourceServerId the identifier of the resource server + * @return a list with all resources associated with the given resource server + */ + List findByResourceServer(String resourceServerId); + + /** + * Finds all {@link Resource} associated with a given scope. + * + * @param id one or more scope identifiers + * @return a list of resources associated with the given scope(s) + */ + List findByScope(String... id); + + /** + * Find a {@link Resource} by its name. + * + * @param name the name of the resource + * @param resourceServerId the identifier of the resource server + * @return a resource with the given name + */ + Resource findByName(String name, String resourceServerId); + + /** + * Finds all {@link Resource} with the given type. + * + * @param type the type of the resource + * @return a list of resources with the given type + */ + List findByType(String type); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/ScopeStore.java b/server-spi/src/main/java/org/keycloak/authorization/store/ScopeStore.java new file mode 100644 index 00000000000..501217f5e77 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/ScopeStore.java @@ -0,0 +1,78 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store; + + +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; + +import java.util.List; + +/** + * A {@link ScopeStore} is responsible to manage the persistence of {@link Scope} instances. + * + * @author Pedro Igor + */ +public interface ScopeStore { + + /** + * Creates a new {@link Scope} instance. The new instance is not necessarily persisted though, which may require + * a call to the {#save} method to actually make it persistent. + * + * @param name the name of the scope + * @param resourceServer the resource server to which this scope belongs + * + * @return a new instance of {@link Scope} + */ + Scope create(String name, ResourceServer resourceServer); + + /** + * Deletes a scope from the underlying persistence mechanism. + * + * @param id the id of the scope to delete + */ + void delete(String id); + + /** + * Returns a {@link Scope} with the given id + * + * @param id the identifier of the scope + * + * @return a scope with the given identifier. + */ + Scope findById(String id); + + /** + * Returns a {@link Scope} with the given name + * + * @param name the name of the scope + * + * @param resourceServerId + * @return a scope with the given name. + */ + Scope findByName(String name, String resourceServerId); + + /** + * Returns a list of {@link Scope} associated with a {@link ResourceServer} with the given resourceServerId. + * + * @param resourceServerId the identifier of a resource server + * + * @return a list of scopes that belong to the given resource server + */ + List findByResourceServer(String id); +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactory.java b/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactory.java new file mode 100644 index 00000000000..4f50c11c49e --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactory.java @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store; + +import org.keycloak.provider.Provider; + +/** + *

A factory for the different types of storages that manage the persistence of the domain model types. + * + *

Implementations of this interface are usually related with the creation of those storage types accordingly with a + * specific persistence mechanism such as relational and NoSQL databases, filesystem, etc. + * + * @author Pedro Igor + */ +public interface StoreFactory extends Provider { + + /** + * Returns a {@link ResourceStore}. + * + * @return the resource store + */ + ResourceStore getResourceStore(); + + /** + * Returns a {@link ResourceServerStore}. + * + * @return the resource server store + */ + ResourceServerStore getResourceServerStore(); + + /** + * Returns a {@link ScopeStore}. + * + * @return the scope store + */ + ScopeStore getScopeStore(); + + /** + * Returns a {@link PolicyStore}. + * + * @return the policy store + */ + PolicyStore getPolicyStore(); + +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactorySpi.java b/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactorySpi.java new file mode 100644 index 00000000000..53bfb256c84 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactorySpi.java @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Pedro Igor + */ +public class StoreFactorySpi implements Spi { + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "authorizationPersister"; + } + + @Override + public Class getProviderClass() { + return StoreFactory.class; + } + + @Override + public Class getProviderFactoryClass() { + return AuthorizationStoreFactory.class; + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/store/package-info.java new file mode 100644 index 00000000000..d9800da36fa --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/package-info.java @@ -0,0 +1,24 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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. + */ + +/** + * Provides classes and a SPI to plug different metadata storage implementations. + * + * @author Pedro Igor + */ +package org.keycloak.authorization.store; \ No newline at end of file diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java new file mode 100644 index 00000000000..67683ff9de0 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java @@ -0,0 +1,51 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store.syncronization; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.AuthorizationStoreFactory; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel.ClientRemovedEvent; +import org.keycloak.provider.ProviderFactory; + +/** + * @author Pedro Igor + */ +public class ClientApplicationSynchronizer implements Synchronizer { + + @Override + public void synchronize(ClientRemovedEvent event, KeycloakSessionFactory factory) { + ProviderFactory providerFactory = factory.getProviderFactory(AuthorizationProvider.class); + AuthorizationProvider authorizationProvider = providerFactory.create(event.getKeycloakSession()); + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + ResourceServerStore store = storeFactory.getResourceServerStore(); + ResourceServer resourceServer = store.findByClient(event.getClient().getId()); + + if (resourceServer != null) { + String id = resourceServer.getId(); + storeFactory.getResourceStore().findByResourceServer(id).forEach(resource -> storeFactory.getResourceStore().delete(resource.getId())); + storeFactory.getScopeStore().findByResourceServer(id).forEach(scope -> storeFactory.getScopeStore().delete(scope.getId())); + storeFactory.getPolicyStore().findByResourceServer(id).forEach(scope -> storeFactory.getPolicyStore().delete(scope.getId())); + storeFactory.getResourceServerStore().delete(id); + } + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java new file mode 100644 index 00000000000..4f0ef32cd6b --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store.syncronization; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel.RealmRemovedEvent; +import org.keycloak.provider.ProviderFactory; + +/* + * @author Pedro Igor + */ +public class RealmSynchronizer implements Synchronizer { + @Override + public void synchronize(RealmRemovedEvent event, KeycloakSessionFactory factory) { + ProviderFactory providerFactory = factory.getProviderFactory(AuthorizationProvider.class); + AuthorizationProvider authorizationProvider = providerFactory.create(event.getKeycloakSession()); + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + + event.getRealm().getClients().forEach(clientModel -> { + ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(clientModel.getClientId()); + + if (resourceServer != null) { + String id = resourceServer.getId(); + storeFactory.getResourceStore().findByResourceServer(id).forEach(resource -> storeFactory.getResourceStore().delete(resource.getId())); + storeFactory.getScopeStore().findByResourceServer(id).forEach(scope -> storeFactory.getScopeStore().delete(scope.getId())); + storeFactory.getPolicyStore().findByResourceServer(id).forEach(scope -> storeFactory.getPolicyStore().delete(scope.getId())); + storeFactory.getResourceServerStore().delete(id); + } + }); + } +} diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/Synchronizer.java b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/Synchronizer.java new file mode 100644 index 00000000000..eb07947dba9 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/Synchronizer.java @@ -0,0 +1,31 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.store.syncronization; + +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderEvent; + +/** + * @author Pedro Igor + */ +public interface Synchronizer { + + void synchronize(E event, KeycloakSessionFactory factory); + +} diff --git a/server-spi/src/main/java/org/keycloak/models/AdminRoles.java b/server-spi/src/main/java/org/keycloak/models/AdminRoles.java index 34cdb363425..24455b8bfd6 100755 --- a/server-spi/src/main/java/org/keycloak/models/AdminRoles.java +++ b/server-spi/src/main/java/org/keycloak/models/AdminRoles.java @@ -37,13 +37,15 @@ public class AdminRoles { public static String VIEW_CLIENTS = "view-clients"; public static String VIEW_EVENTS = "view-events"; public static String VIEW_IDENTITY_PROVIDERS = "view-identity-providers"; + public static String VIEW_AUTHORIZATION = "view-authorization"; public static String MANAGE_REALM = "manage-realm"; public static String MANAGE_USERS = "manage-users"; public static String MANAGE_IDENTITY_PROVIDERS = "manage-identity-providers"; public static String MANAGE_CLIENTS = "manage-clients"; public static String MANAGE_EVENTS = "manage-events"; + public static String MANAGE_AUTHORIZATION = "manage-authorization"; - public static String[] ALL_REALM_ROLES = {CREATE_CLIENT, VIEW_REALM, VIEW_USERS, VIEW_CLIENTS, VIEW_EVENTS, VIEW_IDENTITY_PROVIDERS, MANAGE_REALM, MANAGE_USERS, MANAGE_CLIENTS, MANAGE_EVENTS, MANAGE_IDENTITY_PROVIDERS}; + public static String[] ALL_REALM_ROLES = {CREATE_CLIENT, VIEW_REALM, VIEW_USERS, VIEW_CLIENTS, VIEW_EVENTS, VIEW_IDENTITY_PROVIDERS, VIEW_AUTHORIZATION, MANAGE_REALM, MANAGE_USERS, MANAGE_CLIENTS, MANAGE_EVENTS, MANAGE_IDENTITY_PROVIDERS, MANAGE_AUTHORIZATION}; } diff --git a/server-spi/src/main/java/org/keycloak/models/Constants.java b/server-spi/src/main/java/org/keycloak/models/Constants.java index 460d08ff1fc..7f998dfe793 100755 --- a/server-spi/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi/src/main/java/org/keycloak/models/Constants.java @@ -37,6 +37,10 @@ public interface Constants { String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE}; String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS; + String AUTHZ_UMA_PROTECTION = "uma_protection"; + String AUTHZ_UMA_AUTHORIZATION = "uma_authorization"; + String[] AUTHZ_DEFAULT_AUTHORIZATION_ROLES = {AUTHZ_UMA_AUTHORIZATION}; + String DEFAULT_HASH_ALGORITHM = "pbkdf2"; // 15 minutes diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index 1c42e45e51d..9fe36acd423 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -37,10 +37,20 @@ public interface RealmModel extends RoleContainerModel { RealmModel getCreatedRealm(); } + interface RealmRemovedEvent extends ProviderEvent { + RealmModel getRealm(); + KeycloakSession getKeycloakSession(); + } + interface ClientCreationEvent extends ProviderEvent { ClientModel getCreatedClient(); } + interface ClientRemovedEvent extends ProviderEvent { + ClientModel getClient(); + KeycloakSession getKeycloakSession(); + } + interface UserFederationProviderCreationEvent extends ProviderEvent { UserFederationProviderModel getCreatedFederationProvider(); RealmModel getRealm(); diff --git a/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java b/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java index 24c60b303d4..00542eb08d6 100755 --- a/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java @@ -17,6 +17,8 @@ package org.keycloak.models; +import org.keycloak.provider.ProviderEvent; + import java.util.List; import java.util.Set; @@ -25,6 +27,12 @@ import java.util.Set; * @version $Revision: 1 $ */ public interface RoleContainerModel { + + interface RoleRemovedEvent extends ProviderEvent { + RoleModel getRole(); + KeycloakSession getKeycloakSession(); + } + String getId(); RoleModel getRole(String name); diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java index 0453977162d..013ff9ca739 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java @@ -17,6 +17,8 @@ package org.keycloak.models; +import org.keycloak.provider.ProviderEvent; + import java.util.List; import java.util.Map; import java.util.Set; @@ -32,6 +34,11 @@ public interface UserModel extends RoleMapperModel { String EMAIL = "email"; String LOCALE = "locale"; + interface UserRemovedEvent extends ProviderEvent { + UserModel getUser(); + KeycloakSession getKeycloakSession(); + } + String getId(); String getUsername(); diff --git a/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactoryProvider.java b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactoryProvider.java new file mode 100644 index 00000000000..3be3b786569 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactoryProvider.java @@ -0,0 +1,27 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.cache.authorization; + +import org.keycloak.authorization.store.StoreFactory; + +/** + * @author Pedro Igor + */ +public interface CachedStoreFactoryProvider extends StoreFactory { +} diff --git a/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactorySpi.java b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactorySpi.java new file mode 100644 index 00000000000..226949d1278 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactorySpi.java @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.cache.authorization; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Pedro Igor + */ +public class CachedStoreFactorySpi implements Spi { + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "authz-fached-store-factory"; + } + + @Override + public Class getProviderClass() { + return CachedStoreFactoryProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return CachedStoreProviderFactory.class; + } +} diff --git a/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreProviderFactory.java b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreProviderFactory.java new file mode 100644 index 00000000000..b8563cb74cf --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreProviderFactory.java @@ -0,0 +1,27 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.models.cache.authorization; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Pedro Igor + */ +public interface CachedStoreProviderFactory extends ProviderFactory { +} diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 24e3ac8b6aa..0bec4629d6e 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -17,6 +17,9 @@ package org.keycloak.models.utils; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.ResourceServerStore; import org.keycloak.hash.Pbkdf2PasswordHashProvider; import org.keycloak.models.ClientTemplateModel; import org.keycloak.models.Constants; @@ -88,6 +91,8 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; +import static java.lang.Boolean.TRUE; + public class RepresentationToModel { private static Logger logger = Logger.getLogger(RepresentationToModel.class); @@ -984,6 +989,21 @@ public class RepresentationToModel { if (resourceRep.isUseTemplateMappers() != null) client.setUseTemplateMappers(resourceRep.isUseTemplateMappers()); else client.setUseTemplateMappers(resourceRep.getClientTemplate() != null); + boolean createResourceServer = TRUE.equals(resourceRep.getAuthorizationServicesEnabled()); + + if (createResourceServer) { + AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class); + ResourceServerStore resourceServerStore = provider.getStoreFactory().getResourceServerStore(); + + client.setServiceAccountsEnabled(true); + client.setBearerOnly(false); + client.setPublicClient(false); + + ResourceServer resourceServer = resourceServerStore.create(client.getId()); + + resourceServer.setAllowRemoteResourceManagement(true); + resourceServer.setPolicyEnforcementMode(ResourceServer.PolicyEnforcementMode.ENFORCING); + } return client; } diff --git a/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProvider.java b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProvider.java new file mode 100644 index 00000000000..e30f5dab9f8 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProvider.java @@ -0,0 +1,38 @@ +/* + * 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.protocol.oidc; + +import org.keycloak.provider.Provider; + +import javax.ws.rs.core.Response; + +/** + * Provides introspection for a determined OAuth2 token type. + * + * @author Pedro Igor + */ +public interface TokenIntrospectionProvider extends Provider { + + /** + * Introspect the token. + * + * @param token the token to introspect. + * @return the response with the information about the token + */ + Response introspect(String token); +} diff --git a/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProviderFactory.java b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProviderFactory.java new file mode 100644 index 00000000000..48b75562571 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProviderFactory.java @@ -0,0 +1,28 @@ +/* + * 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.protocol.oidc; + +import org.keycloak.provider.ProviderFactory; + +/** + * A factory that creates {@link TokenIntrospectionProvider} instances. + * + * @author Pedro Igor + */ +public interface TokenIntrospectionProviderFactory extends ProviderFactory { +} diff --git a/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionSpi.java b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionSpi.java new file mode 100644 index 00000000000..4eb6d398d65 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionSpi.java @@ -0,0 +1,49 @@ +/* + * 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.protocol.oidc; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + *

A {@link Spi} to support additional tokens types to the OAuth2 Token Introspection Endpoint. + * + * @author Pedro Igor + */ +public class TokenIntrospectionSpi implements Spi { + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return "oauth2-token-introspection"; + } + + @Override + public Class getProviderClass() { + return TokenIntrospectionProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return TokenIntrospectionProviderFactory.class; + } +} diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 63015cfd3b8..9a8489874d0 100755 --- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -53,4 +53,9 @@ org.keycloak.authentication.RequiredActionSpi org.keycloak.authentication.FormAuthenticatorSpi org.keycloak.authentication.FormActionSpi org.keycloak.cluster.ClusterSpi +org.keycloak.authorization.policy.provider.PolicySpi +org.keycloak.authorization.store.StoreFactorySpi +org.keycloak.authorization.AuthorizationSpi +org.keycloak.models.cache.authorization.CachedStoreFactorySpi +org.keycloak.protocol.oidc.TokenIntrospectionSpi diff --git a/services/pom.xml b/services/pom.xml index 41ca54d634d..16b19e766c5 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -32,6 +32,8 @@ 1.0.5 + 1.8 + 1.8 diff --git a/services/src/main/java/org/keycloak/authorization/AuthorizationService.java b/services/src/main/java/org/keycloak/authorization/AuthorizationService.java new file mode 100644 index 00000000000..f519b40727e --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/AuthorizationService.java @@ -0,0 +1,65 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.authorization.authorization.AuthorizationTokenService; +import org.keycloak.authorization.entitlement.EntitlementService; +import org.keycloak.authorization.protection.ProtectionService; + +import javax.ws.rs.Path; + +/** + * @author Pedro Igor + */ +public class AuthorizationService { + + private final AuthorizationProvider authorization; + + public AuthorizationService(AuthorizationProvider authorization) { + this.authorization = authorization; + } + + @Path("/entitlement") + public Object getEntitlementService() { + EntitlementService service = new EntitlementService(this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(service); + + return service; + } + + @Path("/protection") + public Object getProtectionService() { + ProtectionService service = new ProtectionService(this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(service); + + return service; + } + + @Path("/authorize") + public Object authorize() { + AuthorizationTokenService resource = new AuthorizationTokenService(this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java new file mode 100644 index 00000000000..5df5a0b08b4 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java @@ -0,0 +1,73 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.keycloak.Config; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider; + +import java.util.concurrent.Executor; + +/** + * @author Pedro Igor + */ +public class DefaultAuthorizationProviderFactory implements AuthorizationProviderFactory { + + private Executor scheduler; + + @Override + public AuthorizationProvider create(KeycloakSession session) { + StoreFactory storeFactory = session.getProvider(CachedStoreFactoryProvider.class); + + if (storeFactory == null) { + storeFactory = session.getProvider(StoreFactory.class); + } + + return new AuthorizationProvider(session, storeFactory); + } + + @Override + public void init(Config.Scope config) { + //TODO: user-defined configuration +// Executor executor = Executors.newWorkStealingPool(); +// this.scheduler = command -> { +// Map, Object> contextDataMap = ResteasyProviderFactory.getContextDataMap(); +// executor.execute(() -> { +// ResteasyProviderFactory.pushContextDataMap(contextDataMap); +// command.run(); +// }); +// }; + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "authorization"; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/ErrorCode.java b/services/src/main/java/org/keycloak/authorization/ErrorCode.java new file mode 100644 index 00000000000..63ac38a6dc9 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/ErrorCode.java @@ -0,0 +1,28 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +/** + * @author Pedro Igor + */ +public interface ErrorCode { + + String INVALID_CLIENT_ID = "invalid_client_id"; + +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java b/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java new file mode 100644 index 00000000000..0521c1f655f --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java @@ -0,0 +1,73 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin; + +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.services.resources.admin.RealmAuth; + +import javax.ws.rs.Path; + +/** + * @author Pedro Igor + */ +public class AuthorizationService { + + private final RealmAuth auth; + private final ClientModel client; + private final KeycloakSession session; + private final ResourceServer resourceServer; + private final AuthorizationProvider authorization; + + public AuthorizationService(KeycloakSession session, ClientModel client, RealmAuth auth) { + this.session = session; + this.client = client; + this.authorization = session.getProvider(AuthorizationProvider.class); + this.resourceServer = this.authorization.getStoreFactory().getResourceServerStore().findByClient(this.client.getId()); + this.auth = auth; + + if (auth != null) { + this.auth.init(RealmAuth.Resource.AUTHORIZATION); + } + } + + @Path("/resource-server") + public ResourceServerService resourceServer() { + ResourceServerService resource = new ResourceServerService(this.authorization, this.resourceServer, this.client, this.auth); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } + + public void enable() { + resourceServer().create(); + } + + public void disable() { + resourceServer().delete(); + } + + public boolean isEnabled() { + return this.resourceServer != null; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java new file mode 100644 index 00000000000..2847d6dc1ac --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java @@ -0,0 +1,205 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.representation.PolicyEvaluationRequest; +import org.keycloak.authorization.admin.representation.PolicyEvaluationResponse; +import org.keycloak.authorization.attribute.Attributes; +import org.keycloak.authorization.common.KeycloakEvaluationContext; +import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.permission.ResourcePermission; +import org.keycloak.authorization.policy.evaluation.DecisionResultCollector; +import org.keycloak.authorization.policy.evaluation.EvaluationContext; +import org.keycloak.authorization.policy.evaluation.Result; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.authorization.util.Permissions; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.AccessToken; +import org.keycloak.services.Urls; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; + +/** + * @author Pedro Igor + */ +public class PolicyEvaluationService { + + private final AuthorizationProvider authorization; + @Context + private HttpRequest httpRequest; + + private final ResourceServer resourceServer; + + PolicyEvaluationService(ResourceServer resourceServer, AuthorizationProvider authorization) { + this.resourceServer = resourceServer; + this.authorization = authorization; + } + + @POST + @Consumes("application/json") + @Produces("application/json") + public void evaluate(PolicyEvaluationRequest evaluationRequest, @Suspended AsyncResponse asyncResponse) { + EvaluationContext evaluationContext = createEvaluationContext(evaluationRequest); + authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization), evaluationContext).evaluate(createDecisionCollector(evaluationRequest, authorization, asyncResponse)); + } + + private DecisionResultCollector createDecisionCollector(PolicyEvaluationRequest evaluationRequest, AuthorizationProvider authorization, AsyncResponse asyncResponse) { + return new DecisionResultCollector() { + @Override + protected void onComplete(List results) { + try { + asyncResponse.resume(Response.ok(PolicyEvaluationResponse.build(evaluationRequest, results, resourceServer, authorization)).build()); + } catch (Throwable cause) { + asyncResponse.resume(cause); + } + } + + @Override + public void onError(Throwable cause) { + asyncResponse.resume(cause); + } + }; + } + + private EvaluationContext createEvaluationContext(PolicyEvaluationRequest representation) { + return new KeycloakEvaluationContext(createIdentity(representation), this.authorization.getKeycloakSession()) { + @Override + public Attributes getAttributes() { + Map> attributes = new HashMap<>(super.getAttributes().toMap()); + Map givenAttributes = representation.getContext().get("attributes"); + + if (givenAttributes != null) { + givenAttributes.forEach((key, entryValue) -> { + if (entryValue != null) { + List values = new ArrayList(); + + for (String value : entryValue.split(",")) { + values.add(value); + } + + attributes.put(key, values); + } + }); + } + + return Attributes.from(attributes); + } + }; + } + + private List createPermissions(PolicyEvaluationRequest representation, EvaluationContext evaluationContext, AuthorizationProvider authorization) { + if (representation.isEntitlements()) { + return Permissions.all(this.resourceServer, evaluationContext.getIdentity(), authorization); + } + + return representation.getResources().stream().flatMap((Function>) resource -> { + Set givenScopes = resource.getScopes(); + + if (givenScopes == null) { + givenScopes = new HashSet(); + } + + StoreFactory storeFactory = authorization.getStoreFactory(); + + List scopes = givenScopes.stream().map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, this.resourceServer.getId())).collect(Collectors.toList()); + + if (resource.getId() != null) { + Resource resourceModel = storeFactory.getResourceStore().findById(resource.getId()); + return Stream.of(new ResourcePermission(resourceModel, scopes, resourceServer)); + } else if (resource.getType() != null) { + return storeFactory.getResourceStore().findByType(resource.getType()).stream().map(resource1 -> new ResourcePermission(resource1, scopes, resourceServer)); + } else { + return scopes.stream().map(scope -> new ResourcePermission(null, asList(scope), resourceServer)); + } + }).collect(Collectors.toList()); + } + + private KeycloakIdentity createIdentity(PolicyEvaluationRequest representation) { + RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm(); + AccessToken accessToken = new AccessToken(); + + accessToken.subject(representation.getUserId()); + accessToken.issuedFor(representation.getClientId()); + accessToken.audience(representation.getClientId()); + accessToken.issuer(Urls.realmIssuer(this.authorization.getKeycloakSession().getContext().getUri().getBaseUri(), realm.getName())); + accessToken.setRealmAccess(new AccessToken.Access()); + + Map claims = accessToken.getOtherClaims(); + Map givenAttributes = representation.getContext().get("attributes"); + + if (givenAttributes != null) { + givenAttributes.forEach((key, value) -> claims.put(key, asList(value))); + } + + String subject = accessToken.getSubject(); + + if (subject != null) { + UserModel userModel = this.authorization.getKeycloakSession().users().getUserById(subject, realm); + + if (userModel != null) { + Set roleMappings = userModel.getRoleMappings(); + + roleMappings.stream().map(RoleModel::getName).forEach(roleName -> accessToken.getRealmAccess().addRole(roleName)); + + String clientId = representation.getClientId(); + + if (clientId != null) { + ClientModel clientModel = realm.getClientById(clientId); + + accessToken.addAccess(clientModel.getClientId()); + + userModel.getClientRoleMappings(clientModel).stream().map(RoleModel::getName).forEach(roleName -> accessToken.getResourceAccess(clientModel.getClientId()).addRole(roleName)); + + //TODO: would be awesome if we could transform the access token using the configured protocol mappers. Tried, but without a clientSession and userSession is tuff. + } + } + } + + if (representation.getRoleIds() != null) { + representation.getRoleIds().forEach(roleName -> accessToken.getRealmAccess().addRole(roleName)); + } + + return new KeycloakIdentity(accessToken, this.authorization.getKeycloakSession()); + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java new file mode 100644 index 00000000000..cffba23d6ab --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java @@ -0,0 +1,356 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.representation.PolicyProviderRepresentation; +import org.keycloak.authorization.admin.representation.PolicyRepresentation; +import org.keycloak.authorization.admin.util.Models; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.policy.provider.PolicyProviderAdminService; +import org.keycloak.authorization.policy.provider.PolicyProviderFactory; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.io.IOException; +import java.util.HashSet; +import java.util.stream.Collectors; + +import static org.keycloak.authorization.admin.util.Models.toRepresentation; + +/** + * @author Pedro Igor + */ +public class PolicyService { + + private final ResourceServer resourceServer; + private final AuthorizationProvider authorization; + + public PolicyService(ResourceServer resourceServer, AuthorizationProvider authorization) { + this.resourceServer = resourceServer; + this.authorization = authorization; + } + + @POST + @Consumes("application/json") + @Produces("application/json") + public Response create(PolicyRepresentation representation) { + Policy policy = Models.toModel(representation, this.resourceServer, authorization); + + updateResources(policy, authorization); + updateAssociatedPolicies(policy); + updateScopes(policy, authorization); + + PolicyProviderAdminService resource = getPolicyProviderAdminResource(policy.getType(), authorization); + + if (resource != null) { + try { + resource.onCreate(policy); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + representation.setId(policy.getId()); + + return Response.status(Status.CREATED).entity(representation).build(); + } + + @Path("{id}") + @PUT + @Consumes("application/json") + @Produces("application/json") + public Response update(@PathParam("id") String id, PolicyRepresentation representation) { + representation.setId(id); + StoreFactory storeFactory = authorization.getStoreFactory(); + Policy policy = storeFactory.getPolicyStore().findById(representation.getId()); + + if (policy == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + policy.setName(representation.getName()); + policy.setDescription(representation.getDescription()); + policy.setConfig(representation.getConfig()); + policy.setDecisionStrategy(representation.getDecisionStrategy()); + policy.setLogic(representation.getLogic()); + + updateResources(policy, authorization); + updateAssociatedPolicies(policy); + updateScopes(policy, authorization); + + PolicyProviderAdminService resource = getPolicyProviderAdminResource(policy.getType(), authorization); + + if (resource != null) { + try { + resource.onUpdate(policy); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + return Response.status(Status.CREATED).build(); + } + + @Path("{id}") + @DELETE + public Response delete(@PathParam("id") String id) { + StoreFactory storeFactory = authorization.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + Policy policy = policyStore.findById(id); + + if (policy == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + PolicyProviderAdminService resource = getPolicyProviderAdminResource(policy.getType(), authorization); + + if (resource != null) { + try { + resource.onRemove(policy); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + policyStore.findDependentPolicies(id).forEach(dependentPolicy -> { + dependentPolicy.removeAssociatedPolicy(policy); + }); + + policyStore.delete(policy.getId()); + + return Response.noContent().build(); + } + + @Path("{id}") + @GET + @Produces("application/json") + public Response findById(@PathParam("id") String id) { + StoreFactory storeFactory = authorization.getStoreFactory(); + Policy model = storeFactory.getPolicyStore().findById(id); + + if (model == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + return Response.ok(toRepresentation(model, authorization)).build(); + } + + @GET + @Produces("application/json") + public Response findAll() { + StoreFactory storeFactory = authorization.getStoreFactory(); + return Response.ok( + storeFactory.getPolicyStore().findByResourceServer(resourceServer.getId()).stream() + .map(policy -> toRepresentation(policy, authorization)) + .collect(Collectors.toList())) + .build(); + } + + @Path("providers") + @GET + @Produces("application/json") + public Response findPolicyProviders() { + return Response.ok( + authorization.getProviderFactories().stream() + .map(provider -> { + PolicyProviderRepresentation representation = new PolicyProviderRepresentation(); + + representation.setName(provider.getName()); + representation.setGroup(provider.getGroup()); + representation.setType(provider.getId()); + + return representation; + }) + .collect(Collectors.toList())) + .build(); + } + + @Path("evaluate") + public PolicyEvaluationService getPolicyEvaluateResource() { + PolicyEvaluationService resource = new PolicyEvaluationService(this.resourceServer, this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } + + @Path("{policyType}") + public Object getPolicyTypeResource(@PathParam("policyType") String policyType) { + return getPolicyProviderAdminResource(policyType, this.authorization); + } + + private PolicyProviderAdminService getPolicyProviderAdminResource(String policyType, AuthorizationProvider authorization) { + PolicyProviderFactory providerFactory = authorization.getProviderFactory(policyType); + + if (providerFactory != null) { + return providerFactory.getAdminResource(this.resourceServer); + } + + return null; + } + + private void updateScopes(Policy policy, AuthorizationProvider authorization) { + String scopes = policy.getConfig().get("scopes"); + if (scopes != null) { + String[] scopeIds; + + try { + scopeIds = new ObjectMapper().readValue(scopes, String[].class); + } catch (IOException e) { + throw new RuntimeException(e); + } + + StoreFactory storeFactory = authorization.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + + for (String scopeId : scopeIds) { + boolean hasScope = false; + + for (Scope scopeModel : new HashSet(policy.getScopes())) { + if (scopeModel.getId().equals(scopeId)) { + hasScope = true; + } + } + if (!hasScope) { + policy.addScope(storeFactory.getScopeStore().findById(scopeId)); + } + } + + for (Scope scopeModel : new HashSet(policy.getScopes())) { + boolean hasScope = false; + + for (String scopeId : scopeIds) { + if (scopeModel.getId().equals(scopeId)) { + hasScope = true; + } + } + if (!hasScope) { + policy.removeScope(scopeModel); + } + } + } + } + + private void updateAssociatedPolicies(Policy policy) { + String policies = policy.getConfig().get("applyPolicies"); + + if (policies != null) { + String[] policyIds; + + try { + policyIds = new ObjectMapper().readValue(policies, String[].class); + } catch (IOException e) { + throw new RuntimeException(e); + } + + StoreFactory storeFactory = authorization.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + + for (String policyId : policyIds) { + boolean hasPolicy = false; + + for (Policy policyModel : new HashSet(policy.getAssociatedPolicies())) { + if (policyModel.getId().equals(policyId)) { + hasPolicy = true; + } + } + + + if (!hasPolicy) { + Policy associatedPolicy = policyStore.findById(policyId); + + if (associatedPolicy == null) { + associatedPolicy = policyStore.findByName(policyId, this.resourceServer.getId()); + } + + policy.addAssociatedPolicy(associatedPolicy); + } + } + + for (Policy policyModel : new HashSet(policy.getAssociatedPolicies())) { + boolean hasPolicy = false; + + for (String policyId : policyIds) { + if (policyModel.getId().equals(policyId) || policyModel.getName().equals(policyId)) { + hasPolicy = true; + } + } + if (!hasPolicy) { + policy.removeAssociatedPolicy(policyModel);; + } + } + } + } + + private void updateResources(Policy policy, AuthorizationProvider authorization) { + String resources = policy.getConfig().get("resources"); + if (resources != null) { + String[] resourceIds; + + try { + resourceIds = new ObjectMapper().readValue(resources, String[].class); + } catch (IOException e) { + throw new RuntimeException(e); + } + + StoreFactory storeFactory = authorization.getStoreFactory(); + + for (String resourceId : resourceIds) { + boolean hasResource = false; + for (Resource resourceModel : new HashSet(policy.getResources())) { + if (resourceModel.getId().equals(resourceId)) { + hasResource = true; + } + } + if (!hasResource && !"".equals(resourceId)) { + policy.addResource(storeFactory.getResourceStore().findById(resourceId)); + } + } + + for (Resource resourceModel : new HashSet(policy.getResources())) { + boolean hasResource = false; + + for (String resourceId : resourceIds) { + if (resourceModel.getId().equals(resourceId)) { + hasResource = true; + } + } + + if (!hasResource) { + policy.removeResource(resourceModel); + } + } + } + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java new file mode 100644 index 00000000000..23a5d32b1d8 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java @@ -0,0 +1,617 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin; + +import org.jboss.resteasy.plugins.providers.multipart.InputPart; +import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.representation.PolicyRepresentation; +import org.keycloak.authorization.admin.representation.ResourceOwnerRepresentation; +import org.keycloak.authorization.admin.representation.ResourceRepresentation; +import org.keycloak.authorization.admin.representation.ResourceServerRepresentation; +import org.keycloak.authorization.admin.representation.ScopeRepresentation; +import org.keycloak.authorization.admin.util.Models; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.resources.admin.RealmAuth; +import org.keycloak.util.JsonSerialization; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class ResourceServerService { + + private final AuthorizationProvider authorization; + private final RealmAuth auth; + private ResourceServer resourceServer; + private final ClientModel client; + + public ResourceServerService(AuthorizationProvider authorization, ResourceServer resourceServer, ClientModel client, RealmAuth auth) { + this.authorization = authorization; + this.client = client; + this.resourceServer = resourceServer; + this.auth = auth; + } + + public void create() { + if (resourceServer == null) { + RoleModel umaProtectionRole = client.getRole(Constants.AUTHZ_UMA_PROTECTION); + + if (umaProtectionRole == null) { + umaProtectionRole = client.addRole(Constants.AUTHZ_UMA_PROTECTION); + } + + KeycloakSession session = this.authorization.getKeycloakSession(); + UserModel serviceAccount = session.users().getUserByServiceAccountClient(client); + + if (!serviceAccount.hasRole(umaProtectionRole)) { + serviceAccount.grantRole(umaProtectionRole); + } + + this.resourceServer = this.authorization.getStoreFactory().getResourceServerStore().create(this.client.getId()); + + ResourceRepresentation defaultResource = new ResourceRepresentation(); + + defaultResource.setName("Default Resource"); + defaultResource.setUri("/*"); + defaultResource.setType("urn:" + this.client.getClientId() + ":resources:default"); + + getResourceSetResource().create(defaultResource); + + PolicyRepresentation defaultPolicy = new PolicyRepresentation(); + + defaultPolicy.setName("Only From Realm Policy"); + defaultPolicy.setDescription("A policy that grants access only for users within this realm"); + defaultPolicy.setType("js"); + defaultPolicy.setDecisionStrategy(Policy.DecisionStrategy.AFFIRMATIVE); + defaultPolicy.setLogic(Policy.Logic.POSITIVE); + + HashMap defaultPolicyConfig = new HashMap<>(); + + defaultPolicyConfig.put("code", "var context = $evaluation.getContext();\n" + + "\n" + + "// using attributes from the evaluation context to obtain the realm\n" + + "var contextAttributes = context.getAttributes();\n" + + "var realmName = contextAttributes.getValue('kc.authz.context.authc.realm').asString(0);\n" + + "\n" + + "// using attributes from the identity to obtain the issuer\n" + + "var identity = context.getIdentity();\n" + + "var identityAttributes = identity.getAttributes();\n" + + "var issuer = identityAttributes.getValue('iss').asString(0);\n" + + "\n" + + "// only users from the realm have access granted \n" + + "if (issuer.endsWith(realmName)) {\n" + + " $evaluation.grant();\n" + + "}"); + + defaultPolicy.setConfig(defaultPolicyConfig); + + getPolicyResource().create(defaultPolicy); + + PolicyRepresentation defaultPermission = new PolicyRepresentation(); + + defaultPermission.setName("Default Permission"); + defaultPermission.setType("resource"); + defaultPermission.setDescription("A permission that applies to the default resource type"); + defaultPermission.setDecisionStrategy(Policy.DecisionStrategy.UNANIMOUS); + defaultPermission.setLogic(Policy.Logic.POSITIVE); + + HashMap defaultPermissionConfig = new HashMap<>(); + + defaultPermissionConfig.put("default", "true"); + defaultPermissionConfig.put("defaultResourceType", defaultResource.getType()); + defaultPermissionConfig.put("applyPolicies", "[\"Only From Realm Policy\"]"); + + defaultPermission.setConfig(defaultPermissionConfig); + + getPolicyResource().create(defaultPermission); + } + } + + @PUT + @Consumes("application/json") + @Produces("application/json") + public Response update(ResourceServerRepresentation server) { + this.auth.requireManage(); + + this.resourceServer.setAllowRemoteResourceManagement(server.isAllowRemoteResourceManagement()); + this.resourceServer.setPolicyEnforcementMode(server.getPolicyEnforcementMode()); + + return Response.noContent().build(); + } + + public void delete() { + if (this.resourceServer != null) { + StoreFactory storeFactory = authorization.getStoreFactory(); + ResourceStore resourceStore = storeFactory.getResourceStore(); + String id = resourceServer.getId(); + + resourceStore.findByResourceServer(id).forEach(resource -> resourceStore.delete(resource.getId())); + + ScopeStore scopeStore = storeFactory.getScopeStore(); + + scopeStore.findByResourceServer(id).forEach(scope -> scopeStore.delete(scope.getId())); + + PolicyStore policyStore = storeFactory.getPolicyStore(); + + policyStore.findByResourceServer(id).forEach(scope -> policyStore.delete(scope.getId())); + + storeFactory.getResourceServerStore().delete(id); + } + } + + @GET + @Produces("application/json") + public Response findById() { + this.auth.requireView(); + return Response.ok(Models.toRepresentation(this.resourceServer, getRealm())).build(); + } + + @Path("/settings") + @GET + @Produces("application/json") + public Response exportSettings() { + this.auth.requireManage(); + StoreFactory storeFactory = authorization.getStoreFactory(); + ResourceServerRepresentation settings = Models.toRepresentation(resourceServer, getRealm()); + + settings.setId(null); + settings.setName(null); + settings.setClientId(null); + + List resources = storeFactory.getResourceStore().findByResourceServer(resourceServer.getId()) + .stream().map(resource -> { + ResourceRepresentation rep = Models.toRepresentation(resource, resourceServer, authorization); + + rep.getOwner().setId(null); + rep.setId(null); + rep.setPolicies(null); + rep.getScopes().forEach(scopeRepresentation -> { + scopeRepresentation.setId(null); + scopeRepresentation.setIconUri(null); + }); + + return rep; + }).collect(Collectors.toList()); + + settings.setResources(resources); + + List policies = new ArrayList<>(); + + policies.addAll(storeFactory.getPolicyStore().findByResourceServer(resourceServer.getId()) + .stream().filter(policy -> !policy.getType().equals("resource") && !policy.getType().equals("scope")) + .map(policy -> createPolicyRepresentation(storeFactory, policy)).collect(Collectors.toList())); + policies.addAll(storeFactory.getPolicyStore().findByResourceServer(resourceServer.getId()) + .stream().filter(policy -> policy.getType().equals("resource") || policy.getType().equals("scope")) + .map(policy -> createPolicyRepresentation(storeFactory, policy)).collect(Collectors.toList())); + + settings.setPolicies(policies); + + List scopes = storeFactory.getScopeStore().findByResourceServer(resourceServer.getId()).stream().map(scope -> { + ScopeRepresentation rep = Models.toRepresentation(scope, authorization); + + rep.setId(null); + + rep.getPolicies().forEach(policyRepresentation -> { + policyRepresentation.setId(null); + policyRepresentation.setConfig(null); + policyRepresentation.setType(null); + policyRepresentation.setDecisionStrategy(null); + policyRepresentation.setDescription(null); + policyRepresentation.setDependentPolicies(null); + }); + + return rep; + }).collect(Collectors.toList()); + + settings.setScopes(scopes); + + return Response.ok(settings).build(); + } + + private PolicyRepresentation createPolicyRepresentation(StoreFactory storeFactory, Policy policy) { + PolicyRepresentation rep = Models.toRepresentation(policy, authorization); + + rep.setId(null); + rep.setDependentPolicies(null); + + Map config = rep.getConfig(); + + String roles = config.get("roles"); + + if (roles != null && !roles.isEmpty()) { + roles = roles.replace("[", ""); + roles = roles.replace("]", ""); + + if (!roles.isEmpty()) { + String roleNames = ""; + + for (String role : roles.split(",")) { + if (!roleNames.isEmpty()) { + roleNames = roleNames + ","; + } + + role = role.replace("\"", ""); + + roleNames = roleNames + "\"" + getRealm().getRoleById(role).getName() + "\""; + } + + config.put("roles", "[" + roleNames + "]"); + } + } + + String users = config.get("users"); + + if (users != null) { + users = users.replace("[", ""); + users = users.replace("]", ""); + + if (!users.isEmpty()) { + String userNames = ""; + + for (String user : users.split(",")) { + if (!userNames.isEmpty()) { + userNames = userNames + ","; + } + + user = user.replace("\"", ""); + + userNames = userNames + "\"" + this.authorization.getKeycloakSession().users().getUserById(user, getRealm()).getUsername() + "\""; + } + + config.put("users", "[" + userNames + "]"); + } + } + + String scopes = config.get("scopes"); + + if (scopes != null && !scopes.isEmpty()) { + scopes = scopes.replace("[", ""); + scopes = scopes.replace("]", ""); + + if (!scopes.isEmpty()) { + String scopeNames = ""; + + for (String scope : scopes.split(",")) { + if (!scopeNames.isEmpty()) { + scopeNames = scopeNames + ","; + } + + scope = scope.replace("\"", ""); + + scopeNames = scopeNames + "\"" + storeFactory.getScopeStore().findById(scope).getName() + "\""; + } + + config.put("scopes", "[" + scopeNames + "]"); + } + } + + String policyResources = config.get("resources"); + + if (policyResources != null && !policyResources.isEmpty()) { + policyResources = policyResources.replace("[", ""); + policyResources = policyResources.replace("]", ""); + + if (!policyResources.isEmpty()) { + String resourceNames = ""; + + for (String resource : policyResources.split(",")) { + if (!resourceNames.isEmpty()) { + resourceNames = resourceNames + ","; + } + + resource = resource.replace("\"", ""); + + resourceNames = resourceNames + "\"" + storeFactory.getResourceStore().findById(resource).getName() + "\""; + } + + config.put("resources", "[" + resourceNames + "]"); + } + } + + String policyNames = ""; + Set associatedPolicies = policy.getAssociatedPolicies(); + + if (!associatedPolicies.isEmpty()) { + for (Policy associatedPolicy : associatedPolicies) { + if (!policyNames.isEmpty()) { + policyNames = policyNames + ","; + } + + policyNames = policyNames + "\"" + associatedPolicy.getName() + "\""; + } + + config.put("applyPolicies", "[" + policyNames + "]"); + } + + return rep; + } + + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + public Response importSettings(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException { + Map> uploadForm = input.getFormDataMap(); + List inputParts = uploadForm.get("file"); + + for (InputPart inputPart : inputParts) { + ResourceServerRepresentation rep = JsonSerialization.readValue(inputPart.getBodyAsString(), ResourceServerRepresentation.class); + + resourceServer.setPolicyEnforcementMode(rep.getPolicyEnforcementMode()); + resourceServer.setAllowRemoteResourceManagement(rep.isAllowRemoteResourceManagement()); + + StoreFactory storeFactory = authorization.getStoreFactory(); + ResourceStore resourceStore = storeFactory.getResourceStore(); + ScopeStore scopeStore = storeFactory.getScopeStore(); + ScopeService scopeResource = new ScopeService(resourceServer, this.authorization, this.auth); + + ResteasyProviderFactory.getInstance().injectProperties(scopeResource); + + rep.getScopes().forEach(scope -> { + Scope existing = scopeStore.findByName(scope.getName(), resourceServer.getId()); + + if (existing != null) { + scopeResource.update(existing.getId(), scope); + } else { + scopeResource.create(scope); + } + }); + + ResourceSetService resourceSetResource = new ResourceSetService(resourceServer, this.authorization, this.auth); + + rep.getResources().forEach(resourceRepresentation -> { + ResourceOwnerRepresentation owner = resourceRepresentation.getOwner(); + + if (owner == null) { + owner = new ResourceOwnerRepresentation(); + } + + owner.setId(resourceServer.getClientId()); + + if (owner.getName() != null) { + UserModel user = this.authorization.getKeycloakSession().users().getUserByUsername(owner.getName(), getRealm()); + + if (user != null) { + owner.setId(user.getId()); + } + } + + Resource existing = resourceStore.findByName(resourceRepresentation.getName(), this.resourceServer.getId()); + + if (existing != null) { + resourceSetResource.update(existing.getId(), resourceRepresentation); + } else { + resourceSetResource.create(resourceRepresentation); + } + }); + + PolicyStore policyStore = storeFactory.getPolicyStore(); + PolicyService policyResource = new PolicyService(resourceServer, this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(policyResource); + + rep.getPolicies().forEach(policyRepresentation -> { + Map config = policyRepresentation.getConfig(); + + String roles = config.get("roles"); + + if (roles != null && !roles.isEmpty()) { + roles = roles.replace("[", ""); + roles = roles.replace("]", ""); + + if (!roles.isEmpty()) { + String roleNames = ""; + + for (String role : roles.split(",")) { + if (!roleNames.isEmpty()) { + roleNames = roleNames + ","; + } + + role = role.replace("\"", ""); + + roleNames = roleNames + "\"" + getRealm().getRole(role).getId() + "\""; + } + + config.put("roles", "[" + roleNames + "]"); + } + } + + String users = config.get("users"); + + if (users != null) { + users = users.replace("[", ""); + users = users.replace("]", ""); + + if (!users.isEmpty()) { + String userNames = ""; + + for (String user : users.split(",")) { + if (!userNames.isEmpty()) { + userNames = userNames + ","; + } + + user = user.replace("\"", ""); + + userNames = userNames + "\"" + this.authorization.getKeycloakSession().users().getUserByUsername(user, getRealm()).getId() + "\""; + } + + config.put("users", "[" + userNames + "]"); + } + } + + String scopes = config.get("scopes"); + + if (scopes != null && !scopes.isEmpty()) { + scopes = scopes.replace("[", ""); + scopes = scopes.replace("]", ""); + + if (!scopes.isEmpty()) { + String scopeNames = ""; + + for (String scope : scopes.split(",")) { + if (!scopeNames.isEmpty()) { + scopeNames = scopeNames + ","; + } + + scope = scope.replace("\"", ""); + + Scope newScope = scopeStore.findByName(scope, resourceServer.getId()); + + if (newScope == null) { + throw new RuntimeException("Scope with name [" + scope + "] not defined."); + } + + scopeNames = scopeNames + "\"" + newScope.getId() + "\""; + } + + config.put("scopes", "[" + scopeNames + "]"); + } + } + + String policyResources = config.get("resources"); + + if (policyResources != null && !policyResources.isEmpty()) { + policyResources = policyResources.replace("[", ""); + policyResources = policyResources.replace("]", ""); + + if (!policyResources.isEmpty()) { + String resourceNames = ""; + + for (String resource : policyResources.split(",")) { + if (!resourceNames.isEmpty()) { + resourceNames = resourceNames + ","; + } + + resource = resource.replace("\"", ""); + + if ("".equals(resource)) { + continue; + } + + resourceNames = resourceNames + "\"" + storeFactory.getResourceStore().findByName(resource, resourceServer.getId()).getId() + "\""; + } + + config.put("resources", "[" + resourceNames + "]"); + } + } + + String applyPolicies = config.get("applyPolicies"); + + if (applyPolicies != null && !applyPolicies.isEmpty()) { + applyPolicies = applyPolicies.replace("[", ""); + applyPolicies = applyPolicies.replace("]", ""); + + if (!applyPolicies.isEmpty()) { + String policyNames = ""; + + for (String pId : applyPolicies.split(",")) { + if (!policyNames.isEmpty()) { + policyNames = policyNames + ","; + } + + pId = pId.replace("\"", "").trim(); + + Policy policy = policyStore.findByName(pId, resourceServer.getId()); + + if (policy == null) { + throw new RuntimeException("Policy with name [" + pId + "] not defined."); + } + + policyNames = policyNames + "\"" + policy.getId() + "\""; + } + + config.put("applyPolicies", "[" + policyNames + "]"); + } + } + + Policy existing = policyStore.findByName(policyRepresentation.getName(), this.resourceServer.getId()); + + if (existing != null) { + policyResource.update(existing.getId(), policyRepresentation); + } else { + policyResource.create(policyRepresentation); + } + }); + } + + return Response.noContent().build(); + } + + @Path("/resource") + public ResourceSetService getResourceSetResource() { + ResourceSetService resource = new ResourceSetService(this.resourceServer, this.authorization, this.auth); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } + + @Path("/scope") + public ScopeService getScopeResource() { + ScopeService resource = new ScopeService(this.resourceServer, this.authorization, this.auth); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } + + @Path("/policy") + public PolicyService getPolicyResource() { + StoreFactory storeFactory = authorization.getStoreFactory(); + PolicyService resource = new PolicyService(this.resourceServer, this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } + + private RealmModel getRealm() { + return this.authorization.getKeycloakSession().getContext().getRealm(); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java new file mode 100644 index 00000000000..91774799d65 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java @@ -0,0 +1,176 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.representation.ResourceRepresentation; +import org.keycloak.authorization.admin.representation.ScopeRepresentation; +import org.keycloak.authorization.admin.util.Models; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.admin.RealmAuth; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class ResourceSetService { + + private final AuthorizationProvider authorization; + private final RealmAuth auth; + private ResourceServer resourceServer; + + public ResourceSetService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) { + this.resourceServer = resourceServer; + this.authorization = authorization; + this.auth = auth; + } + + @POST + @Consumes("application/json") + @Produces("application/json") + public Response create(ResourceRepresentation resource) { + requireManage(); + StoreFactory storeFactory = this.authorization.getStoreFactory(); + Resource existingResource = storeFactory.getResourceStore().findByName(resource.getName(), this.resourceServer.getId()); + + if (existingResource != null && existingResource.getResourceServer().getId().equals(this.resourceServer.getId()) + && existingResource.getOwner().equals(resource.getOwner())) { + return ErrorResponse.exists("Resource with name [" + resource.getName() + "] already exists."); + } + + Resource model = Models.toModel(resource, this.resourceServer, authorization); + + ResourceRepresentation representation = new ResourceRepresentation(); + + representation.setId(model.getId()); + + return Response.status(Status.CREATED).entity(representation).build(); + } + + @Path("{id}") + @PUT + @Consumes("application/json") + @Produces("application/json") + public Response update(@PathParam("id") String id, ResourceRepresentation resource) { + requireManage(); + resource.setId(id); + StoreFactory storeFactory = this.authorization.getStoreFactory(); + ResourceStore resourceStore = storeFactory.getResourceStore(); + Resource model = resourceStore.findById(resource.getId()); + + if (model == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + model.setName(resource.getName()); + model.setType(resource.getType()); + model.setUri(resource.getUri()); + model.setIconUri(resource.getIconUri()); + + model.updateScopes(resource.getScopes().stream() + .map((ScopeRepresentation scope) -> Models.toModel(scope, this.resourceServer, authorization)) + .collect(Collectors.toSet())); + + return Response.noContent().build(); + } + + @Path("{id}") + @DELETE + public Response delete(@PathParam("id") String id) { + requireManage(); + StoreFactory storeFactory = authorization.getStoreFactory(); + Resource resource = storeFactory.getResourceStore().findById(id); + + if (resource == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + PolicyStore policyStore = storeFactory.getPolicyStore(); + List policies = policyStore.findByResource(id); + + for (Policy policyModel : policies) { + if (policyModel.getResources().size() == 1) { + policyStore.delete(policyModel.getId()); + } else { + policyModel.addResource(resource); + } + } + + storeFactory.getResourceStore().delete(id); + + return Response.noContent().build(); + } + + @Path("{id}") + @GET + @Produces("application/json") + public Response findById(@PathParam("id") String id) { + requireView(); + StoreFactory storeFactory = authorization.getStoreFactory(); + Resource model = storeFactory.getResourceStore().findById(id); + + if (model == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + return Response.ok(Models.toRepresentation(model, this.resourceServer, authorization)).build(); + } + + @GET + @Produces("application/json") + public Response findAll() { + requireView(); + StoreFactory storeFactory = authorization.getStoreFactory(); + + return Response.ok( + storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream() + .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization)) + .collect(Collectors.toList())) + .build(); + } + + private void requireManage() { + if (this.auth != null) { + this.auth.requireManage(); + } + } + + private void requireView() { + if (this.auth != null) { + this.auth.requireView(); + } + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java new file mode 100644 index 00000000000..56291c801c7 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java @@ -0,0 +1,147 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.representation.ScopeRepresentation; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.services.ErrorResponse; +import org.keycloak.services.resources.admin.RealmAuth; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.keycloak.authorization.admin.util.Models.toModel; +import static org.keycloak.authorization.admin.util.Models.toRepresentation; + +/** + * @author Pedro Igor + */ +public class ScopeService { + + private final AuthorizationProvider authorization; + private final RealmAuth auth; + private ResourceServer resourceServer; + + public ScopeService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) { + this.resourceServer = resourceServer; + this.authorization = authorization; + this.auth = auth; + } + + @POST + @Consumes("application/json") + @Produces("application/json") + public Response create(ScopeRepresentation scope) { + this.auth.requireManage(); + Scope model = toModel(scope, this.resourceServer, authorization); + + scope.setId(model.getId()); + + return Response.status(Status.CREATED).entity(scope).build(); + } + + @Path("{id}") + @PUT + @Consumes("application/json") + @Produces("application/json") + public Response update(@PathParam("id") String id, ScopeRepresentation scope) { + this.auth.requireManage(); + scope.setId(id); + StoreFactory storeFactory = authorization.getStoreFactory(); + Scope model = storeFactory.getScopeStore().findById(scope.getId()); + + if (model == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + model.setName(scope.getName()); + model.setIconUri(scope.getIconUri()); + + return Response.noContent().build(); + } + + @Path("{id}") + @DELETE + public Response delete(@PathParam("id") String id) { + this.auth.requireManage(); + StoreFactory storeFactory = authorization.getStoreFactory(); + List resources = storeFactory.getResourceStore().findByScope(id); + + if (!resources.isEmpty()) { + return ErrorResponse.exists("Scopes can not be removed while associated with resources."); + } + + Scope scope = storeFactory.getScopeStore().findById(id); + PolicyStore policyStore = storeFactory.getPolicyStore(); + List policies = policyStore.findByScopeIds(Arrays.asList(scope.getId()), resourceServer.getId()); + + for (Policy policyModel : policies) { + if (policyModel.getScopes().size() == 1) { + policyStore.delete(policyModel.getId()); + } else { + policyModel.removeScope(scope); + } + } + + storeFactory.getScopeStore().delete(id); + + return Response.noContent().build(); + } + + @Path("{id}") + @GET + @Produces("application/json") + public Response findById(@PathParam("id") String id) { + this.auth.requireView(); + Scope model = this.authorization.getStoreFactory().getScopeStore().findById(id); + + if (model == null) { + return Response.status(Status.NOT_FOUND).build(); + } + + return Response.ok(toRepresentation(model, this.authorization)).build(); + } + + @GET + @Produces("application/json") + public Response findAll() { + this.auth.requireView(); + return Response.ok( + this.authorization.getStoreFactory().getScopeStore().findByResourceServer(this.resourceServer.getId()).stream() + .map(scope -> toRepresentation(scope, this.authorization)) + .collect(Collectors.toList())) + .build(); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java new file mode 100644 index 00000000000..17edef970fc --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java @@ -0,0 +1,123 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin.representation; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * @author Pedro Igor + */ +public class PolicyEvaluationRequest { + + private Map> context; + private List resources; + private String clientId; + private String userId; + private List roleIds; + private boolean entitlements; + + public Map> getContext() { + return this.context; + } + + public void setContext(Map> context) { + this.context = context; + } + + public List getResources() { + return this.resources; + } + + public void setResources(List resources) { + this.resources = resources; + } + + public String getClientId() { + return this.clientId; + } + + public void setClientId(final String clientId) { + this.clientId = clientId; + } + + public String getUserId() { + return this.userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public List getRoleIds() { + return this.roleIds; + } + + public void setRoleIds(List roleIds) { + this.roleIds = roleIds; + } + + public boolean isEntitlements() { + return entitlements; + } + + public void setEntitlements(boolean entitlements) { + this.entitlements = entitlements; + } + + public static class Resource { + private String id; + private String name; + private String type; + private Set scopes; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(final String type) { + this.type = type; + } + + public Set getScopes() { + return scopes; + } + + public void setScopes(final Set scopes) { + this.scopes = scopes; + } + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java new file mode 100644 index 00000000000..57b3e4eed34 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java @@ -0,0 +1,223 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin.representation; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.Decision.Effect; +import org.keycloak.authorization.admin.util.Models; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.policy.evaluation.Result; +import org.keycloak.authorization.policy.evaluation.Result.PolicyResult; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.authorization.util.Permissions; +import org.keycloak.representations.authorization.Permission; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class PolicyEvaluationResponse { + + private List results; + private boolean entitlements; + private Effect status; + + private PolicyEvaluationResponse() { + + } + + public static PolicyEvaluationResponse build(PolicyEvaluationRequest evaluationRequest, List results, ResourceServer resourceServer, AuthorizationProvider authorization) { + PolicyEvaluationResponse response = new PolicyEvaluationResponse(); + List resultsRep = new ArrayList<>(); + + response.entitlements = evaluationRequest.isEntitlements(); + + if (response.entitlements) { + List entitlements = Permissions.allPermits(results); + + if (entitlements.isEmpty()) { + response.status = Effect.DENY; + } else { + StoreFactory storeFactory = authorization.getStoreFactory(); + + for (Permission permission : entitlements) { + EvaluationResultRepresentation rep = new EvaluationResultRepresentation(); + + rep.setStatus(Effect.PERMIT); + resultsRep.add(rep); + + Resource resource = storeFactory.getResourceStore().findById(permission.getResourceSetId()); + + if (resource != null) { + rep.setResource(Models.toRepresentation(resource, resourceServer, authorization)); + } else { + ResourceRepresentation representation = new ResourceRepresentation(); + + representation.setName("Any Resource with Scopes " + permission.getScopes()); + + rep.setResource(representation); + } + + rep.setScopes(permission.getScopes().stream().map(ScopeRepresentation::new).collect(Collectors.toList())); + } + } + } else { + if (results.stream().anyMatch(evaluationResult -> evaluationResult.getEffect().equals(Effect.DENY))) { + response.status = Effect.DENY; + } else { + response.status = Effect.PERMIT; + } + + for (Result result : results) { + EvaluationResultRepresentation rep = new EvaluationResultRepresentation(); + + rep.setStatus(result.getEffect()); + resultsRep.add(rep); + + if (result.getPermission().getResource() != null) { + rep.setResource(Models.toRepresentation(result.getPermission().getResource(), resourceServer, authorization)); + } else { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName("Any Resource with Scopes " + result.getPermission().getScopes()); + + rep.setResource(resource); + } + + rep.setScopes(result.getPermission().getScopes().stream().map(new Function() { + @Override + public ScopeRepresentation apply(Scope scope) { + return Models.toRepresentation(scope, authorization); + } + }).collect(Collectors.toList())); + + List policies = new ArrayList<>(); + + for (PolicyResult policy : result.getResults()) { + policies.add(toRepresentation(policy, authorization)); + } + + rep.setPolicies(policies); + } + } + + response.results = resultsRep; + + return response; + } + + private static PolicyResultRepresentation toRepresentation(PolicyResult policy, AuthorizationProvider authorization) { + PolicyResultRepresentation policyResultRep = new PolicyResultRepresentation(); + + policyResultRep.setPolicy(Models.toRepresentation(policy.getPolicy(), authorization)); + policyResultRep.setStatus(policy.getStatus()); + policyResultRep.setAssociatedPolicies(policy.getAssociatedPolicies().stream().map(result -> toRepresentation(result, authorization)).collect(Collectors.toList())); + + return policyResultRep; + } + + public List getResults() { + return results; + } + + public Effect getStatus() { + return status; + } + + public boolean isEntitlements() { + return entitlements; + } + + public static class EvaluationResultRepresentation { + + private ResourceRepresentation resource; + private List scopes; + private List policies; + private Effect status; + + public void setResource(final ResourceRepresentation resource) { + this.resource = resource; + } + + public ResourceRepresentation getResource() { + return resource; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public List getScopes() { + return scopes; + } + + public void setPolicies(final List policies) { + this.policies = policies; + } + + public List getPolicies() { + return policies; + } + + public void setStatus(final Effect status) { + this.status = status; + } + + public Effect getStatus() { + return status; + } + } + + public static class PolicyResultRepresentation { + + private PolicyRepresentation policy; + private Effect status; + private List associatedPolicies; + + public PolicyRepresentation getPolicy() { + return policy; + } + + public void setPolicy(final PolicyRepresentation policy) { + this.policy = policy; + } + + public Effect getStatus() { + return status; + } + + public void setStatus(final Effect status) { + this.status = status; + } + + public List getAssociatedPolicies() { + return associatedPolicies; + } + + public void setAssociatedPolicies(final List associatedPolicies) { + this.associatedPolicies = associatedPolicies; + } + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyProviderRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyProviderRepresentation.java new file mode 100644 index 00000000000..add09b0c353 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyProviderRepresentation.java @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin.representation; + +/** + * @author Pedro Igor + */ +public class PolicyProviderRepresentation { + + private String type; + private String name; + private String group; + + public String getType() { + return this.type; + } + + public void setType( String type) { + this.type = type; + } + + public String getName() { + return this.name; + } + + public void setName( String name) { + this.name = name; + } + + public String getGroup() { + return this.group; + } + + public void setGroup( String group) { + this.group = group; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyRepresentation.java new file mode 100644 index 00000000000..a3c302b255e --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyRepresentation.java @@ -0,0 +1,119 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin.representation; + +import org.keycloak.authorization.model.Policy.DecisionStrategy; +import org.keycloak.authorization.model.Policy.Logic; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * @author Pedro Igor + */ +public class PolicyRepresentation { + + private String id; + private String name; + private String description; + private String type; + private Logic logic = Logic.POSITIVE; + private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; + private Map config = new HashMap(); + private List dependentPolicies; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + public DecisionStrategy getDecisionStrategy() { + return this.decisionStrategy; + } + + public void setDecisionStrategy(DecisionStrategy decisionStrategy) { + this.decisionStrategy = decisionStrategy; + } + + public Logic getLogic() { + return logic; + } + + public void setLogic(Logic logic) { + this.logic = logic; + } + + public Map getConfig() { + return this.config; + } + + public void setConfig(Map config) { + this.config = config; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final PolicyRepresentation policy = (PolicyRepresentation) o; + return Objects.equals(getId(), policy.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(getId()); + } + + public void setDependentPolicies(List dependentPolicies) { + this.dependentPolicies = dependentPolicies; + } + + public List getDependentPolicies() { + return this.dependentPolicies; + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceOwnerRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceOwnerRepresentation.java new file mode 100644 index 00000000000..498ab9f02d9 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceOwnerRepresentation.java @@ -0,0 +1,44 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin.representation; + +/** + * @author Pedro Igor + */ +public class ResourceOwnerRepresentation { + + private String id; + private String name; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceRepresentation.java new file mode 100644 index 00000000000..b56248b020b --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceRepresentation.java @@ -0,0 +1,170 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin.representation; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +/** + *

One or more resources that the resource server manages as a set of protected resources. + * + *

For more details, OAuth-resource-reg. + * + * @author Pedro Igor + */ +public class ResourceRepresentation { + + @JsonProperty("_id") + private String id; + + private String name; + private String uri; + private String type; + private Set scopes; + + @JsonProperty("icon_uri") + private String iconUri; + private ResourceOwnerRepresentation owner; + + private List policies; + + /** + * Creates a new instance. + * + * @param name a human-readable string describing a set of one or more resources + * @param uri a {@link URI} that provides the network location for the resource set being registered + * @param type a string uniquely identifying the semantics of the resource set + * @param scopes the available scopes for this resource set + * @param iconUri a {@link URI} for a graphic icon representing the resource set + */ + public ResourceRepresentation(String name, Set scopes, String uri, String type, String iconUri) { + this.name = name; + this.scopes = scopes; + this.uri = uri; + this.type = type; + this.iconUri = iconUri; + } + + /** + * Creates a new instance. + * + * @param name a human-readable string describing a set of one or more resources + * @param uri a {@link URI} that provides the network location for the resource set being registered + * @param type a string uniquely identifying the semantics of the resource set + * @param scopes the available scopes for this resource set + */ + public ResourceRepresentation(String name, Set scopes, String uri, String type) { + this(name, scopes, uri, type, null); + } + + /** + * Creates a new instance. + * + * @param name a human-readable string describing a set of one or more resources + * @param serverUri a {@link URI} that identifies this resource server + * @param scopes the available scopes for this resource set + */ + public ResourceRepresentation(String name, Set scopes) { + this(name, scopes, null, null, null); + } + + /** + * Creates a new instance. + * + */ + public ResourceRepresentation() { + this(null, null, null, null, null); + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public String getUri() { + return this.uri; + } + + public String getType() { + return this.type; + } + + public Set getScopes() { + if (this.scopes == null) { + return Collections.emptySet(); + } + + return Collections.unmodifiableSet(this.scopes); + } + + public String getIconUri() { + return this.iconUri; + } + + public void setName(String name) { + this.name = name; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public void setType(String type) { + this.type = type; + } + + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + public ResourceOwnerRepresentation getOwner() { + return this.owner; + } + + public void setOwner(ResourceOwnerRepresentation owner) { + this.owner = owner; + } + + public List getPolicies() { + return this.policies; + } + + public void setPolicies(List policies) { + this.policies = policies; + } + + T test(Predicate t) { + return null; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceServerRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceServerRepresentation.java new file mode 100644 index 00000000000..4549ef57c3b --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceServerRepresentation.java @@ -0,0 +1,104 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin.representation; + +import org.keycloak.authorization.model.ResourceServer.PolicyEnforcementMode; + +import java.util.List; + +import static java.util.Collections.emptyList; + +/** + * @author Pedro Igor + */ +public class ResourceServerRepresentation { + + private String id; + + private String clientId; + private String name; + private boolean allowRemoteResourceManagement = true; + private PolicyEnforcementMode policyEnforcementMode = PolicyEnforcementMode.ENFORCING; + private List resources = emptyList(); + private List policies = emptyList(); + private List scopes = emptyList(); + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isAllowRemoteResourceManagement() { + return this.allowRemoteResourceManagement; + } + + public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) { + this.allowRemoteResourceManagement = allowRemoteResourceManagement; + } + + public PolicyEnforcementMode getPolicyEnforcementMode() { + return this.policyEnforcementMode; + } + + public void setPolicyEnforcementMode(PolicyEnforcementMode policyEnforcementMode) { + this.policyEnforcementMode = policyEnforcementMode; + } + + public void setResources(List resources) { + this.resources = resources; + } + + public List getResources() { + return resources; + } + + public void setPolicies(List policies) { + this.policies = policies; + } + + public List getPolicies() { + return policies; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public List getScopes() { + return scopes; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/ScopeRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/ScopeRepresentation.java new file mode 100644 index 00000000000..74efa7a1d71 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/ScopeRepresentation.java @@ -0,0 +1,108 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin.representation; + +import java.net.URI; +import java.util.List; +import java.util.Objects; + +/** + *

A bounded extent of access that is possible to perform on a resource set. In authorization policy terminology, + * a scope is one of the potentially many "verbs" that can logically apply to a resource set ("object"). + * + *

For more details, OAuth-resource-reg. + * + * @author Pedro Igor + */ +public class ScopeRepresentation { + + private String id; + private String name; + private String iconUri; + private List policies; + + /** + * Creates an instance. + * + * @param name the a human-readable string describing some scope (extent) of access + * @param iconUri a {@link URI} for a graphic icon representing the scope + */ + public ScopeRepresentation(String name, String iconUri) { + this.name = name; + this.iconUri = iconUri; + } + + /** + * Creates an instance. + * + * @param name the a human-readable string describing some scope (extent) of access + */ + public ScopeRepresentation(String name) { + this(name, null); + } + + /** + * Creates an instance. + */ + public ScopeRepresentation() { + this(null, null); + } + + public String getName() { + return this.name; + } + + public String getIconUri() { + return this.iconUri; + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + public List getPolicies() { + return this.policies; + } + + public void setPolicies(List policies) { + this.policies = policies; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ScopeRepresentation scope = (ScopeRepresentation) o; + return Objects.equals(getName(), scope.getName()); + } + + public int hashCode() { + return Objects.hash(getName()); + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/admin/util/Models.java b/services/src/main/java/org/keycloak/authorization/admin/util/Models.java new file mode 100644 index 00000000000..abdd980be46 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/admin/util/Models.java @@ -0,0 +1,285 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.admin.util; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.ErrorCode; +import org.keycloak.authorization.admin.representation.PolicyRepresentation; +import org.keycloak.authorization.admin.representation.ResourceOwnerRepresentation; +import org.keycloak.authorization.admin.representation.ResourceRepresentation; +import org.keycloak.authorization.admin.representation.ResourceServerRepresentation; +import org.keycloak.authorization.admin.representation.ScopeRepresentation; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.util.JsonSerialization; + +import javax.ws.rs.core.Response.Status; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Some utility methods to transform models to representations and vice-versa. + * + * @author Pedro Igor + */ +public final class Models { + + public static ScopeRepresentation toRepresentation(Scope model, AuthorizationProvider authorizationProvider) { + ScopeRepresentation scope = new ScopeRepresentation(); + + scope.setId(model.getId()); + scope.setName(model.getName()); + scope.setIconUri(model.getIconUri()); + scope.setPolicies(new ArrayList<>()); + + Set policies = new HashSet<>(); + + policies.addAll(authorizationProvider.getStoreFactory().getPolicyStore().findByScopeIds(Arrays.asList(model.getId()), model.getResourceServer().getId())); + + for (Policy policyModel : policies) { + PolicyRepresentation policy = new PolicyRepresentation(); + + policy.setId(policyModel.getId()); + policy.setName(policyModel.getName()); + policy.setType(policyModel.getType()); + + if (!scope.getPolicies().contains(policy)) { + scope.getPolicies().add(policy); + } + } + + return scope; + } + + public static Scope toModel(ScopeRepresentation scope, ResourceServer resourceServer, AuthorizationProvider authorization) { + StoreFactory storeFactory = authorization.getStoreFactory(); + Scope model = storeFactory.getScopeStore().findByName(scope.getName(), resourceServer.getId()); + + if (model == null) { + model = storeFactory.getScopeStore().create(scope.getName(), resourceServer); + + model.setIconUri(scope.getIconUri()); + } + + return model; + } + + public static ResourceServerRepresentation toRepresentation(ResourceServer model, RealmModel realm) { + ResourceServerRepresentation server = new ResourceServerRepresentation(); + + server.setId(model.getId()); + server.setClientId(model.getClientId()); + ClientModel clientById = realm.getClientById(model.getClientId()); + server.setName(clientById.getClientId()); + server.setAllowRemoteResourceManagement(model.isAllowRemoteResourceManagement()); + server.setPolicyEnforcementMode(model.getPolicyEnforcementMode()); + + return server; + } + + public static ResourceServer toModel(ResourceServerRepresentation server, AuthorizationProvider authorization) { + RealmModel realm = authorization.getKeycloakSession().getContext().getRealm(); + ClientModel client = realm.getClientById(server.getClientId()); + + if (client == null) { + throw new ErrorResponseException(ErrorCode.INVALID_CLIENT_ID, "Client with id [" + server.getClientId() + "] not found in realm [" + realm.getName() + "].", Status.BAD_REQUEST); + } + + if (!client.isServiceAccountsEnabled()) { + throw new ErrorResponseException(ErrorCode.INVALID_CLIENT_ID, "Client with id [" + server.getClientId() + "] must have a service account.", Status.BAD_REQUEST); + } + + ResourceServer existingResourceServer = authorization.getStoreFactory().getResourceServerStore().findByClient(client.getId()); + + if (existingResourceServer != null) { + throw new ErrorResponseException(ErrorCode.INVALID_CLIENT_ID, "Resource server already exists with client id [" + server.getClientId() + "].", Status.BAD_REQUEST); + } + + if (server.getName() == null) { + server.setName(client.getName()); + } + + ResourceServer model = authorization.getStoreFactory().getResourceServerStore().create(client.getId()); + + model.setAllowRemoteResourceManagement(server.isAllowRemoteResourceManagement()); + model.setPolicyEnforcementMode(server.getPolicyEnforcementMode()); + + return model; + } + + public static PolicyRepresentation toRepresentation(Policy model, AuthorizationProvider authorization) { + PolicyRepresentation representation = new PolicyRepresentation(); + + representation.setId(model.getId()); + representation.setName(model.getName()); + representation.setDescription(model.getDescription()); + representation.setType(model.getType()); + representation.setDecisionStrategy(model.getDecisionStrategy()); + representation.setLogic(model.getLogic()); + representation.setConfig(new HashMap<>(model.getConfig())); + + List policies = authorization.getStoreFactory().getPolicyStore().findDependentPolicies(model.getId()); + + representation.setDependentPolicies(policies.stream().map(policy -> { + PolicyRepresentation representation1 = new PolicyRepresentation(); + + representation1.setId(policy.getId()); + representation1.setName(policy.getName()); + + return representation1; + }).collect(Collectors.toList())); + + List obj = model.getAssociatedPolicies().stream().map(new Function() { + @Override + public String apply(Policy policy) { + return policy.getId(); + } + }).collect(Collectors.toList()); + + try { + representation.getConfig().put("applyPolicies", JsonSerialization.writeValueAsString(obj)); + } catch (IOException e) { + e.printStackTrace(); + } + + return representation; + } + + public static Policy toModel(PolicyRepresentation policy, ResourceServer resourceServer, AuthorizationProvider authorizationManager) { + Policy model = authorizationManager.getStoreFactory().getPolicyStore().create(policy.getName(), policy.getType(), resourceServer); + + model.setDescription(policy.getDescription()); + model.setDecisionStrategy(policy.getDecisionStrategy()); + model.setLogic(policy.getLogic()); + model.setConfig(policy.getConfig()); + + return model; + } + + public static ResourceRepresentation toRepresentation(Resource model, ResourceServer resourceServer, AuthorizationProvider authorization) { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setId(model.getId()); + resource.setType(model.getType()); + resource.setName(model.getName()); + resource.setUri(model.getUri()); + resource.setIconUri(model.getIconUri()); + + ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation(); + + owner.setId(model.getOwner()); + + KeycloakSession keycloakSession = authorization.getKeycloakSession(); + RealmModel realm = keycloakSession.getContext().getRealm(); + + if (owner.getId().equals(resourceServer.getClientId())) { + ClientModel clientModel = realm.getClientById(resourceServer.getClientId()); + owner.setName(clientModel.getClientId()); + } else { + UserModel userModel = keycloakSession.users().getUserById(owner.getId(), realm); + + if (userModel == null) { + throw new ErrorResponseException("invalid_owner", "Could not find the user [" + owner.getId() + "] who owns the Resource [" + resource.getId() + "].", Status.BAD_REQUEST); + } + + owner.setName(userModel.getUsername()); + } + + resource.setOwner(owner); + + resource.setScopes(model.getScopes().stream().map(model1 -> { + ScopeRepresentation scope = new ScopeRepresentation(); + scope.setId(model1.getId()); + scope.setName(model1.getName()); + String iconUri = model1.getIconUri(); + if (iconUri != null) { + scope.setIconUri(iconUri); + } + return scope; + }).collect(Collectors.toSet())); + + resource.setPolicies(new ArrayList<>()); + + Set policies = new HashSet<>(); + PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore(); + + policies.addAll(policyStore.findByResource(resource.getId())); + policies.addAll(policyStore.findByResourceType(resource.getType(), resourceServer.getId())); + policies.addAll(policyStore.findByScopeIds(resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toList()), resourceServer.getId())); + + for (Policy policyModel : policies) { + PolicyRepresentation policy = new PolicyRepresentation(); + + policy.setId(policyModel.getId()); + policy.setName(policyModel.getName()); + policy.setType(policyModel.getType()); + + if (!resource.getPolicies().contains(policy)) { + resource.getPolicies().add(policy); + } + } + + return resource; + } + + public static Resource toModel(ResourceRepresentation resource, ResourceServer resourceServer, AuthorizationProvider authorization) { + ResourceOwnerRepresentation owner = resource.getOwner(); + + if (owner == null) { + owner = new ResourceOwnerRepresentation(); + owner.setId(resourceServer.getClientId()); + } + + if (owner.getId() == null) { + throw new ErrorResponseException("invalid_owner", "No owner specified for resource [" + resource.getName() + "].", Status.BAD_REQUEST); + } + + ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore(); + Resource model = resourceStore.create(resource.getName(), resourceServer, owner.getId()); + + model.setType(resource.getType()); + model.setUri(resource.getUri()); + model.setIconUri(resource.getIconUri()); + + Set scopes = resource.getScopes(); + + if (scopes != null) { + model.updateScopes(scopes.stream().map((Function) scope -> toModel(scope, resourceServer, authorization)).collect(Collectors.toSet())); + } + + return model; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java new file mode 100644 index 00000000000..eaa8c78de81 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -0,0 +1,236 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.authorization; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.OAuthErrorException; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.representation.ScopeRepresentation; +import org.keycloak.authorization.authorization.representation.AuthorizationRequest; +import org.keycloak.authorization.authorization.representation.AuthorizationResponse; +import org.keycloak.authorization.common.KeycloakEvaluationContext; +import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.permission.ResourcePermission; +import org.keycloak.authorization.policy.evaluation.DecisionResultCollector; +import org.keycloak.authorization.policy.evaluation.Result; +import org.keycloak.authorization.protection.permission.PermissionTicket; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.authorization.util.Permissions; +import org.keycloak.authorization.util.Tokens; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.authorization.Permission; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.resources.Cors; + +import javax.ws.rs.Consumes; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Pedro Igor + */ +public class AuthorizationTokenService { + + private final AuthorizationProvider authorization; + + @Context + private HttpRequest httpRequest; + + public AuthorizationTokenService(AuthorizationProvider authorization) { + this.authorization = authorization; + } + + @OPTIONS + public Response authorizepPreFlight() { + return Cors.add(this.httpRequest, Response.ok()).auth().preflight().build(); + } + + @POST + @Consumes("application/json") + @Produces("application/json") + public void authorize(AuthorizationRequest authorizationRequest, @Suspended AsyncResponse asyncResponse) { + KeycloakEvaluationContext evaluationContext = new KeycloakEvaluationContext(this.authorization.getKeycloakSession()); + KeycloakIdentity identity = (KeycloakIdentity) evaluationContext.getIdentity(); + + if (!identity.hasRole("uma_authorization")) { + throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_authorization scope.", Status.FORBIDDEN); + } + + if (authorizationRequest == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid authorization request.", Status.BAD_REQUEST); + } + + PermissionTicket ticket = verifyPermissionTicket(authorizationRequest); + + authorization.evaluators().from(createPermissions(ticket, authorizationRequest, authorization), evaluationContext).evaluate(new DecisionResultCollector() { + @Override + public void onComplete(List results) { + List entitlements = Permissions.allPermits(results); + + if (entitlements.isEmpty()) { + asyncResponse.resume(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN)); + } else { + AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(entitlements, identity.getAccessToken())); + asyncResponse.resume(Cors.add(httpRequest, Response.status(Status.CREATED).entity(response)).allowedOrigins(identity.getAccessToken()) + .allowedMethods("POST") + .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build()); + } + } + + @Override + public void onError(Throwable cause) { + asyncResponse.resume(cause); + } + }); + } + + private List createPermissions(PermissionTicket ticket, AuthorizationRequest request, AuthorizationProvider authorization) { + StoreFactory storeFactory = authorization.getStoreFactory(); + Map> permissionsToEvaluate = new HashMap<>(); + + ticket.getResources().forEach(requestedResource -> { + Resource resource; + + if (requestedResource.getId() != null) { + resource = storeFactory.getResourceStore().findById(requestedResource.getId()); + } else { + resource = storeFactory.getResourceStore().findByName(requestedResource.getName(), ticket.getResourceServerId()); + } + + if (resource == null) { + throw new ErrorResponseException("invalid_resource", "Resource with id [" + requestedResource.getId() + "] or name [" + requestedResource.getName() + "] does not exist.", Status.FORBIDDEN); + } + + Set requestedScopes = requestedResource.getScopes(); + + permissionsToEvaluate.put(resource.getId(), requestedScopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet())); + }); + + String rpt = request.getRpt(); + + if (rpt != null && !"".equals(rpt)) { + if (!Tokens.verifySignature(rpt, getRealm().getPublicKey())) { + throw new ErrorResponseException("invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN); + } + + AccessToken requestingPartyToken; + + try { + requestingPartyToken = new JWSInput(rpt).readJsonContent(AccessToken.class); + } catch (JWSInputException e) { + throw new ErrorResponseException("invalid_rpt", "Invalid RPT", Status.FORBIDDEN); + } + + if (requestingPartyToken.isActive()) { + AccessToken.Authorization authorizationData = requestingPartyToken.getAuthorization(); + + if (authorizationData != null) { + List permissions = authorizationData.getPermissions(); + + if (permissions != null) { + permissions.forEach(permission -> { + Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId()); + + if (resourcePermission != null) { + Set scopes = permissionsToEvaluate.get(resourcePermission.getId()); + + if (scopes == null) { + scopes = new HashSet<>(); + permissionsToEvaluate.put(resourcePermission.getId(), scopes); + } + + Set scopePermission = permission.getScopes(); + + if (scopePermission != null) { + scopes.addAll(scopePermission); + } + } + }); + } + } + } + } + + return permissionsToEvaluate.entrySet().stream() + .flatMap((Function>, Stream>) entry -> { + Resource entryResource = storeFactory.getResourceStore().findById(entry.getKey()); + if (entry.getValue().isEmpty()) { + return Arrays.asList(new ResourcePermission(entryResource, Collections.emptyList(), entryResource.getResourceServer())).stream(); + } else { + return entry.getValue().stream() + .map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, entryResource.getResourceServer().getId())) + .filter(scope -> scope != null) + .map(scope -> new ResourcePermission(entryResource, Arrays.asList(scope), entryResource.getResourceServer())); + } + }).collect(Collectors.toList()); + } + + private RealmModel getRealm() { + return this.authorization.getKeycloakSession().getContext().getRealm(); + } + + private String createRequestingPartyToken(List permissions, AccessToken accessToken) { + AccessToken.Authorization authorization = new AccessToken.Authorization(); + + authorization.setPermissions(permissions); + accessToken.setAuthorization(authorization); + + return new TokenManager().encodeToken(getRealm(), accessToken); + } + + private PermissionTicket verifyPermissionTicket(AuthorizationRequest request) { + if (!Tokens.verifySignature(request.getTicket(), getRealm().getPublicKey())) { + throw new ErrorResponseException("invalid_ticket", "Ticket verification failed", Status.FORBIDDEN); + } + + try { + PermissionTicket ticket = new JWSInput(request.getTicket()).readJsonContent(PermissionTicket.class); + + if (!ticket.isActive()) { + throw new ErrorResponseException("invalid_ticket", "Invalid permission ticket.", Status.FORBIDDEN); + } + + return ticket; + } catch (JWSInputException e) { + throw new ErrorResponseException("invalid_ticket", "Could not parse permission ticket.", Status.FORBIDDEN); + } + } +} diff --git a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java new file mode 100644 index 00000000000..d4f0f24ad99 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.authorization.representation; + +/** + * @author Pedro Igor + */ +public class AuthorizationRequest { + + private String ticket; + private String rpt; + + public AuthorizationRequest(String ticket, String rpt) { + this.ticket = ticket; + this.rpt = rpt; + } + + public AuthorizationRequest(String ticket) { + this(ticket, null); + } + + public AuthorizationRequest() { + this(null, null); + } + + public String getTicket() { + return this.ticket; + } + + public String getRpt() { + return this.rpt; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java new file mode 100644 index 00000000000..cd0a521eb94 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.authorization.representation; + +/** + * @author Pedro Igor + */ +public class AuthorizationResponse { + + private String rpt; + + public AuthorizationResponse(String rpt) { + this.rpt = rpt; + } + + public AuthorizationResponse() { + this(null); + } + + public String getRpt() { + return this.rpt; + } + + public void setRpt(final String rpt) { + this.rpt = rpt; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java new file mode 100644 index 00000000000..bc967b9af4d --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java @@ -0,0 +1,80 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.common; + +import org.keycloak.authorization.attribute.Attributes; +import org.keycloak.authorization.identity.Identity; +import org.keycloak.authorization.policy.evaluation.EvaluationContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.AccessToken; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class KeycloakEvaluationContext implements EvaluationContext { + + private final KeycloakIdentity identity; + private final KeycloakSession keycloakSession; + + public KeycloakEvaluationContext(KeycloakSession keycloakSession) { + this(new KeycloakIdentity(keycloakSession), keycloakSession); + } + + public KeycloakEvaluationContext(KeycloakIdentity identity, KeycloakSession keycloakSession) { + this.identity = identity; + this.keycloakSession = keycloakSession; + } + + @Override + public Identity getIdentity() { + return this.identity; + } + + @Override + public Attributes getAttributes() { + HashMap> attributes = new HashMap<>(); + + attributes.put("kc.authz.context.time.date_time", Arrays.asList(new SimpleDateFormat("MM/dd/yyyy hh:mm:ss").format(new Date()))); + attributes.put("kc.authz.context.client.network.ip_address", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteAddr())); + attributes.put("kc.authz.context.client.network.host", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteHost())); + + AccessToken accessToken = this.identity.getAccessToken(); + + if (accessToken != null) { + attributes.put("kc.authz.context.client_id", Arrays.asList(accessToken.getIssuedFor())); + } + + List userAgents = this.keycloakSession.getContext().getRequestHeaders().getRequestHeader("User-Agent"); + + if (userAgents != null) { + attributes.put("kc.authz.context.client.user_agent", userAgents); + } + + attributes.put("kc.authz.context.authc.realm", Arrays.asList(this.keycloakSession.getContext().getRealm().getName())); + + return Attributes.from(attributes); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java new file mode 100644 index 00000000000..922785423e8 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java @@ -0,0 +1,155 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.common; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.keycloak.authorization.attribute.Attributes; +import org.keycloak.authorization.identity.Identity; +import org.keycloak.authorization.util.Tokens; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.AccessToken; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.util.JsonSerialization; + +import javax.ws.rs.core.Response.Status; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class KeycloakIdentity implements Identity { + + private final AccessToken accessToken; + private final RealmModel realm; + private final KeycloakSession keycloakSession; + private final Attributes attributes; + + public KeycloakIdentity(KeycloakSession keycloakSession) { + this(Tokens.getAccessToken(keycloakSession), keycloakSession); + } + + public KeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession) { + this.accessToken = accessToken; + + if (this.accessToken == null) { + throw new ErrorResponseException("invalid_bearer_token", "Could not obtain bearer access_token from request.", Status.FORBIDDEN); + } + + this.keycloakSession = keycloakSession; + this.realm = keycloakSession.getContext().getRealm(); + + HashMap> attributes = new HashMap<>(); + + try { + ObjectNode objectNode = JsonSerialization.createObjectNode(this.accessToken); + Iterator iterator = objectNode.fieldNames(); + List roleNames = new ArrayList<>(); + + while (iterator.hasNext()) { + String fieldName = iterator.next(); + JsonNode fieldValue = objectNode.get(fieldName); + List values = new ArrayList<>(); + + values.add(fieldValue.asText()); + + if (fieldName.equals("realm_access")) { + JsonNode grantedRoles = fieldValue.get("roles"); + + if (grantedRoles != null) { + Iterator rolesIt = grantedRoles.iterator(); + + while (rolesIt.hasNext()) { + roleNames.add(rolesIt.next().asText()); + } + } + } + + if (fieldName.equals("resource_access")) { + Iterator resourceAccessIt = fieldValue.iterator(); + + while (resourceAccessIt.hasNext()) { + JsonNode grantedRoles = resourceAccessIt.next().get("roles"); + + if (grantedRoles != null) { + Iterator rolesIt = grantedRoles.iterator(); + + while (rolesIt.hasNext()) { + roleNames.add(rolesIt.next().asText()); + } + } + } + } + + attributes.put(fieldName, values); + } + + attributes.put("roles", roleNames); + } catch (Exception e) { + throw new RuntimeException("Error while reading attributes from security token.", e); + } + + this.attributes = Attributes.from(attributes); + } + + @Override + public String getId() { + if (isResourceServer()) { + ClientSessionModel clientSession = this.keycloakSession.sessions().getClientSession(this.accessToken.getClientSession()); + return clientSession.getClient().getId(); + } + + return this.accessToken.getSubject(); + } + + @Override + public Attributes getAttributes() { + return this.attributes; + } + + public AccessToken getAccessToken() { + return this.accessToken; + } + + private boolean isResourceServer() { + UserModel clientUser = null; + + if (this.accessToken.getClientSession() != null) { + ClientSessionModel clientSession = this.keycloakSession.sessions().getClientSession(this.accessToken.getClientSession()); + clientUser = this.keycloakSession.users().getUserByServiceAccountClient(clientSession.getClient()); + } else if (this.accessToken.getIssuedFor() != null) { + ClientModel clientModel = this.keycloakSession.realms().getClientById(this.accessToken.getIssuedFor(), this.realm); + clientUser = this.keycloakSession.users().getUserByServiceAccountClient(clientModel); + } + + + if (clientUser == null) { + return false; + } + + return this.accessToken.getSubject().equals(clientUser.getId()); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/config/Configuration.java b/services/src/main/java/org/keycloak/authorization/config/Configuration.java new file mode 100644 index 00000000000..6669edc0278 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/config/Configuration.java @@ -0,0 +1,269 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.config; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import static org.keycloak.protocol.oidc.OIDCWellKnownProvider.DEFAULT_GRANT_TYPES_SUPPORTED; + +/** + * @author Pedro Igor + */ +public class Configuration { + + private static final String UMA_VERSION = "1.0"; + private static final List DEFAULT_TOKEN_PROFILES = Arrays.asList("bearer"); + + public static final Configuration fromDefault(String authzServerUri, + String realm, + URI authorizationEndpoint, + URI tokenEndpoint, String publicKeyPem) { + Configuration configuration = new Configuration(); + + if (authzServerUri.endsWith("/")) { + authzServerUri = authzServerUri.substring(0, authzServerUri.lastIndexOf("/")); + } + + configuration.setVersion(UMA_VERSION); + configuration.setIssuer(URI.create(authzServerUri)); + configuration.setPatProfiles(DEFAULT_TOKEN_PROFILES); + configuration.setAatProfiles(DEFAULT_TOKEN_PROFILES); + configuration.setRptProfiles(DEFAULT_TOKEN_PROFILES); + configuration.setPatGrantTypes(DEFAULT_GRANT_TYPES_SUPPORTED); + configuration.setAatGrantTypes(DEFAULT_GRANT_TYPES_SUPPORTED); + configuration.setTokenEndpoint(tokenEndpoint); + configuration.setAuthorizationEndpoint(authorizationEndpoint); + configuration.setResourceSetRegistrationEndpoint(URI.create(authzServerUri + "/authz/protection/resource_set")); + configuration.setPermissionRegistrationEndpoint(URI.create(authzServerUri + "/authz/protection/permission")); + configuration.setRptEndpoint(URI.create(authzServerUri + "/authz/authorize")); + configuration.setRealmPublicKey(publicKeyPem); + configuration.setServerUrl(URI.create(authzServerUri)); + configuration.setRealm(realm); + + return configuration; + } + + private String realmPublicKey; + private String version; + private URI issuer; + + @JsonProperty("pat_profiles_supported") + private List patProfiles; + + @JsonProperty("pat_grant_types_supported") + private List patGrantTypes; + + @JsonProperty("aat_profiles_supported") + private List aatProfiles; + + @JsonProperty("aat_grant_types_supported") + private List aatGrantTypes; + + @JsonProperty("rpt_profiles_supported") + private List rptProfiles; + + @JsonProperty("claim_token_profiles_supported") + private List claimTokenProfiles; + + @JsonProperty("dynamic_client_endpoint") + private URI dynamicClientEndpoint; + + @JsonProperty("token_endpoint") + private URI tokenEndpoint; + + @JsonProperty("authorization_endpoint") + private URI authorizationEndpoint; + + @JsonProperty("requesting_party_claims_endpoint") + private URI requestingPartyClaimsEndpoint; + + @JsonProperty("resource_set_registration_endpoint") + private URI resourceSetRegistrationEndpoint; + + @JsonProperty("introspection_endpoint") + private URI introspectionEndpoint; + + @JsonProperty("permission_registration_endpoint") + private URI permissionRegistrationEndpoint; + + @JsonProperty("rpt_endpoint") + private URI rptEndpoint; + + /** + * Non-standard, Keycloak specific configuration options + */ + private String realm; + + private URI serverUrl; + + public String getVersion() { + return this.version; + } + + void setVersion(final String version) { + this.version = version; + } + + public URI getIssuer() { + return this.issuer; + } + + void setIssuer(final URI issuer) { + this.issuer = issuer; + } + + public List getPatProfiles() { + return this.patProfiles; + } + + void setPatProfiles(final List patProfiles) { + this.patProfiles = patProfiles; + } + + public List getPatGrantTypes() { + return this.patGrantTypes; + } + + void setPatGrantTypes(final List patGrantTypes) { + this.patGrantTypes = patGrantTypes; + } + + public List getAatProfiles() { + return this.aatProfiles; + } + + void setAatProfiles(final List aatProfiles) { + this.aatProfiles = aatProfiles; + } + + public List getAatGrantTypes() { + return this.aatGrantTypes; + } + + void setAatGrantTypes(final List aatGrantTypes) { + this.aatGrantTypes = aatGrantTypes; + } + + public List getRptProfiles() { + return this.rptProfiles; + } + + void setRptProfiles(final List rptProfiles) { + this.rptProfiles = rptProfiles; + } + + public List getClaimTokenProfiles() { + return this.claimTokenProfiles; + } + + void setClaimTokenProfiles(final List claimTokenProfiles) { + this.claimTokenProfiles = claimTokenProfiles; + } + + public URI getDynamicClientEndpoint() { + return this.dynamicClientEndpoint; + } + + void setDynamicClientEndpoint(final URI dynamicClientEndpoint) { + this.dynamicClientEndpoint = dynamicClientEndpoint; + } + + public URI getTokenEndpoint() { + return this.tokenEndpoint; + } + + void setTokenEndpoint(final URI tokenEndpoint) { + this.tokenEndpoint = tokenEndpoint; + } + + public URI getAuthorizationEndpoint() { + return this.authorizationEndpoint; + } + + void setAuthorizationEndpoint(final URI authorizationEndpoint) { + this.authorizationEndpoint = authorizationEndpoint; + } + + public URI getRequestingPartyClaimsEndpoint() { + return this.requestingPartyClaimsEndpoint; + } + + void setRequestingPartyClaimsEndpoint(final URI requestingPartyClaimsEndpoint) { + this.requestingPartyClaimsEndpoint = requestingPartyClaimsEndpoint; + } + + public URI getResourceSetRegistrationEndpoint() { + return this.resourceSetRegistrationEndpoint; + } + + void setResourceSetRegistrationEndpoint(final URI resourceSetRegistrationEndpoint) { + this.resourceSetRegistrationEndpoint = resourceSetRegistrationEndpoint; + } + + public URI getIntrospectionEndpoint() { + return this.introspectionEndpoint; + } + + void setIntrospectionEndpoint(final URI introspectionEndpoint) { + this.introspectionEndpoint = introspectionEndpoint; + } + + public URI getPermissionRegistrationEndpoint() { + return this.permissionRegistrationEndpoint; + } + + void setPermissionRegistrationEndpoint(final URI permissionRegistrationEndpoint) { + this.permissionRegistrationEndpoint = permissionRegistrationEndpoint; + } + + public URI getRptEndpoint() { + return this.rptEndpoint; + } + + void setRptEndpoint(final URI rptEndpoint) { + this.rptEndpoint = rptEndpoint; + } + + public String getRealm() { + return this.realm; + } + + public void setRealm(final String realm) { + this.realm = realm; + } + + public URI getServerUrl() { + return this.serverUrl; + } + + public void setServerUrl(URI serverUrl) { + this.serverUrl = serverUrl; + } + + public void setRealmPublicKey(String realmPublicKey) { + this.realmPublicKey = realmPublicKey; + } + + public String getRealmPublicKey() { + return realmPublicKey; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java new file mode 100644 index 00000000000..07e5908666d --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java @@ -0,0 +1,56 @@ +/* + * 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.authorization.config; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.services.resources.RealmsResource; +import org.keycloak.wellknown.WellKnownProvider; + +import javax.ws.rs.core.UriInfo; +import java.net.URI; + +/** + * @author Pedro Igor + */ +public class UmaWellKnownProvider implements WellKnownProvider { + + private final KeycloakSession session; + + public UmaWellKnownProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public Object getConfig() { + RealmModel realm = this.session.getContext().getRealm(); + UriInfo uriInfo = this.session.getContext().getUri(); + + return Configuration.fromDefault(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(), realm.getName(), + URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "auth").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()), + URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "token").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()), + realm.getPublicKeyPem()); + } + + @Override + public void close() { + + } +} diff --git a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java new file mode 100644 index 00000000000..77767207911 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java @@ -0,0 +1,54 @@ +/* + * 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.authorization.config; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.wellknown.WellKnownProvider; +import org.keycloak.wellknown.WellKnownProviderFactory; + +/** + * @author Pedro Igor + */ +public class UmaWellKnownProviderFactory implements WellKnownProviderFactory { + @Override + public WellKnownProvider create(KeycloakSession session) { + return new UmaWellKnownProvider(session); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "uma-configuration"; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java new file mode 100644 index 00000000000..983646b6a68 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java @@ -0,0 +1,260 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.entitlement; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.OAuthErrorException; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.common.KeycloakEvaluationContext; +import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.entitlement.representation.EntitlementRequest; +import org.keycloak.authorization.entitlement.representation.EntitlementResponse; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.permission.ResourcePermission; +import org.keycloak.authorization.policy.evaluation.DecisionResultCollector; +import org.keycloak.authorization.policy.evaluation.Result; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.authorization.util.Permissions; +import org.keycloak.authorization.util.Tokens; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakContext; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.authorization.Permission; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.resources.Cors; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Pedro Igor + */ +public class EntitlementService { + + private final AuthorizationProvider authorization; + + @Context + private HttpRequest request; + + public EntitlementService(AuthorizationProvider authorization) { + this.authorization = authorization; + } + + @OPTIONS + public Response authorizePreFlight() { + return Cors.add(this.request, Response.ok()).auth().preflight().build(); + } + + @Path("{resource_server_id}") + @GET() + @Produces("application/json") + @Consumes("application/json") + public void getAll(@PathParam("resource_server_id") String resourceServerId, @Suspended AsyncResponse asyncResponse) { + KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession()); + + if (resourceServerId == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Requires resource_server_id request parameter.", Status.BAD_REQUEST); + } + + RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm(); + ClientModel client = realm.getClientByClientId(resourceServerId); + + if (client == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Identifier is not associated with any client and resource server.", Status.BAD_REQUEST); + } + + StoreFactory storeFactory = authorization.getStoreFactory(); + ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(client.getId()); + + authorization.evaluators().from(Permissions.all(resourceServer, identity, authorization), new KeycloakEvaluationContext(this.authorization.getKeycloakSession())).evaluate(new DecisionResultCollector() { + + @Override + public void onError(Throwable cause) { + asyncResponse.resume(cause); + } + + @Override + protected void onComplete(List results) { + List entitlements = Permissions.allPermits(results); + + if (entitlements.isEmpty()) { + asyncResponse.resume(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN)); + } else { + asyncResponse.resume(Cors.add(request, Response.ok().entity(new EntitlementResponse(createRequestingPartyToken(entitlements)))).allowedOrigins(identity.getAccessToken()).allowedMethods("GET").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build()); + } + } + }); + } + + @Path("{resource_server_id}") + @POST + @Consumes("application/json") + @Produces("application/json") + public void get(@PathParam("resource_server_id") String resourceServerId, EntitlementRequest entitlementRequest, @Suspended AsyncResponse asyncResponse) { + KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession()); + + if (entitlementRequest == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid entitlement request.", Status.BAD_REQUEST); + } + + if (resourceServerId == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid resource_server_id.", Status.BAD_REQUEST); + } + + RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm(); + + ClientModel client = realm.getClientByClientId(resourceServerId); + + if (client == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Identifier is not associated with any resource server.", Status.BAD_REQUEST); + } + + StoreFactory storeFactory = authorization.getStoreFactory(); + ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(client.getId()); + + authorization.evaluators().from(createPermissions(entitlementRequest, resourceServer, authorization), new KeycloakEvaluationContext(this.authorization.getKeycloakSession())).evaluate(new DecisionResultCollector() { + + @Override + public void onError(Throwable cause) { + asyncResponse.resume(cause); + } + + @Override + protected void onComplete(List results) { + List entitlements = Permissions.allPermits(results); + + if (entitlements.isEmpty()) { + asyncResponse.resume(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN)); + } else { + asyncResponse.resume(Cors.add(request, Response.ok().entity(new EntitlementResponse(createRequestingPartyToken(entitlements)))).allowedOrigins(identity.getAccessToken()).allowedMethods("GET").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build()); + } + } + }); + } + + private String createRequestingPartyToken(List permissions) { + AccessToken accessToken = Tokens.getAccessToken(this.authorization.getKeycloakSession()); + RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm(); + AccessToken.Authorization authorization = new AccessToken.Authorization(); + + authorization.setPermissions(permissions); + + accessToken.setAuthorization(authorization); + ; + return new TokenManager().encodeToken(realm, accessToken); + } + + private List createPermissions(EntitlementRequest entitlementRequest, ResourceServer resourceServer, AuthorizationProvider authorization) { + StoreFactory storeFactory = authorization.getStoreFactory(); + Map> permissionsToEvaluate = new HashMap<>(); + + entitlementRequest.getPermissions().forEach(requestedResource -> { + Resource resource; + + if (requestedResource.getResourceSetId() != null) { + resource = storeFactory.getResourceStore().findById(requestedResource.getResourceSetId()); + } else { + resource = storeFactory.getResourceStore().findByName(requestedResource.getResourceSetName(), resourceServer.getId()); + } + + if (resource == null) { + throw new ErrorResponseException("invalid_resource", "Resource with id [" + requestedResource.getResourceSetId() + "] or name [" + requestedResource.getResourceSetName() + "] does not exist.", Status.FORBIDDEN); + } + + permissionsToEvaluate.put(resource.getId(), requestedResource.getScopes()); + }); + + String rpt = entitlementRequest.getRpt(); + + if (rpt != null && !"".equals(rpt)) { + KeycloakContext context = authorization.getKeycloakSession().getContext(); + + if (!Tokens.verifySignature(rpt, context.getRealm().getPublicKey())) { + throw new ErrorResponseException("invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN); + } + + AccessToken requestingPartyToken; + + try { + requestingPartyToken = new JWSInput(rpt).readJsonContent(AccessToken.class); + } catch (JWSInputException e) { + throw new ErrorResponseException("invalid_rpt", "Invalid RPT", Status.FORBIDDEN); + } + + if (requestingPartyToken.isActive()) { + AccessToken.Authorization authorizationData = requestingPartyToken.getAuthorization(); + + if (authorizationData != null) { + authorizationData.getPermissions().forEach(permission -> { + Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId()); + + if (resourcePermission != null) { + Set scopes = permissionsToEvaluate.get(resourcePermission.getId()); + + if (scopes == null) { + scopes = new HashSet<>(); + permissionsToEvaluate.put(resourcePermission.getId(), scopes); + } + + scopes.addAll(permission.getScopes()); + } + }); + } + } + } + + return permissionsToEvaluate.entrySet().stream() + .flatMap((Function>, Stream>) entry -> { + Resource entryResource = storeFactory.getResourceStore().findById(entry.getKey()); + + if (entry.getValue().isEmpty()) { + return Arrays.asList(new ResourcePermission(entryResource, Collections.emptyList(), entryResource.getResourceServer())).stream(); + } else { + return entry.getValue().stream() + .map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, entryResource.getResourceServer().getId())) + .filter(scope -> scope != null) + .map(scope -> new ResourcePermission(entryResource, Arrays.asList(scope), entryResource.getResourceServer())); + } + }).collect(Collectors.toList()); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java new file mode 100644 index 00000000000..3afcc31651d --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java @@ -0,0 +1,25 @@ +package org.keycloak.authorization.entitlement.representation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.keycloak.authorization.protection.permission.representation.PermissionRequest; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class EntitlementRequest { + + private String rpt; + + private List permissions = new ArrayList<>(); + + public List getPermissions() { + return permissions; + } + + public String getRpt() { + return rpt; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java new file mode 100644 index 00000000000..8e883cd3217 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java @@ -0,0 +1,42 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.entitlement.representation; + +/** + * @author Pedro Igor + */ +public class EntitlementResponse { + + private String rpt; + + public EntitlementResponse(String rpt) { + this.rpt = rpt; + } + + public EntitlementResponse() { + this(null); + } + + public String getRpt() { + return this.rpt; + } + + public void setRpt(final String rpt) { + this.rpt = rpt; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java new file mode 100644 index 00000000000..f3695d00ef5 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java @@ -0,0 +1,117 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection; + +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.OAuthErrorException; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.ResourceSetService; +import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.identity.Identity; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.protection.permission.PermissionService; +import org.keycloak.authorization.protection.permission.PermissionsService; +import org.keycloak.authorization.protection.resource.ResourceService; +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.services.ErrorResponseException; + +import javax.ws.rs.Path; +import javax.ws.rs.core.Response.Status; + +/** + * @author Pedro Igor + */ +public class ProtectionService { + + private final AuthorizationProvider authorization; + + public ProtectionService(AuthorizationProvider authorization) { + this.authorization = authorization; + } + + @Path("/resource_set") + public Object resource() { + KeycloakIdentity identity = createIdentity(); + + if (!identity.hasRole("uma_protection")) { + throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN); + } + + ResourceSetService resourceManager = new ResourceSetService(getResourceServer(identity), this.authorization, null); + + ResteasyProviderFactory.getInstance().injectProperties(resourceManager); + + ResourceService resource = new ResourceService(getResourceServer(identity), identity, resourceManager, this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } + + @Path("/permission") + public Object permission() { + KeycloakIdentity identity = createIdentity(); + + if (!identity.hasRole("uma_protection")) { + throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN); + } + + PermissionService resource = new PermissionService(identity, getResourceServer(identity), this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } + + @Path("/permissions") + public Object permissions() { + KeycloakIdentity identity = createIdentity(); + + if (!identity.hasRole("uma_protection")) { + throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN); + } + + PermissionsService resource = new PermissionsService(identity, getResourceServer(identity), this.authorization); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } + + private KeycloakIdentity createIdentity() { + return new KeycloakIdentity(this.authorization.getKeycloakSession()); + } + + private ResourceServer getResourceServer(Identity identity) { + RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm(); + ClientModel clientApplication = realm.getClientById(identity.getId()); + + if (clientApplication == null) { + throw new ErrorResponseException("invalid_clientId", "Client application with id [" + identity.getId() + "] does not exist in realm [" + realm.getName() + "]", Status.BAD_REQUEST); + } + + ResourceServer resourceServer = this.authorization.getStoreFactory().getResourceServerStore().findByClient(identity.getId()); + + if (resourceServer == null) { + throw new ErrorResponseException("invalid_clientId", "Client application [" + clientApplication.getClientId() + "] is not registered as resource server.", Status.FORBIDDEN); + } + + return resourceServer; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java new file mode 100644 index 00000000000..a31e834e4b8 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java @@ -0,0 +1,86 @@ +/* + * 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.authorization.protection.introspect; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessToken.Authorization; +import org.keycloak.util.JsonSerialization; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Introspects token accordingly with UMA Bearer Token Profile. + * + * @author Pedro Igor + */ +public class RPTIntrospectionProvider extends AccessTokenIntrospectionProvider { + + protected static final Logger LOGGER = Logger.getLogger(RPTIntrospectionProvider.class); + + public RPTIntrospectionProvider(KeycloakSession session) { + super(session); + } + + @Override + public Response introspect(String token) { + LOGGER.debug("Introspecting requesting party token"); + try { + AccessToken requestingPartyToken = toAccessToken(token); + boolean active = isActive(requestingPartyToken); + ObjectNode tokenMetadata; + + if (active) { + LOGGER.debug("Token is active"); + AccessToken introspect = new AccessToken(); + introspect.type(requestingPartyToken.getType()); + introspect.expiration(requestingPartyToken.getExpiration()); + introspect.issuedAt(requestingPartyToken.getIssuedAt()); + introspect.audience(requestingPartyToken.getAudience()); + introspect.notBefore(requestingPartyToken.getNotBefore()); + introspect.setRealmAccess(null); + introspect.setResourceAccess(null); + tokenMetadata = JsonSerialization.createObjectNode(introspect); + tokenMetadata.putPOJO("permissions", requestingPartyToken.getAuthorization().getPermissions()); + } else { + LOGGER.debug("Token is not active"); + tokenMetadata = JsonSerialization.createObjectNode(); + } + + tokenMetadata.put("active", active); + + return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).type(MediaType.APPLICATION_JSON_TYPE).build(); + } catch (Exception e) { + throw new RuntimeException("Error creating token introspection response.", e); + } + } + + private boolean isActive(AccessToken requestingPartyToken) { + Authorization authorization = requestingPartyToken.getAuthorization(); + return requestingPartyToken.isActive() && authorization != null && authorization.getPermissions() != null && !authorization.getPermissions().isEmpty(); + } + + @Override + public void close() { + + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProviderFactory.java b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProviderFactory.java new file mode 100644 index 00000000000..1c4d894842f --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProviderFactory.java @@ -0,0 +1,54 @@ +/* + * 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.authorization.protection.introspect; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.protocol.oidc.TokenIntrospectionProvider; +import org.keycloak.protocol.oidc.TokenIntrospectionProviderFactory; + +/** + * @author Pedro Igor + */ +public class RPTIntrospectionProviderFactory implements TokenIntrospectionProviderFactory { + @Override + public TokenIntrospectionProvider create(KeycloakSession session) { + return new RPTIntrospectionProvider(session); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return "requesting_party_token"; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java new file mode 100644 index 00000000000..cf2f9e01b68 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java @@ -0,0 +1,102 @@ +package org.keycloak.authorization.protection.permission; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.representation.ResourceRepresentation; +import org.keycloak.authorization.admin.representation.ScopeRepresentation; +import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.protection.permission.representation.PermissionRequest; +import org.keycloak.authorization.protection.permission.representation.PermissionResponse; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.services.ErrorResponseException; + +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class AbstractPermissionService { + + private final AuthorizationProvider authorization; + private final KeycloakIdentity identity; + private final ResourceServer resourceServer; + + public AbstractPermissionService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization) { + this.identity = identity; + this.resourceServer = resourceServer; + this.authorization = authorization; + } + + public Response create(List request) { + if (request == null) { + throw new ErrorResponseException("invalid_permission_request", "Invalid permission request.", Response.Status.BAD_REQUEST); + } + + List resource = verifyRequestedResource(request); + + return Response.status(Response.Status.CREATED).entity(new PermissionResponse(createPermissionTicket(resource))).build(); + } + + private List verifyRequestedResource(List request) { + StoreFactory storeFactory = authorization.getStoreFactory(); + return request.stream().map(request1 -> { + String resourceSetId = request1.getResourceSetId(); + String resourceSetName = request1.getResourceSetName(); + + if (resourceSetId == null && resourceSetName == null) { + throw new ErrorResponseException("invalid_resource_set_id", "Resource id or name not provided.", Response.Status.BAD_REQUEST); + } + + Resource resource; + + if (resourceSetId != null) { + resource = storeFactory.getResourceStore().findById(resourceSetId); + } else { + resource = storeFactory.getResourceStore().findByName(resourceSetName, this.resourceServer.getId()); + } + + if (resource == null) { + if (resourceSetId != null) { + throw new ErrorResponseException("nonexistent_resource_set_id", "Resource set with id[" + resourceSetId + "] does not exists in this server.", Response.Status.BAD_REQUEST); + } else { + throw new ErrorResponseException("nonexistent_resource_set_name", "Resource set with name[" + resourceSetName + "] does not exists in this server.", Response.Status.BAD_REQUEST); + } + } + + return new ResourceRepresentation(resource.getName(), verifyRequestedScopes(request1, resource)); + }).collect(Collectors.toList()); + } + + private Set verifyRequestedScopes(PermissionRequest request, Resource resource) { + return request.getScopes().stream().map(scopeName -> { + for (Scope scope : resource.getScopes()) { + if (scope.getName().equals(scopeName)) { + return new ScopeRepresentation(scopeName); + } + } + + for (Resource baseResource : authorization.getStoreFactory().getResourceStore().findByType(resource.getType())) { + if (baseResource.getOwner().equals(resource.getResourceServer().getClientId())) { + for (Scope baseScope : baseResource.getScopes()) { + if (baseScope.getName().equals(scopeName)) { + return new ScopeRepresentation(scopeName); + } + } + } + } + + throw new ErrorResponseException("invalid_scope", "Scope [" + scopeName + " is not valid.", Response.Status.BAD_REQUEST); + }).collect(Collectors.toSet()); + } + + private String createPermissionTicket(List resources) { + return new JWSBuilder().jsonContent(new PermissionTicket(resources, this.resourceServer.getId(), this.identity.getAccessToken())) + .rsa256(this.authorization.getKeycloakSession().getContext().getRealm().getPrivateKey()); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java new file mode 100644 index 00000000000..9d547307a32 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java @@ -0,0 +1,59 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.permission; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.representation.ResourceRepresentation; +import org.keycloak.authorization.admin.representation.ScopeRepresentation; +import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.protection.permission.representation.PermissionRequest; +import org.keycloak.authorization.protection.permission.representation.PermissionResponse; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.services.ErrorResponseException; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class PermissionService extends AbstractPermissionService { + + public PermissionService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization) { + super(identity, resourceServer, authorization); + } + + @POST + @Consumes("application/json") + @Produces("application/json") + public Response create(PermissionRequest request) { + return create(Arrays.asList(request)); + } + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java new file mode 100644 index 00000000000..9ee636898f6 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java @@ -0,0 +1,58 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.permission; + +import org.keycloak.TokenIdGenerator; +import org.keycloak.authorization.admin.representation.ResourceRepresentation; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.JsonWebToken; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class PermissionTicket extends JsonWebToken { + + private final List resources = new ArrayList<>(); + private final String resourceServerId; + + public PermissionTicket() { + this.resourceServerId = null; + } + + public PermissionTicket(List resources, String resourceServerId, AccessToken accessToken) { + id(TokenIdGenerator.generateId()); + subject(accessToken.getSubject()); + expiration(accessToken.getExpiration()); + notBefore(accessToken.getNotBefore()); + issuedAt(accessToken.getIssuedAt()); + issuedFor(accessToken.getIssuedFor()); + this.resources.addAll(resources); + this.resourceServerId = resourceServerId; + } + + public List getResources() { + return this.resources; + } + + public String getResourceServerId() { + return this.resourceServerId; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java new file mode 100644 index 00000000000..eea21088f31 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java @@ -0,0 +1,46 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.permission; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.common.KeycloakIdentity; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.protection.permission.representation.PermissionRequest; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class PermissionsService extends AbstractPermissionService { + + public PermissionsService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization) { + super(identity, resourceServer, authorization); + } + + @POST + @Consumes("application/json") + @Produces("application/json") + public Response create(List request) { + return super.create(request); + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java new file mode 100644 index 00000000000..31d6d559bbb --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.permission.representation; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Pedro Igor + */ +public class PermissionRequest { + + @JsonProperty("resource_set_id") + private final String resourceSetId; + + @JsonProperty("resource_set_name") + private final String resourceSetName; + + private final Set scopes; + + public PermissionRequest(String resourceSetId, String... scopes) { + this.resourceSetId = resourceSetId; + + if (scopes != null) { + this.scopes = new HashSet(Arrays.asList(scopes)); + } else { + this.scopes = new HashSet<>(); + } + + this.resourceSetName = null; + } + + public PermissionRequest() { + this(null, null); + } + + public String getResourceSetId() { + return this.resourceSetId; + } + + public String getResourceSetName() { + return resourceSetName; + } + + public Set getScopes() { + return this.scopes; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java new file mode 100644 index 00000000000..24efefa3a8d --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java @@ -0,0 +1,39 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.permission.representation; + +/** + * @author Pedro Igor + */ +public class PermissionResponse { + + private final String ticket; + + public PermissionResponse(String ticket) { + this.ticket = ticket; + } + + public PermissionResponse() { + this(null); + } + + public String getTicket() { + return this.ticket; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/RegistrationResponse.java b/services/src/main/java/org/keycloak/authorization/protection/resource/RegistrationResponse.java new file mode 100644 index 00000000000..4f822429510 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/resource/RegistrationResponse.java @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.resource; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import org.keycloak.authorization.protection.resource.representation.UmaResourceRepresentation; + +/** + * @author Pedro Igor + */ +public class RegistrationResponse { + + private final UmaResourceRepresentation resourceDescription; + + public RegistrationResponse(UmaResourceRepresentation resourceDescription) { + this.resourceDescription = resourceDescription; + } + + public RegistrationResponse() { + this(null); + } + + @JsonUnwrapped + public UmaResourceRepresentation getResourceDescription() { + return this.resourceDescription; + } + + public String getId() { + if (this.resourceDescription != null) { + return this.resourceDescription.getId(); + } + + return null; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java new file mode 100644 index 00000000000..f4aaac576e9 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java @@ -0,0 +1,228 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.resource; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.admin.ResourceSetService; +import org.keycloak.authorization.admin.representation.ResourceOwnerRepresentation; +import org.keycloak.authorization.admin.representation.ResourceRepresentation; +import org.keycloak.authorization.admin.representation.ScopeRepresentation; +import org.keycloak.authorization.admin.util.Models; +import org.keycloak.authorization.identity.Identity; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.protection.resource.representation.UmaResourceRepresentation; +import org.keycloak.authorization.protection.resource.representation.UmaScopeRepresentation; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.services.ErrorResponseException; + +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.QueryParam; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public class ResourceService { + + private final ResourceServer resourceServer; + private final ResourceSetService resourceManager; + private final Identity identity; + private final AuthorizationProvider authorization; + + public ResourceService(ResourceServer resourceServer, Identity identity, ResourceSetService resourceManager, AuthorizationProvider authorization) { + this.identity = identity; + this.resourceServer = resourceServer; + this.resourceManager = resourceManager; + this.authorization = authorization; + } + + @POST + @Consumes("application/json") + @Produces("application/json") + public Response create(UmaResourceRepresentation umaResource) { + checkResourceServerSettings(); + ResourceRepresentation resource = toResourceRepresentation(umaResource); + Response response = this.resourceManager.create(resource); + + if (response.getEntity() instanceof ResourceRepresentation) { + return Response.status(Status.CREATED).entity(toUmaRepresentation((ResourceRepresentation) response.getEntity())).build(); + } + + return response; + } + + @Path("/{id}") + @DELETE + public Response delete(@PathParam("id") String id) { + checkResourceServerSettings(); + return this.resourceManager.delete(id); + } + + @Path("/{id}") + @GET + @Produces("application/json") + public RegistrationResponse findById(@PathParam("id") String id) { + Response response = this.resourceManager.findById(id); + UmaResourceRepresentation resource = toUmaRepresentation((ResourceRepresentation) response.getEntity()); + + if (resource == null) { + throw new ErrorResponseException("not_found", "Resource with id [" + id + "] not found.", Status.NOT_FOUND); + } + + return new RegistrationResponse(resource); + } + + @GET + @Produces("application/json") + public Set find(@QueryParam("filter") String filter) { + if (filter == null) { + return findAll(); + } else { + return findByFilter(filter); + } + } + + private Set findAll() { + Response response = this.resourceManager.findAll(); + List resources = (List) response.getEntity(); + return resources.stream().map(ResourceRepresentation::getId).collect(Collectors.toSet()); + } + + private Set findByFilter(String filter) { + Set resources = new HashSet<>(); + StoreFactory storeFactory = authorization.getStoreFactory(); + + if (filter != null) { + for (String currentFilter : filter.split("&")) { + String[] parts = currentFilter.split("="); + String filterType = parts[0]; + final String filterValue; + + if (parts.length > 1) { + filterValue = parts[1]; + } else { + filterValue = null; + } + + + if ("name".equals(filterType)) { + resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getName())).collect(Collectors.toSet()).stream() + .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization)) + .collect(Collectors.toList())); + } else if ("type".equals(filterType)) { + resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getType())).collect(Collectors.toSet()).stream() + .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization)) + .collect(Collectors.toList())); + } else if ("uri".equals(filterType)) { + resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getUri())).collect(Collectors.toSet()).stream() + .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization)) + .collect(Collectors.toList())); + } else if ("owner".equals(filterType)) { + resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getOwner())).collect(Collectors.toSet()).stream() + .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization)) + .collect(Collectors.toList())); + } + } + } else { + resources = storeFactory.getResourceStore().findByOwner(identity.getId()).stream() + .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization)) + .collect(Collectors.toSet()); + } + + return resources.stream() + .map(ResourceRepresentation::getId) + .collect(Collectors.toSet()); + } + + private ResourceRepresentation toResourceRepresentation(UmaResourceRepresentation umaResource) { + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setId(umaResource.getId()); + resource.setIconUri(umaResource.getIconUri()); + resource.setName(umaResource.getName()); + resource.setUri(umaResource.getUri()); + resource.setType(umaResource.getType()); + + ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation(); + String ownerId = umaResource.getOwner(); + + if (ownerId == null) { + ownerId = this.identity.getId(); + } + + owner.setId(ownerId); + resource.setOwner(owner); + + resource.setScopes(umaResource.getScopes().stream().map(representation -> { + ScopeRepresentation scopeRepresentation = new ScopeRepresentation(); + + scopeRepresentation.setId(representation.getId()); + scopeRepresentation.setName(representation.getName()); + scopeRepresentation.setIconUri(representation.getIconUri()); + + return scopeRepresentation; + }).collect(Collectors.toSet())); + + return resource; + } + + private UmaResourceRepresentation toUmaRepresentation(ResourceRepresentation representation) { + if (representation == null) { + return null; + } + + UmaResourceRepresentation resource = new UmaResourceRepresentation(); + + resource.setId(representation.getId()); + resource.setIconUri(representation.getIconUri()); + resource.setName(representation.getName()); + resource.setUri(representation.getUri()); + resource.setType(representation.getType()); + + if (representation.getOwner() != null) { + resource.setOwner(representation.getOwner().getId()); + } + + resource.setScopes(representation.getScopes().stream().map(scopeRepresentation -> { + UmaScopeRepresentation umaScopeRep = new UmaScopeRepresentation(); + umaScopeRep.setId(scopeRepresentation.getId()); + umaScopeRep.setName(scopeRepresentation.getName()); + umaScopeRep.setIconUri(scopeRepresentation.getIconUri()); + return umaScopeRep; + }).collect(Collectors.toSet())); + + return resource; + } + + private void checkResourceServerSettings() { + if (!this.resourceServer.isAllowRemoteResourceManagement()) { + throw new ErrorResponseException("not_supported", "Remote management is disabled.", Status.BAD_REQUEST); + } + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/RegistrationResponse.java b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/RegistrationResponse.java new file mode 100644 index 00000000000..6922d51f7bd --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/RegistrationResponse.java @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.resource.representation; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; + +/** + * @author Pedro Igor + */ +public class RegistrationResponse { + + private final UmaResourceRepresentation resourceDescription; + + public RegistrationResponse(UmaResourceRepresentation resourceDescription) { + this.resourceDescription = resourceDescription; + } + + public RegistrationResponse() { + this(null); + } + + @JsonUnwrapped + public UmaResourceRepresentation getResourceDescription() { + return this.resourceDescription; + } + + public String getId() { + if (this.resourceDescription != null) { + return this.resourceDescription.getId(); + } + + return null; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java new file mode 100644 index 00000000000..0e9bafae518 --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java @@ -0,0 +1,150 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.resource.representation; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.net.URI; +import java.util.Collections; +import java.util.Set; + +/** + *

One or more resources that the resource server manages as a set of protected resources. + * + *

For more details, OAuth-resource-reg. + * + * @author Pedro Igor + */ +public class UmaResourceRepresentation { + + @JsonProperty("_id") + private String id; + + private String name; + private String uri; + private String type; + private Set scopes; + + @JsonProperty("icon_uri") + private String iconUri; + private String owner; + + /** + * Creates a new instance. + * + * @param name a human-readable string describing a set of one or more resources + * @param uri a {@link URI} that provides the network location for the resource set being registered + * @param type a string uniquely identifying the semantics of the resource set + * @param scopes the available scopes for this resource set + * @param iconUri a {@link URI} for a graphic icon representing the resource set + */ + public UmaResourceRepresentation(String name, Set scopes, String uri, String type, String iconUri) { + this.name = name; + this.scopes = scopes; + this.uri = uri; + this.type = type; + this.iconUri = iconUri; + } + + /** + * Creates a new instance. + * + * @param name a human-readable string describing a set of one or more resources + * @param uri a {@link URI} that provides the network location for the resource set being registered + * @param type a string uniquely identifying the semantics of the resource set + * @param scopes the available scopes for this resource set + */ + public UmaResourceRepresentation(String name, Set scopes, String uri, String type) { + this(name, scopes, uri, type, null); + } + + /** + * Creates a new instance. + * + * @param name a human-readable string describing a set of one or more resources + * @param serverUri a {@link URI} that identifies this resource server + * @param scopes the available scopes for this resource set + */ + public UmaResourceRepresentation(String name, Set scopes) { + this(name, scopes, null, null, null); + } + + /** + * Creates a new instance. + * + */ + public UmaResourceRepresentation() { + this(null, null, null, null, null); + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public String getUri() { + return this.uri; + } + + public String getType() { + return this.type; + } + + public Set getScopes() { + return Collections.unmodifiableSet(this.scopes); + } + + public String getIconUri() { + return this.iconUri; + } + + public void setName(String name) { + this.name = name; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public void setType(String type) { + this.type = type; + } + + public void setScopes(Set scopes) { + this.scopes = scopes; + } + + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaScopeRepresentation.java b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaScopeRepresentation.java new file mode 100644 index 00000000000..4a184d9daeb --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaScopeRepresentation.java @@ -0,0 +1,98 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.protection.resource.representation; + +import java.net.URI; +import java.util.Objects; + +/** + *

A bounded extent of access that is possible to perform on a resource set. In authorization policy terminology, + * a scope is one of the potentially many "verbs" that can logically apply to a resource set ("object"). + * + *

For more details, OAuth-resource-reg. + * + * @author Pedro Igor + */ +public class UmaScopeRepresentation { + + private String id; + private String name; + private String iconUri; + + /** + * Creates an instance. + * + * @param name the a human-readable string describing some scope (extent) of access + * @param iconUri a {@link URI} for a graphic icon representing the scope + */ + public UmaScopeRepresentation(String name, String iconUri) { + this.name = name; + this.iconUri = iconUri; + } + + /** + * Creates an instance. + * + * @param name the a human-readable string describing some scope (extent) of access + */ + public UmaScopeRepresentation(String name) { + this(name, null); + } + + /** + * Creates an instance. + */ + public UmaScopeRepresentation() { + this(null, null); + } + + public String getName() { + return this.name; + } + + public String getIconUri() { + return this.iconUri; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UmaScopeRepresentation scope = (UmaScopeRepresentation) o; + return Objects.equals(getName(), scope.getName()); + } + + public int hashCode() { + return Objects.hash(getName()); + } + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setIconUri(String iconUri) { + this.iconUri = iconUri; + } +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/authorization/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java new file mode 100644 index 00000000000..240fafad9eb --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java @@ -0,0 +1,138 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.util; + +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.Decision.Effect; +import org.keycloak.authorization.identity.Identity; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.model.Scope; +import org.keycloak.authorization.permission.ResourcePermission; +import org.keycloak.authorization.policy.evaluation.Result; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.representations.authorization.Permission; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public final class Permissions { + + /** + * Returns a list of permissions for all resources and scopes that belong to the given resourceServer and + * identity. + * + * TODO: review once we support caches + * + * @param resourceServer + * @param identity + * @param authorization + * @return + */ + public static List all(ResourceServer resourceServer, Identity identity, AuthorizationProvider authorization) { + List permissions = new ArrayList<>(); + StoreFactory storeFactory = authorization.getStoreFactory(); + + storeFactory.getResourceStore().findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource))); + storeFactory.getResourceStore().findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource))); + + return permissions; + } + + public static List createResourcePermissions(Resource resource) { + List permissions = new ArrayList<>(); + List scopes = resource.getScopes(); + + permissions.add(new ResourcePermission(resource, Collections.emptyList(), resource.getResourceServer())); + + for (Scope scope : scopes) { + permissions.add(new ResourcePermission(resource, Arrays.asList(scope), resource.getResourceServer())); + } + + return permissions; + } + + public static List allPermits(List evaluation) { + List permissions = evaluation.stream() + .filter(evaluationResult -> evaluationResult.getEffect().equals(Effect.PERMIT)) + .map(evaluationResult -> { + ResourcePermission permission = evaluationResult.getPermission(); + String resourceId = null; + String resourceName = null; + + Resource resource = permission.getResource(); + + if (resource != null) { + resourceId = resource.getId(); + resourceName = resource.getName(); + } + + Set scopes = null; + + if (!permission.getScopes().isEmpty()) { + scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()); + } + + return new Permission(resourceId, resourceName, scopes); + }).collect(Collectors.toList()); + + Map perms = new HashMap<>(); + + permissions.forEach(permission -> { + Permission evalPermission = perms.get(permission.getResourceSetId()); + + if (evalPermission == null) { + evalPermission = permission; + if (evalPermission.getScopes() != null && evalPermission.getScopes().isEmpty()) { + evalPermission.setScopes(null); + } + perms.put(permission.getResourceSetId(), evalPermission); + } + + Set permissionScopes = permission.getScopes(); + + if (permissionScopes != null && !permissionScopes.isEmpty()) { + Set scopes = evalPermission.getScopes(); + + if (scopes == null) { + scopes = new HashSet(); + evalPermission.setScopes(scopes); + } + + for (String scopeName : permissionScopes) { + if (!scopes.contains(scopeName)) { + scopes.add(scopeName); + } + } + } + }); + + return perms.values().stream().collect(Collectors.toList()); + } +} diff --git a/services/src/main/java/org/keycloak/authorization/util/Tokens.java b/services/src/main/java/org/keycloak/authorization/util/Tokens.java new file mode 100644 index 00000000000..0deeef5ecac --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/util/Tokens.java @@ -0,0 +1,65 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization.util; + +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.KeycloakContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.representations.AccessToken; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.AuthenticationManager.AuthResult; + +import javax.ws.rs.core.Response.Status; +import java.security.PublicKey; + +/** + * @author Pedro Igor + */ +public class Tokens { + + public static AccessToken getAccessToken(KeycloakSession keycloakSession) { + AppAuthManager authManager = new AppAuthManager(); + KeycloakContext context = keycloakSession.getContext(); + AuthResult authResult = authManager.authenticateBearerToken(keycloakSession, context.getRealm(), context.getUri(), context.getConnection(), context.getRequestHeaders()); + + if (authResult != null) { + return authResult.getToken(); + } + + return null; + } + + public static String getAccessTokenAsString(KeycloakSession keycloakSession) { + AppAuthManager authManager = new AppAuthManager(); + + return authManager.extractAuthorizationHeaderToken(keycloakSession.getContext().getRequestHeaders()); + } + + public static boolean verifySignature(String token, PublicKey publicKey) { + try { + JWSInput jws = new JWSInput(token); + + return RSAProvider.verify(jws, publicKey); + } catch (Exception e) { + throw new ErrorResponseException("invalid_signature", "Unexpected error while validating signature.", Status.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java new file mode 100644 index 00000000000..03787652b99 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java @@ -0,0 +1,83 @@ +/* + * 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.protocol.oidc; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.keycloak.RSATokenVerifier; +import org.keycloak.common.VerificationException; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.AccessToken; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.util.JsonSerialization; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * @author Pedro Igor + */ +public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvider { + + private final KeycloakSession session; + private final TokenManager tokenManager; + private final RealmModel realm; + + public AccessTokenIntrospectionProvider(KeycloakSession session) { + this.session = session; + this.realm = session.getContext().getRealm(); + this.tokenManager = new TokenManager(); + } + + public Response introspect(String token) { + try { + AccessToken toIntrospect = toAccessToken(token); + RealmModel realm = this.session.getContext().getRealm(); + ObjectNode tokenMetadata; + + boolean active = tokenManager.isTokenValid(session, realm, toIntrospect); + + if (active) { + tokenMetadata = JsonSerialization.createObjectNode(toIntrospect); + tokenMetadata.put("client_id", toIntrospect.getIssuedFor()); + tokenMetadata.put("username", toIntrospect.getPreferredUsername()); + } else { + tokenMetadata = JsonSerialization.createObjectNode(); + } + + tokenMetadata.put("active", active); + + return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).type(MediaType.APPLICATION_JSON_TYPE).build(); + } catch (Exception e) { + throw new RuntimeException("Error creating token introspection response.", e); + } + } + + protected AccessToken toAccessToken(String token) { + try { + return RSATokenVerifier.toAccessToken(token, realm.getPublicKey()); + } catch (VerificationException e) { + throw new ErrorResponseException("invalid_request", "Invalid token.", Response.Status.UNAUTHORIZED); + } + } + + @Override + public void close() { + + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProviderFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProviderFactory.java new file mode 100644 index 00000000000..19c9f182249 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProviderFactory.java @@ -0,0 +1,55 @@ +/* + * 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.protocol.oidc; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Pedro Igor + */ +public class AccessTokenIntrospectionProviderFactory implements TokenIntrospectionProviderFactory { + + public static final String ACCESS_TOKEN_TYPE = "access_token"; + + @Override + public TokenIntrospectionProvider create(KeycloakSession session) { + return new AccessTokenIntrospectionProvider(session); + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return ACCESS_TOKEN_TYPE; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProvider.java new file mode 100644 index 00000000000..23c675b86d7 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProvider.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.protocol.oidc; + +import org.keycloak.models.KeycloakSession; + +/** + * @author Pedro Igor + */ +public class RefreshTokenIntrospectionProvider extends AccessTokenIntrospectionProvider { + + public RefreshTokenIntrospectionProvider(KeycloakSession session) { + super(session); + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProviderFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProviderFactory.java new file mode 100644 index 00000000000..9f5ef76093c --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProviderFactory.java @@ -0,0 +1,38 @@ +/* + * 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.protocol.oidc; + +import org.keycloak.models.KeycloakSession; + +/** + * @author Pedro Igor + */ +public class RefreshTokenIntrospectionProviderFactory extends AccessTokenIntrospectionProviderFactory { + + private static final String REFRESH_TOKEN_TYPE = "refresh_token"; + + @Override + public TokenIntrospectionProvider create(KeycloakSession session) { + return new RefreshTokenIntrospectionProvider(session); + } + + @Override + public String getId() { + return REFRESH_TOKEN_TYPE; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 1ff3da9d017..4f17cdda069 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -132,7 +132,7 @@ public class TokenEndpoint { @Path("introspect") public Object introspect() { - TokenIntrospectionEndpoint tokenIntrospectionEndpoint = new TokenIntrospectionEndpoint(this.realm, this.tokenManager, this.event); + TokenIntrospectionEndpoint tokenIntrospectionEndpoint = new TokenIntrospectionEndpoint(this.realm, this.event); ResteasyProviderFactory.getInstance().injectProperties(tokenIntrospectionEndpoint); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenIntrospectionEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenIntrospectionEndpoint.java index 323a6ff3313..f6c59c85d51 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenIntrospectionEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenIntrospectionEndpoint.java @@ -16,26 +16,19 @@ */ package org.keycloak.protocol.oidc.endpoints; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.OAuthErrorException; -import org.keycloak.RSATokenVerifier; import org.keycloak.common.ClientConnection; -import org.keycloak.common.VerificationException; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; -import org.keycloak.jose.jws.JWSInputException; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.protocol.oidc.TokenManager; -import org.keycloak.protocol.oidc.TokenManager.TokenValidation; +import org.keycloak.protocol.oidc.AccessTokenIntrospectionProviderFactory; +import org.keycloak.protocol.oidc.TokenIntrospectionProvider; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; -import org.keycloak.representations.AccessToken; import org.keycloak.services.ErrorResponseException; -import org.keycloak.util.JsonSerialization; import javax.ws.rs.POST; import javax.ws.rs.core.Context; @@ -52,8 +45,6 @@ import javax.ws.rs.core.UriInfo; */ public class TokenIntrospectionEndpoint { - private static final String TOKEN_TYPE_ACCESS_TOKEN = "access_token"; - private static final String TOKEN_TYPE_REFRESH_TOKEN = "refresh_token"; private static final String PARAM_TOKEN_TYPE_HINT = "token_type_hint"; private static final String PARAM_TOKEN = "token"; @@ -72,12 +63,10 @@ public class TokenIntrospectionEndpoint { private ClientConnection clientConnection; private final RealmModel realm; - private final TokenManager tokenManager; private final EventBuilder event; - public TokenIntrospectionEndpoint(RealmModel realm, TokenManager tokenManager, EventBuilder event) { + public TokenIntrospectionEndpoint(RealmModel realm, EventBuilder event) { this.realm = realm; - this.tokenManager = tokenManager; this.event = event; } @@ -94,7 +83,7 @@ public class TokenIntrospectionEndpoint { String tokenTypeHint = formParams.getFirst(PARAM_TOKEN_TYPE_HINT); if (tokenTypeHint == null) { - tokenTypeHint = TOKEN_TYPE_ACCESS_TOKEN; + tokenTypeHint = AccessTokenIntrospectionProviderFactory.ACCESS_TOKEN_TYPE; } String token = formParams.getFirst(PARAM_TOKEN); @@ -103,39 +92,26 @@ public class TokenIntrospectionEndpoint { throw throwErrorResponseException(Errors.INVALID_REQUEST, "Token not provided.", Status.BAD_REQUEST); } + TokenIntrospectionProvider provider = this.session.getProvider(TokenIntrospectionProvider.class, tokenTypeHint); + + if (provider == null) { + throw throwErrorResponseException(Errors.INVALID_REQUEST, "Unsupported token type [" + tokenTypeHint + "].", Status.BAD_REQUEST); + } + try { - AccessToken toIntrospect = toAccessToken(tokenTypeHint, token); - ObjectNode tokenMetadata; - boolean active = tokenManager.isTokenValid(session, realm, toIntrospect); - if (active) { - tokenMetadata = JsonSerialization.createObjectNode(toIntrospect); - tokenMetadata.put("client_id", toIntrospect.getIssuedFor()); - tokenMetadata.put("username", toIntrospect.getPreferredUsername()); - } else { - tokenMetadata = JsonSerialization.createObjectNode(); - } - - tokenMetadata.put("active", active); + Response response = provider.introspect(token); this.event.success(); - return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).build(); + return response; + } catch (ErrorResponseException ere) { + throw ere; } catch (Exception e) { throw throwErrorResponseException(Errors.INVALID_REQUEST, "Failed to introspect token.", Status.BAD_REQUEST); } } - private AccessToken toAccessToken(String tokenTypeHint, String token) throws JWSInputException, OAuthErrorException { - if (TOKEN_TYPE_ACCESS_TOKEN.equals(tokenTypeHint)) { - return toAccessToken(token); - } else if (TOKEN_TYPE_REFRESH_TOKEN.equals(tokenTypeHint)) { - return this.tokenManager.toRefreshToken(this.realm, token); - } else { - throw throwErrorResponseException(Errors.INVALID_REQUEST, "Unsupported token type [" + tokenTypeHint + "].", Status.BAD_REQUEST); - } - } - private void authorizeClient() { try { ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient(); @@ -153,14 +129,6 @@ public class TokenIntrospectionEndpoint { } } - private AccessToken toAccessToken(String tokenString) { - try { - return RSATokenVerifier.toAccessToken(tokenString, realm.getPublicKey()); - } catch (VerificationException e) { - throw new ErrorResponseException("invalid_request", "Invalid token.", Status.UNAUTHORIZED); - } - } - private void checkSsl() { if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) { throw new ErrorResponseException("invalid_request", "HTTPS required", Status.FORBIDDEN); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java index f3f502dbef2..a7781debf88 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java @@ -20,12 +20,16 @@ package org.keycloak.protocol.oidc.installation; import org.keycloak.Config; import org.keycloak.authentication.ClientAuthenticator; import org.keycloak.authentication.ClientAuthenticatorFactory; +import org.keycloak.authorization.admin.AuthorizationService; import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.protocol.ClientInstallationProvider; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; import org.keycloak.services.managers.ClientManager; import org.keycloak.util.JsonSerialization; @@ -34,6 +38,7 @@ import javax.ws.rs.core.Response; import java.io.IOException; import java.net.URI; import java.util.Map; +import java.util.Set; /** * @author Bill Burke @@ -59,6 +64,9 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide Map adapterConfig = getClientCredentialsAdapterConfig(session, client); rep.setCredentials(adapterConfig); } + + configureAuthorizationSettings(session, client, rep); + String json = null; try { json = JsonSerialization.writeValueAsPrettyString(rep); @@ -143,4 +151,22 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide return MediaType.APPLICATION_JSON; } + private void configureAuthorizationSettings(KeycloakSession session, ClientModel client, ClientManager.InstallationAdapterConfig rep) { + if (new AuthorizationService(session, client, null).isEnabled()) { + PolicyEnforcerConfig enforcerConfig = new PolicyEnforcerConfig(); + + enforcerConfig.setEnforcementMode(null); + enforcerConfig.setPaths(null); + + rep.setEnforcerConfig(enforcerConfig); + + Set clientRoles = client.getRoles(); + + if (clientRoles.size() == 1) { + if (clientRoles.iterator().next().getName().equals(Constants.AUTHZ_UMA_PROTECTION)) { + rep.setUseResourceRoleMappings(null); + } + } + } + } } diff --git a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java index 282fd3186ab..40c1ac6d2ce 100755 --- a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java +++ b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java @@ -23,6 +23,8 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakTransaction; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; @@ -87,15 +89,46 @@ public class KeycloakSessionServletFilter implements Filter { try { filterChain.doFilter(servletRequest, servletResponse); } finally { - // KeycloakTransactionCommitter is responsible for committing the transaction, but if an exception is thrown it's not invoked and transaction - // should be rolled back - if (session.getTransaction() != null && session.getTransaction().isActive()) { - session.getTransaction().rollback(); + if (servletRequest.isAsyncStarted()) { + servletRequest.getAsyncContext().addListener(createAsyncLifeCycleListener(session)); + } else { + closeSession(session); + } + } + } + + private AsyncListener createAsyncLifeCycleListener(final KeycloakSession session) { + return new AsyncListener() { + @Override + public void onComplete(AsyncEvent event) { + closeSession(session); } - session.close(); - ResteasyProviderFactory.clearContextData(); + @Override + public void onTimeout(AsyncEvent event) { + closeSession(session); + } + + @Override + public void onError(AsyncEvent event) { + closeSession(session); + } + + @Override + public void onStartAsync(AsyncEvent event) { + } + }; + } + + private void closeSession(KeycloakSession session) { + // KeycloakTransactionCommitter is responsible for committing the transaction, but if an exception is thrown it's not invoked and transaction + // should be rolled back + if (session.getTransaction() != null && session.getTransaction().isActive()) { + session.getTransaction().rollback(); } + + session.close(); + ResteasyProviderFactory.clearContextData(); } @Override diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index a8120e96403..607123ecb3c 100644 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -35,6 +35,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper; import org.keycloak.representations.adapters.config.BaseRealmConfig; import org.keycloak.common.util.Time; +import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.services.ServicesLogger; @@ -207,6 +208,8 @@ public class ClientManager { protected Boolean publicClient; @JsonProperty("credentials") protected Map credentials; + @JsonProperty("policy-enforcer") + protected PolicyEnforcerConfig enforcerConfig; public Boolean isUseResourceRoleMappings() { return useResourceRoleMappings; @@ -247,6 +250,14 @@ public class ClientManager { public void setBearerOnly(Boolean bearerOnly) { this.bearerOnly = bearerOnly; } + + public PolicyEnforcerConfig getEnforcerConfig() { + return this.enforcerConfig; + } + + public void setEnforcerConfig(PolicyEnforcerConfig enforcerConfig) { + this.enforcerConfig = enforcerConfig; + } } diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 4ff55881839..3926447c998 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -47,7 +47,6 @@ import org.keycloak.representations.idm.OAuthClientRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.timer.TimerProvider; import java.util.Collections; import java.util.HashSet; @@ -120,6 +119,7 @@ public class RealmManager implements RealmImporter { setupAuthenticationFlows(realm); setupRequiredActions(realm); setupOfflineTokens(realm); + setupAuthorizationServices(realm); return realm; } @@ -489,6 +489,9 @@ public class RealmManager implements RealmImporter { for (final UserFederationProviderModel fedProvider : federationProviders) { usersSyncManager.notifyToRefreshPeriodicSync(session, realm, fedProvider, false); } + + setupAuthorizationServices(realm); + return realm; } @@ -581,4 +584,14 @@ public class RealmManager implements RealmImporter { return session.users().searchForUser(searchString.trim(), realmModel); } + private void setupAuthorizationServices(RealmModel realm) { + for (String roleName : Constants.AUTHZ_DEFAULT_AUTHORIZATION_ROLES) { + if (realm.getRole(roleName) == null) { + RoleModel role = realm.addRole(roleName); + role.setDescription("${role_" + roleName + "}"); + role.setScopeParamRequired(false); + realm.addDefaultRole(roleName); + } + } + } } diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 4f1c5f77afd..4161763ef10 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -18,6 +18,8 @@ package org.keycloak.services.resources; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.AuthorizationService; import org.keycloak.common.ClientConnection; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.events.EventBuilder; @@ -40,8 +42,12 @@ import javax.ws.rs.NotFoundException; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; -import javax.ws.rs.core.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; import java.net.URI; /** @@ -242,6 +248,17 @@ public class RealmsResource { return Cors.add(request, responseBuilder).allowedOrigins("*").build(); } + @Path("{realm}/authz") + public Object getAuthorizationService(@PathParam("realm") String name) { + init(name); + AuthorizationProvider authorization = this.session.getProvider(AuthorizationProvider.class); + AuthorizationService service = new AuthorizationService(authorization); + + ResteasyProviderFactory.getInstance().injectProperties(service); + + return service; + } + /** * A JAX-RS sub-resource locator that uses the {@link org.keycloak.services.resource.RealmResourceSPI} to resolve sub-resources instances given an unknownPath. * diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 08177b9887d..18023e87110 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -20,6 +20,7 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.authorization.admin.AuthorizationService; import org.keycloak.events.admin.OperationType; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; @@ -133,12 +134,18 @@ public class ClientResource { } } - public static void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException { + public void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException { if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) { new ClientManager(new RealmManager(session)).enableServiceAccount(client); } RepresentationToModel.updateClient(rep, client); + + if (TRUE.equals(rep.getAuthorizationServicesEnabled())) { + authorization().enable(); + } else { + authorization().disable(); + } } /** @@ -156,7 +163,11 @@ public class ClientResource { throw new NotFoundException("Could not find client"); } - return ModelToRepresentation.toRepresentation(client); + ClientRepresentation representation = ModelToRepresentation.toRepresentation(client); + + representation.setAuthorizationServicesEnabled(authorization().isEnabled()); + + return representation; } /** @@ -537,4 +548,12 @@ public class ClientResource { return result; } + @Path("/authz") + public AuthorizationService authorization() { + AuthorizationService resource = new AuthorizationService(this.session, this.client, this.auth); + + ResteasyProviderFactory.getInstance().injectProperties(resource); + + return resource; + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAuth.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAuth.java index 440241d6589..176c4803828 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAuth.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAuth.java @@ -31,7 +31,7 @@ public class RealmAuth { private Resource resource; public enum Resource { - CLIENT, USER, REALM, EVENTS, IDENTITY_PROVIDER, IMPERSONATION + CLIENT, USER, REALM, EVENTS, IDENTITY_PROVIDER, IMPERSONATION, AUTHORIZATION } private AdminAuth auth; @@ -89,6 +89,8 @@ public class RealmAuth { return AdminRoles.VIEW_EVENTS; case IDENTITY_PROVIDER: return AdminRoles.VIEW_IDENTITY_PROVIDERS; + case AUTHORIZATION: + return AdminRoles.VIEW_AUTHORIZATION; default: throw new IllegalStateException(); } @@ -108,6 +110,8 @@ public class RealmAuth { return AdminRoles.MANAGE_IDENTITY_PROVIDERS; case IMPERSONATION: return ImpersonationConstants.IMPERSONATION_ROLE; + case AUTHORIZATION: + return AdminRoles.MANAGE_AUTHORIZATION; default: throw new IllegalStateException(); } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authorization.AuthorizationProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.authorization.AuthorizationProviderFactory new file mode 100644 index 00000000000..3ffe34c19e1 --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.authorization.AuthorizationProviderFactory @@ -0,0 +1,19 @@ +# +# JBoss, Home of Professional Open Source. +# Copyright 2016 Red Hat, Inc., and individual 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. +# + +org.keycloak.authorization.DefaultAuthorizationProviderFactory \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.TokenIntrospectionProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.TokenIntrospectionProviderFactory new file mode 100644 index 00000000000..ae459237cf5 --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.TokenIntrospectionProviderFactory @@ -0,0 +1,21 @@ +# +# 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. +# +# + +org.keycloak.protocol.oidc.AccessTokenIntrospectionProviderFactory +org.keycloak.protocol.oidc.RefreshTokenIntrospectionProviderFactory +org.keycloak.authorization.protection.introspect.RPTIntrospectionProviderFactory \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory index afc490eb765..df3dd7ad03f 100644 --- a/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory @@ -15,4 +15,5 @@ # limitations under the License. # -org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory \ No newline at end of file +org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory +org.keycloak.authorization.config.UmaWellKnownProviderFactory \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java index 68a8f8db47b..2a9c643f3b9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java @@ -359,7 +359,7 @@ public class ClientTest extends AbstractAdminTest { Assert.assertNames(scopesResource.realmLevel().listAll(), "role1"); Assert.assertNames(scopesResource.realmLevel().listEffective(), "role1", "role2"); - Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access"); + Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll(), AccountRoles.VIEW_PROFILE); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective(), AccountRoles.VIEW_PROFILE); @@ -376,7 +376,7 @@ public class ClientTest extends AbstractAdminTest { Assert.assertNames(scopesResource.realmLevel().listAll()); Assert.assertNames(scopesResource.realmLevel().listEffective()); - Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", "role1", "role2"); + Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "role1", "role2"); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll()); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT); Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 19645a89434..bb231169f87 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -29,6 +29,7 @@ import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RoleMappingResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.events.admin.OperationType; +import org.keycloak.models.Constants; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.ErrorRepresentation; @@ -789,7 +790,7 @@ public class UserTest extends AbstractAdminTest { assertAdminEvents.clear(); RoleMappingResource roles = realm.users().get(userId).roles(); - assertNames(roles.realmLevel().listAll(), "user", "offline_access"); + assertNames(roles.realmLevel().listAll(), "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION); // Add realm roles List l = new LinkedList<>(); @@ -808,9 +809,9 @@ public class UserTest extends AbstractAdminTest { assertAdminEvents.assertEvent("test", OperationType.CREATE, AdminEventPaths.userClientRoleMappingsPath(userId, clientUuid), list); // List realm roles - assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite", "user", "offline_access"); + assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION); assertNames(roles.realmLevel().listAvailable(), "admin"); - assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child", "user", "offline_access"); + assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION); // List client roles assertNames(roles.clientLevel(clientUuid).listAll(), "client-role", "client-composite"); @@ -819,7 +820,7 @@ public class UserTest extends AbstractAdminTest { // Get mapping representation MappingsRepresentation all = roles.getAll(); - assertNames(all.getRealmMappings(), "realm-role", "realm-composite", "user", "offline_access"); + assertNames(all.getRealmMappings(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION); assertEquals(2, all.getClientMappings().size()); assertNames(all.getClientMappings().get("myclient").getMappings(), "client-role", "client-composite"); assertNames(all.getClientMappings().get("account").getMappings(), "manage-account", "view-profile"); @@ -829,7 +830,7 @@ public class UserTest extends AbstractAdminTest { roles.realmLevel().remove(Collections.singletonList(realmRoleRep)); assertAdminEvents.assertEvent("test", OperationType.DELETE, AdminEventPaths.userRealmRoleMappingsPath(userId), Collections.singletonList(realmRoleRep)); - assertNames(roles.realmLevel().listAll(), "realm-composite", "user", "offline_access"); + assertNames(roles.realmLevel().listAll(), "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION); // Remove client role RoleRepresentation clientRoleRep = realm.clients().get(clientUuid).roles().get("client-role").toRepresentation(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java index 988ecafbace..9ac45d0437c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RoleMappingResource; import org.keycloak.events.admin.OperationType; +import org.keycloak.models.Constants; import org.keycloak.models.RoleModel; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ClientRepresentation; @@ -430,7 +431,7 @@ public class GroupTest extends AbstractGroupTest { // List realm roles assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite"); - assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", "user"); + assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user"); assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child"); // List client roles diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java index 74b0516b106..900767fa86c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java @@ -122,7 +122,7 @@ public class TokenIntrospectionTest extends TestRealmKeycloakTest { EventRepresentation loginEvent = events.expectLogin().assertEvent(); String sessionId = loginEvent.getSessionId(); AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password"); - String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); + String tokenResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken()); ObjectMapper objectMapper = new ObjectMapper(); JsonNode jsonNode = objectMapper.readTree(tokenResponse); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 9208927bc54..78479d7feb5 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -27,6 +27,10 @@ "provider": "${keycloak.userSessionPersister.provider:jpa}" }, + "authorizationPersister": { + "provider": "${keycloak.authorization.provider:jpa}" + }, + "userCache": { "provider": "${keycloak.user.cache.provider:default}", "default" : { diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml index 362d749a289..d394e2a383f 100755 --- a/testsuite/integration/pom.xml +++ b/testsuite/integration/pom.xml @@ -29,6 +29,11 @@ Keycloak Integration TestSuite + + 1.8 + 1.8 + + org.bouncycastle @@ -400,6 +405,7 @@ jpa jpa jpa + jpa debug @@ -439,6 +445,7 @@ mongo mongo mongo + mongo ${keycloak.connectionsMongo.host} ${keycloak.connectionsMongo.port} ${keycloak.connectionsMongo.db} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java index 19da666fa97..6c10322aaf9 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java @@ -107,7 +107,7 @@ public class AddUserTest { List realmRoles = userResource.roles().realmLevel().listAll(); - assertRoles(realmRoles, "admin", "offline_access"); + assertRoles(realmRoles, "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION); List clients = realm.clients().findAll(); String accountId = null; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractAuthorizationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractAuthorizationTest.java new file mode 100644 index 00000000000..301adcae3a0 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractAuthorizationTest.java @@ -0,0 +1,127 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.junit.Before; +import org.junit.Rule; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakTransactionManager; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.AccessTokenResponse; + +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.Invocation; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MASTER; +import static org.keycloak.models.AdminRoles.ADMIN; +import static org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT; + +/** + * @author Pedro Igor + */ +public abstract class AbstractAuthorizationTest { + + protected static final String TEST_REALM_NAME = "photoz"; + + @Rule + public KeycloakAuthorizationServerRule keycloak = new KeycloakAuthorizationServerRule(TEST_REALM_NAME); + + private Keycloak adminClient; + + @Before + public void onBefore() { + adminClient = Keycloak.getInstance(AUTH_SERVER_ROOT, MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID); + } + + protected R onAuthorizationSession(Function function) { + KeycloakSession keycloakSession = startKeycloakSession(); + KeycloakTransactionManager transaction = keycloakSession.getTransaction(); + + try { + AuthorizationProvider authorizationProvider = keycloakSession.getProvider(AuthorizationProvider.class); + + R result = function.apply(authorizationProvider); + + transaction.commit(); + + return result; + } catch (Exception e) { + transaction.rollback(); + throw new RuntimeException(e); + } finally { + if (keycloakSession != null) { + keycloakSession.close(); + } + } + } + + protected void onAuthorizationSession(Consumer consumer) { + KeycloakSession keycloakSession = startKeycloakSession(); + KeycloakTransactionManager transaction = keycloakSession.getTransaction(); + + try { + AuthorizationProvider authorizationProvider = keycloakSession.getProvider(AuthorizationProvider.class); + + consumer.accept(authorizationProvider); + + transaction.commit(); + } catch (Exception e) { + transaction.rollback(); + throw new RuntimeException(e); + } finally { + if (keycloakSession != null) { + keycloakSession.close(); + } + } + } + + protected Invocation.Builder newClient(ClientModel client, String authzRelativePath) { + return ClientBuilder.newClient() + .register((ClientRequestFilter) requestContext -> { + AccessTokenResponse accessToken = adminClient.tokenManager().getAccessToken(); + requestContext.getHeaders().add("Authorization", "Bearer " + accessToken.getToken()); + }).target(AUTH_SERVER_ROOT + "/admin/realms/" + TEST_REALM_NAME + "/clients/" + client.getId() + "/authz" + authzRelativePath).request(); + } + + protected ClientModel getClientByClientId(String clientId) { + KeycloakSession session = this.keycloak.startSession(); + + try { + RealmModel realm = session.realms().getRealmByName(TEST_REALM_NAME); + return realm.getClientByClientId(clientId); + } finally { + session.close(); + } + } + + private KeycloakSession startKeycloakSession() { + KeycloakSession keycloakSession = this.keycloak.startSession(); + + keycloakSession.getContext().setRealm(keycloakSession.realms().getRealmByName(TEST_REALM_NAME)); + + return keycloakSession; + } +} 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 new file mode 100644 index 00000000000..8876f3088af --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java @@ -0,0 +1,370 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.apache.commons.collections.map.HashedMap; +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; +import org.keycloak.authorization.model.Resource; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.authorization.permission.ResourcePermission; +import org.keycloak.authorization.policy.evaluation.DefaultEvaluation; +import org.keycloak.authorization.store.PolicyStore; +import org.keycloak.authorization.store.ResourceServerStore; +import org.keycloak.authorization.store.ResourceStore; +import org.keycloak.authorization.store.ScopeStore; +import org.keycloak.authorization.store.StoreFactory; +import org.keycloak.common.ClientConnection; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.representations.AccessToken; +import org.keycloak.util.JsonSerialization; + +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import java.io.IOException; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Pedro Igor + */ +public abstract class AbstractPhotozAdminTest extends AbstractAuthorizationTest { + + protected ResourceServer resourceServer; + protected Resource adminResource; + protected Policy anyAdminPolicy; + protected Policy onlyFromSpecificAddressPolicy; + protected Policy administrationPolicy; + + protected Resource albumResource; + protected Policy anyUserPolicy; + + @Before + public void onBefore() { + super.onBefore(); + this.resourceServer = createResourceServer(); + this.adminResource = createAdminAlbumResource(); + this.anyAdminPolicy = createAnyAdminPolicy(); + this.onlyFromSpecificAddressPolicy = createOnlyFromSpecificAddressPolicy(); + this.administrationPolicy = createAdministrationPolicy(); + + this.albumResource = createAlbumResource(); + this.anyUserPolicy = createAnyUserPolicy(); + } + + protected ResourceServer createResourceServer() { + return onAuthorizationSession(authorizationProvider -> { + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore(); + + return resourceServerStore.create(getClientByClientId("photoz-restful-api").getId()); + }); + } + + protected Map performEvaluation(List permissions, AccessToken accessToken, ClientConnection clientConnection) { + Map evaluations = new HashedMap(); + + onAuthorizationSession(authorizationProvider -> { + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + + // during tests we create resource instances, but we need to reload them to get their collections updated + List updatedPermissions = permissions.stream().map(permission -> { + Resource resource = storeFactory.getResourceStore().findById(permission.getResource().getId()); + return new ResourcePermission(resource, permission.getScopes(), permission.getResourceServer()); + }).collect(Collectors.toList()); + + authorizationProvider.evaluators().from(updatedPermissions, createEvaluationContext(accessToken, clientConnection, authorizationProvider)).evaluate(new Decision() { + @Override + public void onDecision(DefaultEvaluation evaluation) { + evaluations.put(evaluation.getPolicy().getId(), evaluation); + } + + @Override + public void onError(Throwable cause) { + throw new RuntimeException("Permission evaluation failed.", cause); + } + }); + }); + + return evaluations; + } + + private KeycloakEvaluationContext createEvaluationContext(AccessToken accessToken, ClientConnection clientConnection, AuthorizationProvider authorizationProvider) { + KeycloakSession keycloakSession = authorizationProvider.getKeycloakSession(); + + keycloakSession.getContext().setConnection(clientConnection); + + keycloakSession.getContext().setClient(getClientByClientId("photoz-html5-client")); + + ResteasyProviderFactory.pushContext(HttpHeaders.class, createHttpHeaders()); + + KeycloakIdentity identity = new KeycloakIdentity(accessToken, keycloakSession); + + return new KeycloakEvaluationContext(identity, keycloakSession); + } + + protected AccessToken createAccessToken(Set roles) { + AccessToken accessToken = new AccessToken(); + + accessToken.setRealmAccess(new AccessToken.Access()); + accessToken.getRealmAccess().roles(roles); + + return accessToken; + } + + + private HttpHeaders createHttpHeaders() { + return new HttpHeaders() { + @Override + public List getRequestHeader(String name) { + return null; + } + + @Override + public String getHeaderString(String name) { + return null; + } + + @Override + public MultivaluedMap getRequestHeaders() { + return null; + } + + @Override + public List getAcceptableMediaTypes() { + return null; + } + + @Override + public List getAcceptableLanguages() { + return null; + } + + @Override + public MediaType getMediaType() { + return null; + } + + @Override + public Locale getLanguage() { + return null; + } + + @Override + public Map getCookies() { + return null; + } + + @Override + public Date getDate() { + return null; + } + + @Override + public int getLength() { + return 0; + } + }; + } + + protected ClientConnection createClientConnection(String remoteAddr) { + return new ClientConnection() { + @Override + public String getRemoteAddr() { + return remoteAddr; + } + + @Override + public String getRemoteHost() { + return "localhost"; + } + + @Override + public int getRemotePort() { + return 0; + } + + @Override + public String getLocalAddr() { + return null; + } + + @Override + public int getLocalPort() { + return 0; + } + }; + } + + protected Invocation.Builder newPermissionRequest(String... id) { + String idPathParam = ""; + + if (id.length != 0) { + idPathParam = "/" + id[0]; + } + + return newClient(getClientByClientId("photoz-restful-api"), "/resource-server/policy" + idPathParam); + } + + private Policy createAdministrationPolicy() { + return onAuthorizationSession(authorizationProvider -> { + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + Policy policy = policyStore.create("Administration Policy", "aggregate", resourceServer); + + policy.addAssociatedPolicy(anyAdminPolicy); + policy.addAssociatedPolicy(onlyFromSpecificAddressPolicy); + + return policy; + }); + } + + private Policy createOnlyFromSpecificAddressPolicy() { + return onAuthorizationSession(authorizationProvider -> { + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + Policy policy = policyStore.create("Only From a Specific Client Address", "js", resourceServer); + HashedMap config = new HashedMap(); + + config.put("code", + "var contextAttributes = $evaluation.getContext().getAttributes();" + + "var networkAddress = contextAttributes.getValue('kc.authz.context.client.network.ip_address');" + + "if ('127.0.0.1'.equals(networkAddress.asInetAddress(0).getHostAddress())) {" + + "$evaluation.grant();" + + "}"); + + policy.setConfig(config); + + return policy; + }); + } + + private Policy createAnyAdminPolicy() { + return onAuthorizationSession(authorizationProvider -> { + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + Policy policy = policyStore.create("Any Admin Policy", "role", resourceServer); + HashedMap config = new HashedMap(); + RealmModel realm = authorizationProvider.getKeycloakSession().realms().getRealmByName(TEST_REALM_NAME); + RoleModel adminRole = realm.getRole("admin"); + + try { + config.put("roles", JsonSerialization.writeValueAsString(new String[] {adminRole.getId()})); + } catch (IOException e) { + throw new RuntimeException(e); + } + + policy.setConfig(config); + + return policy; + }); + } + + private Resource createAdminAlbumResource() { + ResourceRepresentation representation = new ResourceRepresentation(); + + representation.setName("Admin Resources"); + representation.setType("http://photoz.com/admin"); + representation.setUri("/admin/*"); + + HashSet scopes = new HashSet<>(); + + scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:admin:manage")); + + representation.setScopes(scopes); + + return createResource(representation); + } + + private Resource createAlbumResource() { + ResourceRepresentation representation = new ResourceRepresentation(); + + representation.setName("Album Resource"); + representation.setType("http://photoz.com/album"); + representation.setUri("/album/*"); + + HashSet scopes = new HashSet<>(); + + scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:view")); + scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:create")); + scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:delete")); + + representation.setScopes(scopes); + + return createResource(representation); + } + + protected Resource createResource(ResourceRepresentation representation) { + return onAuthorizationSession(authorizationProvider -> { + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + ScopeStore scopeStore = storeFactory.getScopeStore(); + + representation.getScopes().forEach(scopeRepresentation -> { + scopeStore.create(scopeRepresentation.getName(), resourceServer); + }); + + ResourceStore resourceStore = storeFactory.getResourceStore(); + Resource albumResource = resourceStore.create(representation.getName(), resourceServer, resourceServer.getId()); + + albumResource.setType(representation.getType()); + albumResource.setUri(representation.getUri()); + albumResource.setIconUri(representation.getIconUri()); + + return albumResource; + }); + } + + private Policy createAnyUserPolicy() { + return onAuthorizationSession(authorizationProvider -> { + StoreFactory storeFactory = authorizationProvider.getStoreFactory(); + PolicyStore policyStore = storeFactory.getPolicyStore(); + Policy policy = policyStore.create("Any User Policy", "role", resourceServer); + HashedMap config = new HashedMap(); + RealmModel realm = authorizationProvider.getKeycloakSession().realms().getRealmByName(TEST_REALM_NAME); + RoleModel adminRole = realm.getRole("user"); + + try { + config.put("roles", JsonSerialization.writeValueAsString(new String[] {adminRole.getId()})); + } catch (IOException e) { + throw new RuntimeException(e); + } + + policy.setConfig(config); + + return policy; + }); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AttributeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AttributeTest.java new file mode 100644 index 00000000000..86e1ba0998c --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AttributeTest.java @@ -0,0 +1,73 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.apache.commons.collections.map.HashedMap; +import org.junit.Test; +import org.keycloak.authorization.attribute.Attributes; + +import java.net.InetAddress; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Map; +import java.util.function.Consumer; + +import static java.util.Arrays.asList; +import static org.junit.Assert.*; + +/** + * @author Pedro Igor + */ +public class AttributeTest { + + @Test + public void testManageAttributes() throws ParseException { + Map> map = new HashedMap(); + + map.put("integer", asList("1")); + map.put("long", asList("" + Long.MAX_VALUE)); + map.put("string", asList("some string")); + map.put("date", asList("12/12/2016")); + map.put("ip_network_address", asList("127.0.0.1")); + map.put("host_network_address", asList("localhost")); + map.put("multi_valued", asList("1", "2", "3", "4")); + + Attributes attributes = Attributes.from(map); + + map.keySet().forEach(new Consumer() { + @Override + public void accept(String name) { + assertTrue(attributes.exists(name)); + } + }); + + assertFalse(attributes.exists("not_found")); + assertTrue(attributes.containsValue("integer", "1")); + assertTrue(attributes.containsValue("multi_valued", "3")); + + assertEquals(1, attributes.getValue("multi_valued").asInt(0)); + assertEquals(4, attributes.getValue("multi_valued").asInt(3)); + + assertEquals(new SimpleDateFormat("dd/MM/yyyy").parse("12/12/2016"), attributes.getValue("date").asDate(0, "dd/MM/yyyy")); + + assertEquals(InetAddress.getLoopbackAddress(), attributes.getValue("ip_network_address").asInetAddress(0)); + assertEquals(InetAddress.getLoopbackAddress(), attributes.getValue("host_network_address").asInetAddress(0)); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/KeycloakAuthorizationServerRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/KeycloakAuthorizationServerRule.java new file mode 100644 index 00000000000..1df0b615ddb --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/KeycloakAuthorizationServerRule.java @@ -0,0 +1,47 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; + +/** + * @author Pedro Igor + */ +public class KeycloakAuthorizationServerRule extends AbstractKeycloakRule { + + private final String realmName; + + KeycloakAuthorizationServerRule(String realmName) { + this.realmName = realmName; + } + + @Override + protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) { + server.importRealm(getClass().getResourceAsStream("/authorization-test/test-" + realmName + "-realm.json")); + } + + @Override + protected String[] getTestRealms() { + return new String[] {this.realmName}; + } +} 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 new file mode 100644 index 00000000000..f323265a50b --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java @@ -0,0 +1,163 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.junit.Test; +import org.keycloak.authorization.admin.representation.ResourceRepresentation; +import org.keycloak.authorization.model.Resource; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import static org.junit.Assert.*; + +/** + * @author Pedro Igor + */ +public class ResourceManagementTest extends AbstractPhotozAdminTest { + + @Test + public void testCreate() throws Exception { + ResourceRepresentation newResource = new ResourceRepresentation(); + + newResource.setName("New Resource"); + newResource.setType("Resource Type"); + newResource.setIconUri("Resource Icon URI"); + newResource.setUri("Resource URI"); + + Response response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class); + + onAuthorizationSession(authorizationProvider -> { + Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId()); + + assertNotNull(resourceModel); + assertEquals(resource.getId(), resourceModel.getId()); + assertEquals("New Resource", resourceModel.getName()); + assertEquals("Resource Type", resourceModel.getType()); + assertEquals("Resource Icon URI", resourceModel.getIconUri()); + assertEquals("Resource URI", resourceModel.getUri()); + assertEquals(resourceServer.getClientId(), resourceModel.getOwner()); + assertEquals(resourceServer.getId(), resourceModel.getResourceServer().getId()); + }); + } + + @Test + public void testUpdate() throws Exception { + ResourceRepresentation newResource = new ResourceRepresentation(); + + newResource.setName("New Resource"); + newResource.setType("Resource Type"); + newResource.setIconUri("Resource Icon URI"); + newResource.setUri("Resource URI"); + + Response response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + newResource.setName("New Resource Changed"); + newResource.setType("Resource Type Changed"); + newResource.setIconUri("Resource Icon URI Changed"); + newResource.setUri("Resource URI Changed"); + + response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE)); + + ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class); + + onAuthorizationSession(authorizationProvider -> { + Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId()); + + assertNotNull(resourceModel); + assertEquals(resource.getId(), resourceModel.getId()); + assertEquals("New Resource Changed", resourceModel.getName()); + assertEquals("Resource Type Changed", resourceModel.getType()); + assertEquals("Resource Icon URI Changed", resourceModel.getIconUri()); + assertEquals("Resource URI Changed", resourceModel.getUri()); + assertEquals(resourceServer.getId(), resourceModel.getResourceServer().getId()); + }); + } + + @Test + public void testFindById() throws Exception { + ResourceRepresentation newResource = new ResourceRepresentation(); + + newResource.setName("New Resource"); + newResource.setType("Resource Type"); + newResource.setIconUri("Resource Icon URI"); + newResource.setUri("Resource URI"); + + Response response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class); + + response = newResourceRequest(resource.getId()).get(); + + assertEquals(Status.OK.getStatusCode(), response.getStatus()); + + resource = response.readEntity(ResourceRepresentation.class); + + assertEquals("New Resource", resource.getName()); + assertEquals("Resource Type", resource.getType()); + assertEquals("Resource Icon URI", resource.getIconUri()); + assertEquals("Resource URI", resource.getUri()); + } + + @Test + public void testDelete() throws Exception { + ResourceRepresentation newResource = new ResourceRepresentation(); + + newResource.setName("New Resource"); + + Response response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class); + + assertNotNull(resource.getId()); + + response = newResourceRequest(resource.getId()).delete(); + + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + + onAuthorizationSession(authorizationProvider -> { + Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId()); + + assertNull(resourceModel); + }); + } + + private Builder newResourceRequest(String... id) { + String idPathParam = ""; + + if (id.length != 0) { + idPathParam = "/" + id[0]; + } + + return newClient(getClientByClientId("photoz-restful-api"), "/resource-server/resource" + idPathParam); + } +} 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 new file mode 100644 index 00000000000..a4cc5513be5 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java @@ -0,0 +1,364 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.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.util.JsonSerialization; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.*; + +/** + * @author Pedro Igor + */ +public class ResourcePermissionManagementTest extends AbstractPhotozAdminTest { + + @Test + public void testCreateForTypeWithSinglePolicy() throws Exception { + PolicyRepresentation newPermission = new PolicyRepresentation(); + + newPermission.setName("Admin Resource Policy"); + newPermission.setType("resource"); + + HashedMap config = new HashedMap(); + + config.put("defaultResourceType", "http://photoz.com/admin"); + config.put("applyPolicies", JsonSerialization.writeValueAsString(new String[] {this.administrationPolicy.getId()})); + + newPermission.setConfig(config); + + Response response = newPermissionRequest().post(Entity.entity(newPermission, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class); + + onAuthorizationSession(authorizationProvider -> { + Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId()); + + assertNotNull(policyModel); + assertEquals(permission.getId(), policyModel.getId()); + assertEquals(newPermission.getName(), policyModel.getName()); + assertEquals(newPermission.getType(), policyModel.getType()); + assertEquals(resourceServer.getId(), policyModel.getResourceServer().getId()); + }); + + Set roles = new HashSet<>(); + + roles.add("admin"); + + Map evaluationsAdminRole = performEvaluation( + Arrays.asList(new ResourcePermission(adminResource, Collections.emptyList(), resourceServer)), + createAccessToken(roles), + createClientConnection("127.0.0.1")); + + assertEquals(1, evaluationsAdminRole.size()); + assertTrue(evaluationsAdminRole.containsKey(this.administrationPolicy.getId())); + assertEquals(Effect.PERMIT, evaluationsAdminRole.get(this.administrationPolicy.getId()).getEffect()); + + evaluationsAdminRole = performEvaluation( + Arrays.asList(new ResourcePermission(adminResource, Collections.emptyList(), resourceServer)), + createAccessToken(roles), + createClientConnection("127.0.0.10")); + + assertEquals(1, evaluationsAdminRole.size()); + assertTrue(evaluationsAdminRole.containsKey(this.administrationPolicy.getId())); + assertEquals(Effect.DENY, evaluationsAdminRole.get(this.administrationPolicy.getId()).getEffect()); + + roles.clear(); + roles.add("user"); + + Map evaluationsUserRole = performEvaluation( + Arrays.asList(new ResourcePermission(adminResource, Collections.emptyList(), resourceServer)), + createAccessToken(roles), + createClientConnection("127.0.0.1")); + + assertEquals(1, evaluationsUserRole.size()); + assertTrue(evaluationsUserRole.containsKey(this.administrationPolicy.getId())); + assertEquals(Effect.DENY, evaluationsUserRole.get(this.administrationPolicy.getId()).getEffect()); + } + + @Test + public void testCreateForTypeWithMultiplePolicies() throws Exception { + createAlbumResourceTypePermission(); + + HashSet roles = new HashSet<>(); + + roles.add("admin"); + + Map evaluationsAdminRole = performEvaluation( + Arrays.asList(new ResourcePermission(albumResource, Collections.emptyList(), resourceServer)), + createAccessToken(roles), + createClientConnection("127.0.0.1")); + + assertEquals(2, evaluationsAdminRole.size()); + assertTrue(evaluationsAdminRole.containsKey(this.administrationPolicy.getId())); + assertTrue(evaluationsAdminRole.containsKey(this.anyUserPolicy.getId())); + assertEquals(Effect.DENY, evaluationsAdminRole.get(this.anyUserPolicy.getId()).getEffect()); + assertEquals(Effect.PERMIT, evaluationsAdminRole.get(this.administrationPolicy.getId()).getEffect()); + + evaluationsAdminRole = performEvaluation( + Arrays.asList(new ResourcePermission(albumResource, Collections.emptyList(), resourceServer)), + createAccessToken(roles), + createClientConnection("127.0.0.10")); + + assertEquals(2, evaluationsAdminRole.size()); + assertTrue(evaluationsAdminRole.containsKey(this.administrationPolicy.getId())); + assertTrue(evaluationsAdminRole.containsKey(this.anyUserPolicy.getId())); + assertEquals(Effect.DENY, evaluationsAdminRole.get(this.anyUserPolicy.getId()).getEffect()); + assertEquals(Effect.DENY, evaluationsAdminRole.get(this.administrationPolicy.getId()).getEffect()); + + roles.clear(); + roles.add("user"); + + Map evaluationsUserRole = performEvaluation( + Arrays.asList(new ResourcePermission(albumResource, Collections.emptyList(), resourceServer)), + createAccessToken(roles), + createClientConnection("127.0.0.1")); + + assertEquals(2, evaluationsUserRole.size()); + assertTrue(evaluationsUserRole.containsKey(this.administrationPolicy.getId())); + assertTrue(evaluationsUserRole.containsKey(this.anyUserPolicy.getId())); + assertEquals(Effect.PERMIT, evaluationsUserRole.get(this.anyUserPolicy.getId()).getEffect()); + assertEquals(Effect.DENY, evaluationsUserRole.get(this.administrationPolicy.getId()).getEffect()); + } + + @Test + public void testUpdate() throws Exception { + PolicyRepresentation permission = createAlbumResourceTypePermission(); + Map config = permission.getConfig(); + + config.put("applyPolicies", JsonSerialization.writeValueAsString(new String[] {this.anyUserPolicy.getId()})); + + permission.setConfig(config); + + newPermissionRequest(permission.getId()).put(Entity.entity(permission, MediaType.APPLICATION_JSON_TYPE)); + + HashSet roles = new HashSet<>(); + + roles.add("admin"); + + Map evaluationsAdminRole = performEvaluation( + Arrays.asList(new ResourcePermission(albumResource, Collections.emptyList(), resourceServer)), + createAccessToken(roles), + createClientConnection("127.0.0.1")); + + assertEquals(1, evaluationsAdminRole.size()); + assertTrue(evaluationsAdminRole.containsKey(this.anyUserPolicy.getId())); + assertEquals(Effect.DENY, evaluationsAdminRole.get(this.anyUserPolicy.getId()).getEffect()); + } + + @Test + public void testDelete() throws Exception { + PolicyRepresentation newPermission = createAlbumResourceTypePermission(); + + Response delete = newPermissionRequest(newPermission.getId()).delete(); + + assertEquals(Status.NO_CONTENT.getStatusCode(), delete.getStatus()); + } + + @Test + public void testFindById() throws Exception { + PolicyRepresentation newPermission = createAlbumResourceTypePermission(); + + Response response = newPermissionRequest(newPermission.getId()).get(); + + PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class); + + assertEquals(newPermission.getId(), permission.getId()); + assertEquals(newPermission.getName(), permission.getName()); + assertEquals(newPermission.getType(), permission.getType()); + } + + @Test + public void testCreatePolicyForResource() throws Exception { + PolicyRepresentation newPermission = new PolicyRepresentation(); + + newPermission.setName("Multiple Resource Policy"); + newPermission.setType("resource"); + + HashedMap config = new HashedMap(); + + config.put("resources", JsonSerialization.writeValueAsString(new String[] {this.albumResource.getId(), this.adminResource.getId()})); + config.put("applyPolicies", JsonSerialization.writeValueAsString(new String[] {this.onlyFromSpecificAddressPolicy.getId()})); + + newPermission.setConfig(config); + + Response response = newPermissionRequest().post(Entity.entity(newPermission, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + List permissions = new ArrayList<>(); + + permissions.add(new ResourcePermission(this.albumResource, Collections.emptyList(), this.resourceServer)); + + Map evaluations = performEvaluation( + permissions, + createAccessToken(Collections.emptySet()), + createClientConnection("127.0.0.1")); + + assertEquals(1, evaluations.size()); + assertTrue(evaluations.containsKey(this.onlyFromSpecificAddressPolicy.getId())); + assertEquals(Effect.PERMIT, evaluations.get(this.onlyFromSpecificAddressPolicy.getId()).getEffect()); + + permissions = new ArrayList<>(); + + permissions.add(new ResourcePermission(this.adminResource, Collections.emptyList(), this.resourceServer)); + + evaluations = performEvaluation( + permissions, + createAccessToken(Collections.emptySet()), + createClientConnection("127.0.0.1")); + + assertEquals(1, evaluations.size()); + assertTrue(evaluations.containsKey(this.onlyFromSpecificAddressPolicy.getId())); + assertEquals(Effect.PERMIT, evaluations.get(this.onlyFromSpecificAddressPolicy.getId()).getEffect()); + + permissions = new ArrayList<>(); + + permissions.add(new ResourcePermission(this.adminResource, Collections.emptyList(), this.resourceServer)); + permissions.add(new ResourcePermission(this.albumResource, Collections.emptyList(), this.resourceServer)); + + evaluations = performEvaluation( + permissions, + createAccessToken(Collections.emptySet()), + createClientConnection("127.0.0.1")); + + assertEquals(1, evaluations.size()); + assertTrue(evaluations.containsKey(this.onlyFromSpecificAddressPolicy.getId())); + assertEquals(Effect.PERMIT, evaluations.get(this.onlyFromSpecificAddressPolicy.getId()).getEffect()); + + permissions = new ArrayList<>(); + + permissions.add(new ResourcePermission(this.adminResource, Collections.emptyList(), this.resourceServer)); + permissions.add(new ResourcePermission(this.albumResource, Collections.emptyList(), this.resourceServer)); + + evaluations = performEvaluation( + permissions, + createAccessToken(Collections.emptySet()), + createClientConnection("127.0.0.10")); + + assertEquals(1, evaluations.size()); + assertTrue(evaluations.containsKey(this.onlyFromSpecificAddressPolicy.getId())); + assertEquals(Effect.DENY, evaluations.get(this.onlyFromSpecificAddressPolicy.getId()).getEffect()); + } + + /** + * Tests if a resource can inherit the policies defined for another resource based on its type + * + * @throws Exception + */ + @Test + public void testInheritPoliciesBasedOnResourceType() throws Exception { + createAlbumResourceTypePermission(); + ResourceRepresentation representation = new ResourceRepresentation(); + + representation.setName("Alice Family Album"); + representation.setType(this.albumResource.getType()); + + Resource resource = createResource(representation); + + Set roles = new HashSet<>(); + + roles.add("user"); + + Map evaluationsUserRole = performEvaluation( + Arrays.asList(new ResourcePermission(resource, Collections.emptyList(), resourceServer)), + createAccessToken(roles), + createClientConnection("127.0.0.1")); + + assertEquals(2, evaluationsUserRole.size()); + assertTrue(evaluationsUserRole.containsKey(this.administrationPolicy.getId())); + assertTrue(evaluationsUserRole.containsKey(this.anyUserPolicy.getId())); + assertEquals(Effect.PERMIT, evaluationsUserRole.get(this.anyUserPolicy.getId()).getEffect()); + assertEquals(Effect.DENY, evaluationsUserRole.get(this.administrationPolicy.getId()).getEffect()); + + ResourceRepresentation someResourceRep = new ResourceRepresentation(); + + someResourceRep.setName("Some Resource"); + someResourceRep.setType("Some non-existent type"); + + Resource someResource = createResource(someResourceRep); + + evaluationsUserRole = performEvaluation( + Arrays.asList(new ResourcePermission(someResource, Collections.emptyList(), resourceServer)), + createAccessToken(roles), + createClientConnection("127.0.0.1")); + + // no policies can be applied given that there is no policy defined for this resource or its type + assertEquals(0, evaluationsUserRole.size()); + } + + private PolicyRepresentation createAlbumResourceTypePermission() throws Exception { + PolicyRepresentation newPermission = new PolicyRepresentation(); + + newPermission.setName("Album Resource Policy"); + newPermission.setType("resource"); + newPermission.setDecisionStrategy(Policy.DecisionStrategy.AFFIRMATIVE); + + HashedMap config = new HashedMap(); + + config.put("defaultResourceType", albumResource.getType()); + + String applyPolicies = JsonSerialization.writeValueAsString(new String[]{this.anyUserPolicy.getId(), this.administrationPolicy.getId()}); + + config.put("applyPolicies", applyPolicies); + + newPermission.setConfig(config); + + Response response = newPermissionRequest().post(Entity.entity(newPermission, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class); + + onAuthorizationSession(authorizationProvider -> { + Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId()); + + assertNotNull(policyModel); + assertEquals(permission.getId(), policyModel.getId()); + assertEquals(permission.getName(), policyModel.getName()); + assertEquals(permission.getType(), policyModel.getType()); + assertTrue(permission.getConfig().containsValue(albumResource.getType())); + assertTrue(permission.getConfig().containsValue(applyPolicies)); + assertEquals(resourceServer.getId(), policyModel.getResourceServer().getId()); + }); + + return permission; + } +} 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 new file mode 100644 index 00000000000..839a813f2e0 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java @@ -0,0 +1,148 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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.authorization; + +import org.junit.Test; +import org.keycloak.authorization.admin.representation.ScopeRepresentation; +import org.keycloak.authorization.model.Scope; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import static org.junit.Assert.*; + +/** + * @author Pedro Igor + */ +public class ScopeManagementTest extends AbstractPhotozAdminTest { + + @Test + public void testCreate() throws Exception { + ScopeRepresentation newScope = new ScopeRepresentation(); + + newScope.setName("New Scope"); + newScope.setIconUri("Icon URI"); + + Response response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class); + + onAuthorizationSession(authorizationProvider -> { + Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId()); + + assertNotNull(scopeModel); + assertEquals(scope.getId(), scopeModel.getId()); + assertEquals("New Scope", scopeModel.getName()); + assertEquals("Icon URI", scopeModel.getIconUri()); + assertEquals(resourceServer.getId(), scopeModel.getResourceServer().getId()); + }); + } + + @Test + public void testUpdate() throws Exception { + ScopeRepresentation newScope = new ScopeRepresentation(); + + newScope.setName("New Scope"); + newScope.setIconUri("Icon URI"); + + Response response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + newScope.setName("New Scope Changed"); + newScope.setIconUri("Icon URI Changed"); + + response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE)); + + ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class); + + onAuthorizationSession(authorizationProvider -> { + Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId()); + + assertNotNull(scopeModel); + assertEquals(scope.getId(), scopeModel.getId()); + assertEquals("New Scope Changed", scopeModel.getName()); + assertEquals("Icon URI Changed", scopeModel.getIconUri()); + assertEquals(resourceServer.getId(), scopeModel.getResourceServer().getId()); + }); + } + + @Test + public void testFindById() throws Exception { + ScopeRepresentation newScope = new ScopeRepresentation(); + + newScope.setName("New Scope"); + newScope.setIconUri("Icon URI"); + + Response response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class); + + response = newScopeRequest(scope.getId()).get(); + + assertEquals(Status.OK.getStatusCode(), response.getStatus()); + + scope = response.readEntity(ScopeRepresentation.class); + + assertEquals("New Scope", scope.getName()); + assertEquals("Icon URI", scope.getIconUri()); + } + + @Test + public void testDelete() throws Exception { + ScopeRepresentation newScope = new ScopeRepresentation(); + + newScope.setName("New Scope"); + + Response response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE)); + + assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); + + ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class); + + assertNotNull(scope.getId()); + + response = newScopeRequest(scope.getId()).delete(); + + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + + onAuthorizationSession(authorizationProvider -> { + Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId()); + + assertNull(scopeModel); + }); + } + + private Builder newScopeRequest(String... id) { + String idPathParam = ""; + + if (id.length != 0) { + idPathParam = "/" + id[0]; + } + + return newClient(getClientByClientId("photoz-restful-api"), "/resource-server/scope" + idPathParam); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java index 27b550a31c0..f3403794dea 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java @@ -86,7 +86,7 @@ public class AdapterTest extends AbstractModelTest { Assert.assertEquals(realmModel.getName(), "JUGGLER"); Assert.assertArrayEquals(realmModel.getPrivateKey().getEncoded(), keyPair.getPrivate().getEncoded()); Assert.assertArrayEquals(realmModel.getPublicKey().getEncoded(), keyPair.getPublic().getEncoded()); - Assert.assertEquals(2, realmModel.getDefaultRoles().size()); + Assert.assertEquals(3, realmModel.getDefaultRoles().size()); Assert.assertTrue(realmModel.getDefaultRoles().contains("foo")); } @@ -112,7 +112,7 @@ public class AdapterTest extends AbstractModelTest { Assert.assertEquals(realmModel.getName(), "JUGGLER"); Assert.assertArrayEquals(realmModel.getPrivateKey().getEncoded(), keyPair.getPrivate().getEncoded()); Assert.assertArrayEquals(realmModel.getPublicKey().getEncoded(), keyPair.getPublic().getEncoded()); - Assert.assertEquals(2, realmModel.getDefaultRoles().size()); + Assert.assertEquals(3, realmModel.getDefaultRoles().size()); Assert.assertTrue(realmModel.getDefaultRoles().contains("foo")); realmModel.getId(); @@ -462,7 +462,7 @@ public class AdapterTest extends AbstractModelTest { realmModel.addRole("admin"); realmModel.addRole("user"); Set roles = realmModel.getRoles(); - Assert.assertEquals(4, roles.size()); + Assert.assertEquals(5, roles.size()); UserModel user = realmManager.getSession().users().addUser(realmModel, "bburke"); RoleModel realmUserRole = realmModel.getRole("user"); user.grantRole(realmUserRole); @@ -488,7 +488,7 @@ public class AdapterTest extends AbstractModelTest { user.grantRole(application.getRole("user")); roles = user.getRealmRoleMappings(); - Assert.assertEquals(roles.size(), 3); + Assert.assertEquals(4, roles.size()); assertRolesContains(realmUserRole, roles); Assert.assertTrue(user.hasRole(realmUserRole)); // Role "foo" is default realm role @@ -503,13 +503,13 @@ public class AdapterTest extends AbstractModelTest { // Test that application role 'user' don't clash with realm role 'user' Assert.assertNotEquals(realmModel.getRole("user").getId(), application.getRole("user").getId()); - Assert.assertEquals(7, user.getRoleMappings().size()); + Assert.assertEquals(8, user.getRoleMappings().size()); // Revoke some roles user.deleteRoleMapping(realmModel.getRole("foo")); user.deleteRoleMapping(appBarRole); roles = user.getRoleMappings(); - Assert.assertEquals(5, roles.size()); + Assert.assertEquals(6, roles.size()); assertRolesContains(realmUserRole, roles); assertRolesContains(application.getRole("user"), roles); Assert.assertFalse(user.hasRole(appBarRole)); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java index 136fc691750..547b58356b7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java @@ -99,7 +99,7 @@ public class ImportTest extends AbstractModelTest { Assert.assertEquals(1, creds.size()); RequiredCredentialModel cred = creds.get(0); Assert.assertEquals("password", cred.getFormLabel()); - Assert.assertEquals(3, realm.getDefaultRoles().size()); + Assert.assertEquals(4, realm.getDefaultRoles().size()); Assert.assertNotNull(realm.getRole("foo")); Assert.assertNotNull(realm.getRole("bar")); diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json index 1c63a35f8bb..16ded246e31 100755 --- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json @@ -26,6 +26,10 @@ "provider": "${keycloak.userSessionPersister.provider:jpa}" }, + "authorizationPersister": { + "provider": "${keycloak.authorization.provider:jpa}" + }, + "userCache": { "default" : { "enabled": true diff --git a/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json b/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json new file mode 100644 index 00000000000..e23c9a4427a --- /dev/null +++ b/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json @@ -0,0 +1,161 @@ +{ + "realm": "photoz", + "enabled": true, + "accessTokenLifespan": 60, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "ssoSessionIdleTimeout": 600, + "ssoSessionMaxLifespan": 36000, + "sslRequired": "external", + "registrationAllowed": false, + "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" + ], + "clientRoles": { + "photoz-html5-client": [ + "uma_authorization", + "kc_entitlement" + ] + } + }, + { + "username": "jdoe", + "enabled": true, + "email": "jdoe@keycloak.org", + "firstName": "John", + "lastName": "Doe", + "credentials": [ + { + "type": "password", + "value": "jdoe" + } + ], + "realmRoles": [ + "user" + ], + "clientRoles": { + "photoz-html5-client": [ + "uma_authorization", + "kc_entitlement" + ] + } + }, + { + "username": "admin", + "enabled": true, + "email": "admin@admin.com", + "firstName": "Admin", + "lastName": "Istrator", + "credentials": [ + { + "type": "password", + "value": "admin" + } + ], + "realmRoles": [ + "user", + "admin" + ], + "clientRoles": { + "realm-management": [ + "realm-admin" + ], + "photoz-html5-client": [ + "uma_authorization", + "kc_entitlement" + ] + } + }, + { + "username": "service-account-photoz-restful-api", + "enabled": true, + "email": "service-account-photoz-restful-api@placeholder.org", + "serviceAccountClientId": "photoz-restful-api", + "realmRoles": [ + "uma_protection" + ] + } + ], + "roles": { + "realm": [ + { + "name": "user", + "description": "User privileges" + }, + { + "name": "admin", + "description": "Administrator privileges" + }, + { + "name": "uma_protection", + "description": "Allows access to the Protection API" + } + ] + }, + "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", + "enabled": true, + "publicClient": false, + "baseUrl": "/photoz-restful-api", + "bearerOnly": false, + "serviceAccountsEnabled": true, + "redirectUris": [ + "/photoz-restful-api/*" + ], + "secret": "secret" + }, + { + "clientId": "public-client-01", + "enabled": true, + "publicClient": true, + "baseUrl": "public-client-01", + "redirectUris": [ + "/public-client-01/*" + ] + }, + { + "clientId": "confidential-no-service-account", + "enabled": true, + "publicClient": false, + "baseUrl": "/confidential-no-service-account", + "bearerOnly": false, + "serviceAccountsEnabled": false, + "redirectUris": [ + "/confidential-no-service-account/*" + ], + "secret": "secret" + } + ] +} \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 8c0727fd79b..6c5ac3cc5f3 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -53,6 +53,7 @@ role_view-profile=View profile role_manage-account=Manage account role_read-token=Read token role_offline-access=Offline access +role_uma_authorization=Obtain permissions client_account=Account client_security-admin-console=Security Admin Console client_admin-cli=Admin CLI diff --git a/themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties b/themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties index e2e874366aa..8b07e7bfc42 100644 --- a/themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties @@ -53,6 +53,7 @@ role_view-profile=Visualiza perfil role_manage-account=Gerencia conta role_read-token=L\u00EA token role_offline-access=Acesso Offline +role_uma_authorization=Obter permiss\u00F5es client_account=Conta client_security-admin-console=Console de Administra\u00E7\u00E3o de Seguran\u00E7a client_admin-cli=Admin CLI diff --git a/themes/src/main/resources/theme/base/admin/index.ftl b/themes/src/main/resources/theme/base/admin/index.ftl index 9f5f94aa064..782e089d2f5 100755 --- a/themes/src/main/resources/theme/base/admin/index.ftl +++ b/themes/src/main/resources/theme/base/admin/index.ftl @@ -50,6 +50,14 @@ + + + + + + + + diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js new file mode 100644 index 00000000000..467038c95cc --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js @@ -0,0 +1,412 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2016 Red Hat, Inc., and individual 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. + */ + +module.requires.push('ui.ace'); + +module.config(['$routeProvider', function ($routeProvider) { + $routeProvider + .when('/realms/:realm/authz', { + templateUrl: resourceUrl + '/partials/authz/resource-server-list.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + } + }, + controller: 'ResourceServerCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/create', { + templateUrl: resourceUrl + '/partials/authz/resource-server-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + }, + clients: function (ClientListLoader) { + return ClientListLoader(); + } + }, + controller: 'ResourceServerDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server', { + templateUrl: resourceUrl + '/partials/authz/resource-server-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + }, + clients: function (ClientListLoader) { + return ClientListLoader(); + }, + serverInfo: function (ServerInfoLoader) { + return ServerInfoLoader(); + } + }, + controller: 'ResourceServerDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/evaluate', { + templateUrl: resourceUrl + '/partials/authz/policy/resource-server-policy-evaluate.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + }, + clients: function (ClientListLoader) { + return ClientListLoader(); + }, + roles: function (RoleListLoader) { + return new RoleListLoader(); + } + }, + controller: 'PolicyEvaluateCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/evaluate/result', { + templateUrl: resourceUrl + '/partials/authz/policy/resource-server-policy-evaluate-result.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + }, + }, + controller: 'PolicyEvaluateCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/resource', { + templateUrl: resourceUrl + '/partials/authz/resource-server-resource-list.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerResourceCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/resource/create', { + templateUrl: resourceUrl + '/partials/authz/resource-server-resource-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerResourceDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/resource/:rsrid', { + templateUrl: resourceUrl + '/partials/authz/resource-server-resource-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerResourceDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/scope', { + templateUrl: resourceUrl + '/partials/authz/resource-server-scope-list.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerScopeCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/scope/create', { + templateUrl: resourceUrl + '/partials/authz/resource-server-scope-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerScopeDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/scope/:id', { + templateUrl: resourceUrl + '/partials/authz/resource-server-scope-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerScopeDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/permission', { + templateUrl: resourceUrl + '/partials/authz/permission/resource-server-permission-list.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPermissionCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy', { + templateUrl: resourceUrl + '/partials/authz/policy/resource-server-policy-list.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/drools/create', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-drools-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyDroolsDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/drools/:id', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-drools-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyDroolsDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/permission/resource/create', { + templateUrl: resourceUrl + '/partials/authz/permission/provider/resource-server-policy-resource-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyResourceDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/permission/resource/:id', { + templateUrl: resourceUrl + '/partials/authz/permission/provider/resource-server-policy-resource-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyResourceDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/permission/scope/create', { + templateUrl: resourceUrl + '/partials/authz/permission/provider/resource-server-policy-scope-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyScopeDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/permission/scope/:id', { + templateUrl: resourceUrl + '/partials/authz/permission/provider/resource-server-policy-scope-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyScopeDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/user/create', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-user-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyUserDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/user/:id', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-user-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyUserDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/role/create', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-role-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyRoleDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/role/:id', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-role-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyRoleDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-js-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyJSDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/:id', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-js-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyJSDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/time/create', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-time-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyTimeDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/time/:id', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-time-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyTimeDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/aggregate/create', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyAggregateDetailCtrl' + }).when('/realms/:realm/clients/:client/authz/resource-server/policy/aggregate/:id', { + templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html', + resolve: { + realm: function (RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + } + }, + controller: 'ResourceServerPolicyAggregateDetailCtrl' + }); +}]); + +module.directive('kcTabsResourceServer', function () { + return { + scope: true, + restrict: 'E', + replace: true, + templateUrl: resourceUrl + '/templates/authz/kc-tabs-resource-server.html' + } +}); + +module.filter('unique', function () { + + return function (items, filterOn) { + + if (filterOn === false) { + return items; + } + + if ((filterOn || angular.isUndefined(filterOn)) && angular.isArray(items)) { + var hashCheck = {}, newItems = []; + + var extractValueToCompare = function (item) { + if (angular.isObject(item) && angular.isString(filterOn)) { + return item[filterOn]; + } else { + return item; + } + }; + + angular.forEach(items, function (item) { + var valueToCheck, isDuplicate = false; + + for (var i = 0; i < newItems.length; i++) { + if (angular.equals(extractValueToCompare(newItems[i]), extractValueToCompare(item))) { + isDuplicate = true; + break; + } + } + if (!isDuplicate) { + newItems.push(item); + } + + }); + items = newItems; + } + return items; + }; +}); + +module.filter('toCamelCase', function () { + return function (input) { + input = input || ''; + return input.replace(/\w\S*/g, function (txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + }); + }; +}) \ No newline at end of file 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 new file mode 100644 index 00000000000..e07ed1af542 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js @@ -0,0 +1,1205 @@ +module.controller('ResourceServerCtrl', function($scope, realm, ResourceServer) { + $scope.realm = realm; + + ResourceServer.query({realm : realm.realm}, function (data) { + $scope.servers = data; + }); +}); + +module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $location, $upload, realm, ResourceServer, client, AuthzDialog, Notifications) { + $scope.realm = realm; + $scope.client = client; + + ResourceServer.get({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + $scope.server = angular.copy(data); + $scope.changed = false; + + $scope.$watch('server', function() { + if (!angular.equals($scope.server, data)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + ResourceServer.update({realm : realm.realm, client : $scope.server.clientId}, $scope.server, function() { + $route.reload(); + Notifications.success("The resource server has been created."); + }); + } + + $scope.reset = function() { + $scope.server = angular.copy(data); + $scope.changed = false; + } + + $scope.export = function() { + $scope.exportSettings = true; + ResourceServer.settings({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + var tmp = angular.fromJson(data); + $scope.settings = angular.toJson(tmp, true); + }) + } + + $scope.downloadSettings = function() { + saveAs(new Blob([$scope.settings], { type: 'application/json' }), $scope.server.name + "-authz-config.json"); + } + + $scope.cancelExport = function() { + delete $scope.settings + } + + $scope.onFileSelect = function($files) { + $scope.files = $files; + }; + + $scope.clearFileSelect = function() { + $scope.files = null; + } + + $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."); + }); + } + }; + }); +}); + +module.controller('ResourceServerResourceCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerResource, client) { + $scope.realm = realm; + $scope.client = client; + + ResourceServer.get({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + $scope.server = data; + + $scope.createPolicy = function(resource) { + $location.path('/realms/' + $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/permission/resource/create').search({rsrid: resource._id}); + } + + ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) { + $scope.resources = data; + }); + }); +}); + +module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $route, $location, realm, ResourceServer, client, ResourceServerResource, ResourceServerScope, AuthzDialog, Notifications) { + $scope.realm = realm; + $scope.client = client; + + ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) { + $scope.scopes = data; + }); + + ResourceServer.get({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + $scope.server = data; + + var resourceId = $route.current.params.rsrid; + + if (!resourceId) { + $scope.create = true; + $scope.changed = false; + + var resource = {}; + resource.scopes = []; + + $scope.resource = angular.copy(resource); + + $scope.$watch('resource', function() { + if (!angular.equals($scope.resource, resource)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + ResourceServerResource.save({realm : realm.realm, client : $scope.client.id}, $scope.resource, function(data) { + $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource/" + data._id); + Notifications.success("The resource has been created."); + }); + } + + $scope.cancel = function() { + $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource/"); + } + } else { + ResourceServerResource.get({ + realm : $route.current.params.realm, + client : client.id, + rsrid : $route.current.params.rsrid, + }, function(data) { + $scope.resource = angular.copy(data); + $scope.changed = false; + + for (i = 0; i < $scope.resource.scopes.length; i++) { + $scope.resource.scopes[i] = $scope.resource.scopes[i].name; + } + + $scope.$watch('resource', function() { + if (!angular.equals($scope.resource, data)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + ResourceServerResource.update({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, $scope.resource, function() { + $route.reload(); + Notifications.success("The resource has been updated."); + }); + } + + $scope.remove = function() { + var msg = ""; + + if ($scope.resource.policies.length > 0) { + msg = "

This resource is referenced in some policies:

"; + msg += "
    "; + for (i = 0; i < $scope.resource.policies.length; i++) { + msg+= "
  • " + $scope.resource.policies[i].name + "
  • "; + } + msg += "
"; + msg += "

If you remove this resource, the policies above will be affected and will not be associated with this resource anymore.

"; + } + + AuthzDialog.confirmDeleteWithMsg($scope.resource.name, "Resource", msg, function() { + ResourceServerResource.delete({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, null, function() { + $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource"); + Notifications.success("The resource has been deleted."); + }); + }); + } + + $scope.reset = function() { + $scope.resource = angular.copy(data); + $scope.changed = false; + } + }); + } + }); +}); + +module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerScope, client) { + $scope.realm = realm; + $scope.client = client; + + ResourceServer.get({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + $scope.server = data; + + ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) { + $scope.scopes = data; + }); + }); +}); + +module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $route, $location, realm, ResourceServer, client, ResourceServerScope, AuthzDialog, Notifications) { + $scope.realm = realm; + $scope.client = client; + + ResourceServer.get({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + $scope.server = data; + + var scopeId = $route.current.params.id; + + if (!scopeId) { + $scope.create = true; + $scope.changed = false; + + var scope = {}; + + $scope.resource = angular.copy(scope); + + $scope.$watch('scope', function() { + if (!angular.equals($scope.scope, scope)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + ResourceServerScope.save({realm : realm.realm, client : $scope.client.id}, $scope.scope, function(data) { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/scope/" + data.id); + Notifications.success("The scope has been created."); + }); + } + } else { + ResourceServerScope.get({ + realm : $route.current.params.realm, + client : client.id, + id : $route.current.params.id, + }, function(data) { + $scope.scope = angular.copy(data); + $scope.changed = false; + + $scope.$watch('scope', function() { + if (!angular.equals($scope.scope, data)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + ResourceServerScope.update({realm : realm.realm, client : $scope.client.id, id : $scope.scope.id}, $scope.scope, function() { + $scope.changed = false; + Notifications.success("The scope has been updated."); + }); + } + + $scope.remove = function() { + var msg = ""; + + if ($scope.scope.policies.length > 0) { + msg = "

This resource is referenced in some policies:

"; + msg += "
    "; + for (i = 0; i < $scope.scope.policies.length; i++) { + msg+= "
  • " + $scope.scope.policies[i].name + "
  • "; + } + msg += "
"; + msg += "

If you remove this resource, the policies above will be affected and will not be associated with this resource anymore.

"; + } + + AuthzDialog.confirmDeleteWithMsg($scope.scope.name, "Scope", msg, function() { + ResourceServerScope.delete({realm : realm.realm, client : $scope.client.id, id : $scope.scope.id}, null, function() { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/scope"); + Notifications.success("The scope has been deleted."); + }); + }); + } + + $scope.reset = function() { + $scope.scope = angular.copy(data); + $scope.changed = false; + } + }); + } + }); +}); + +module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPolicy, PolicyProvider, client) { + $scope.realm = realm; + $scope.client = client; + $scope.policyProviders = []; + + PolicyProvider.query({ + realm : $route.current.params.realm, + client : client.id + }, function (data) { + for (i = 0; i < data.length; i++) { + if (data[i].type != 'resource' && data[i].type != 'scope') { + $scope.policyProviders.push(data[i]); + } + } + }); + + ResourceServer.get({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + $scope.server = data; + + ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) { + $scope.policies = []; + + for (i = 0; i < data.length; i++) { + if (data[i].type != 'resource' && data[i].type != 'scope') { + $scope.policies.push(data[i]); + } + } + }); + }); + + $scope.addPolicy = function(policyType) { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy/" + policyType.type + "/create"); + } +}); + +module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPolicy, PolicyProvider, client) { + $scope.realm = realm; + $scope.client = client; + $scope.policyProviders = []; + + PolicyProvider.query({ + realm : $route.current.params.realm, + client : client.id + }, function (data) { + for (i = 0; i < data.length; i++) { + if (data[i].type == 'resource' || data[i].type == 'scope') { + $scope.policyProviders.push(data[i]); + } + } + }); + + ResourceServer.get({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + $scope.server = data; + + ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) { + $scope.policies = []; + + for (i = 0; i < data.length; i++) { + if (data[i].type == 'resource' || data[i].type == 'scope') { + $scope.policies.push(data[i]); + } + } + }); + }); + + $scope.addPolicy = function(policyType) { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission/" + policyType.type + "/create"); + } +}); + +module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http, $route, realm, client, PolicyController) { + PolicyController.onInit({ + getPolicyType : function() { + return "drools"; + }, + + onInit : function() { + $scope.drools = {}; + + $scope.resolveModules = function(policy) { + if (!policy) { + policy = $scope.policy; + } + + $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/drools/resolveModules' + , policy).success(function(data) { + $scope.drools.moduleNames = data; + $scope.resolveSessions(); + }); + } + + $scope.resolveSessions = function() { + $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/drools/resolveSessions' + , $scope.policy).success(function(data) { + $scope.drools.moduleSessions = data; + }); + } + }, + + onInitUpdate : function(policy) { + policy.config.scannerPeriod = parseInt(policy.config.scannerPeriod); + $scope.resolveModules(policy); + }, + + onUpdate : function() { + $scope.policy.config.resources = JSON.stringify($scope.policy.config.resources); + }, + + onInitCreate : function(newPolicy) { + newPolicy.config.scannerPeriod = 1; + newPolicy.config.scannerPeriodUnit = 'Hours'; + } + }, realm, client, $scope); +}); + +module.controller('ResourceServerPolicyResourceDetailCtrl', function($scope, $route, $location, realm, client, PolicyController, ResourceServerPolicy, ResourceServerResource) { + PolicyController.onInit({ + getPolicyType : function() { + return "resource"; + }, + + isPermission : function() { + return true; + }, + + onInit : function() { + ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) { + $scope.resources = data; + }); + + ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) { + $scope.policies = []; + + for (i = 0; i < data.length; i++) { + if (data[i].type != 'resource' && data[i].type != 'scope') { + $scope.policies.push(data[i]); + } + } + }); + + $scope.applyToResourceType = function() { + if ($scope.policy.config.default) { + $scope.policy.config.resources = []; + } else { + $scope.policy.config.defaultResourceType = null; + } + } + }, + + onInitUpdate : function(policy) { + policy.config.default = eval(policy.config.default); + policy.config.resources = eval(policy.config.resources); + policy.config.applyPolicies = eval(policy.config.applyPolicies); + }, + + onUpdate : function() { + $scope.policy.config.resources = JSON.stringify($scope.policy.config.resources); + $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies); + }, + + onInitCreate : function(newPolicy) { + newPolicy.decisionStrategy = 'UNANIMOUS'; + newPolicy.config = {}; + newPolicy.config.resources = ''; + + var resourceId = $location.search()['rsrid']; + + if (resourceId) { + newPolicy.config.resources = [resourceId]; + } + }, + + onCreate : function() { + $scope.policy.config.resources = JSON.stringify($scope.policy.config.resources); + $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies); + } + }, realm, client, $scope); +}); + +module.controller('ResourceServerPolicyScopeDetailCtrl', function($scope, $route, realm, client, PolicyController, ResourceServerPolicy, ResourceServerResource, ResourceServerScope) { + PolicyController.onInit({ + getPolicyType : function() { + return "scope"; + }, + + isPermission : function() { + return true; + }, + + onInit : function() { + ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) { + $scope.scopes = data; + }); + + ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) { + $scope.resources = data; + }); + + ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) { + $scope.policies = []; + + for (i = 0; i < data.length; i++) { + if (data[i].type != 'resource' && data[i].type != 'scope') { + $scope.policies.push(data[i]); + } + } + }); + + $scope.resolveScopes = function(policy, keepScopes) { + if (!keepScopes) { + policy.config.scopes = []; + } + + if (!policy) { + policy = $scope.policy; + } + + if (policy.config.resources != null) { + ResourceServerResource.get({ + realm : $route.current.params.realm, + client : client.id, + rsrid : policy.config.resources + }, function(data) { + $scope.scopes = data.scopes; + }); + } else { + ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) { + $scope.scopes = data; + }); + } + } + }, + + onInitUpdate : function(policy) { + if (policy.config.resources) { + policy.config.resources = eval(policy.config.resources); + + if (policy.config.resources.length > 0) { + policy.config.resources = policy.config.resources[0]; + } else { + policy.config.resources = null; + } + } + + $scope.resolveScopes(policy, true); + + policy.config.applyPolicies = eval(policy.config.applyPolicies); + policy.config.scopes = eval(policy.config.scopes); + }, + + onUpdate : function() { + if ($scope.policy.config.resources != null) { + var resources = undefined; + + if ($scope.policy.config.resources.length != 0) { + resources = JSON.stringify([$scope.policy.config.resources]) + } + + $scope.policy.config.resources = resources; + } + + $scope.policy.config.scopes = JSON.stringify($scope.policy.config.scopes); + $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies); + }, + + onInitCreate : function(newPolicy) { + newPolicy.decisionStrategy = 'UNANIMOUS'; + newPolicy.config = {}; + newPolicy.config.resources = ''; + }, + + onCreate : function() { + if ($scope.policy.config.resources != null) { + var resources = undefined; + + if ($scope.policy.config.resources.length != 0) { + resources = JSON.stringify([$scope.policy.config.resources]) + } + + $scope.policy.config.resources = resources; + } + $scope.policy.config.scopes = JSON.stringify($scope.policy.config.scopes); + $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies); + } + }, realm, client, $scope); +}); + +module.controller('ResourceServerPolicyUserDetailCtrl', function($scope, $route, realm, client, PolicyController, User) { + PolicyController.onInit({ + getPolicyType : function() { + return "user"; + }, + + onInit : function() { + User.query({realm: $route.current.params.realm}, function(data) { + $scope.users = data; + }); + + $scope.selectedUsers = []; + + $scope.selectUser = function(user) { + if (!user || !user.id) { + return; + } + + $scope.selectedUser = {}; + $scope.selectedUsers.push(user); + } + + $scope.removeFromList = function(list, index) { + list.splice(index, 1); + } + }, + + onInitUpdate : function(policy) { + var selectedUsers = []; + + if (policy.config.users) { + var users = eval(policy.config.users); + + for (i = 0; i < users.length; i++) { + User.get({realm: $route.current.params.realm, userId: users[i]}, function(data) { + selectedUsers.push(data); + $scope.selectedUsers = angular.copy(selectedUsers); + }); + } + } + + $scope.$watch('selectedUsers', function() { + if (!angular.equals($scope.selectedUsers, selectedUsers)) { + $scope.changed = true; + } + }, true); + }, + + onUpdate : function() { + var users = []; + + for (i = 0; i < $scope.selectedUsers.length; i++) { + users.push($scope.selectedUsers[i].id); + } + + $scope.policy.config.users = JSON.stringify(users); + }, + + onCreate : function() { + var users = []; + + for (i = 0; i < $scope.selectedUsers.length; i++) { + users.push($scope.selectedUsers[i].id); + } + + $scope.policy.config.users = JSON.stringify(users); + } + }, realm, client, $scope); +}); + +module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, realm, client, PolicyController, Role, RoleById) { + PolicyController.onInit({ + getPolicyType : function() { + return "role"; + }, + + onInit : function() { + Role.query({realm: $route.current.params.realm}, function(data) { + $scope.roles = data; + }); + + $scope.selectedRoles = []; + + $scope.selectRole = function(role) { + if (!role || !role.id) { + return; + } + + $scope.selectedRole = {}; + $scope.selectedRoles.push(role); + } + + $scope.removeFromList = function(list, index) { + list.splice(index, 1); + } + }, + + onInitUpdate : function(policy) { + var selectedRoles = []; + + if (policy.config.roles) { + var roles = eval(policy.config.roles); + + for (i = 0; i < roles.length; i++) { + RoleById.get({realm: $route.current.params.realm, role: roles[i]}, function(data) { + selectedRoles.push(data); + $scope.selectedRoles = angular.copy(selectedRoles); + }); + } + } + + $scope.$watch('selectedRoles', function() { + if (!angular.equals($scope.selectedRoles, selectedRoles)) { + $scope.changed = true; + } + }, true); + }, + + onUpdate : function() { + var roles = []; + + for (i = 0; i < $scope.selectedRoles.length; i++) { + roles.push($scope.selectedRoles[i].id); + } + + $scope.policy.config.roles = JSON.stringify(roles); + }, + + onCreate : function() { + var roles = []; + + for (i = 0; i < $scope.selectedRoles.length; i++) { + roles.push($scope.selectedRoles[i].id); + } + + $scope.policy.config.roles = JSON.stringify(roles); + } + }, realm, client, $scope); +}); + +module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) { + PolicyController.onInit({ + getPolicyType : function() { + return "js"; + }, + + onInit : function() { + $scope.initEditor = function(editor){ + var session = editor.getSession(); + + session.setMode('ace/mode/javascript'); + }; + }, + + onInitUpdate : function(policy) { + + }, + + onUpdate : function() { + + }, + + onInitCreate : function(newPolicy) { + newPolicy.config = {}; + }, + + onCreate : function() { + + } + }, realm, client, $scope); +}); + +module.controller('ResourceServerPolicyTimeDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) { + PolicyController.onInit({ + getPolicyType : function() { + return "time"; + }, + + onInit : function() { + }, + + onInitUpdate : function(policy) { + + }, + + onUpdate : function() { + + }, + + onInitCreate : function(newPolicy) { + newPolicy.config.expirationTime = 1; + newPolicy.config.expirationUnit = 'Minutes'; + }, + + onCreate : function() { + + } + }, realm, client, $scope); +}); + +module.controller('ResourceServerPolicyAggregateDetailCtrl', function($scope, $route, $location, realm, PolicyController, ResourceServerPolicy, client) { + PolicyController.onInit({ + getPolicyType : function() { + return "aggregate"; + }, + + onInit : function() { + ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) { + $scope.policies = []; + + for (i = 0; i < data.length; i++) { + if (data[i].type != 'resource' && data[i].type != 'scope') { + $scope.policies.push(data[i]); + } + } + }); + }, + + onInitUpdate : function(policy) { + policy.config.applyPolicies = eval(policy.config.applyPolicies); + }, + + onUpdate : function() { + $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies); + }, + + onInitCreate : function(newPolicy) { + newPolicy.config = {}; + newPolicy.decisionStrategy = 'UNANIMOUS'; + }, + + onCreate : function() { + $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies); + } + }, realm, client, $scope); +}); + +module.service("PolicyController", function($http, $route, $location, ResourceServer, ResourceServerPolicy, AuthzDialog, Notifications) { + + var PolicyController = {}; + + PolicyController.onInit = function(delegate, realm, client, $scope) { + if (!delegate.isPermission) { + delegate.isPermission = function () { + return false; + } + } + + $scope.realm = realm; + $scope.client = client; + + $scope.decisionStrategies = ['AFFIRMATIVE', 'UNANIMOUS', 'CONSENSUS']; + $scope.logics = ['POSITIVE', 'NEGATIVE']; + + delegate.onInit(); + + ResourceServer.get({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + $scope.server = data; + + var policyId = $route.current.params.id; + + if (!policyId) { + $scope.create = true; + $scope.changed = false; + + var policy = {}; + + policy.type = delegate.getPolicyType(); + policy.config = {}; + policy.logic = 'POSITIVE'; + + if (delegate.onInitCreate) { + delegate.onInitCreate(policy); + } + + $scope.policy = angular.copy(policy); + + $scope.$watch('policy', function() { + if (!angular.equals($scope.policy, policy)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + if (delegate.onCreate) { + delegate.onCreate(); + } + ResourceServerPolicy.save({realm : realm.realm, client : client.id}, $scope.policy, function(data) { + if (delegate.isPermission()) { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission/" + $scope.policy.type + "/" + data.id); + Notifications.success("The permission has been created."); + } else { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy/" + $scope.policy.type + "/" + data.id); + Notifications.success("The policy has been created."); + } + }); + } + + $scope.cancel = function() { + if (delegate.isPermission()) { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission/"); + } else { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy/"); + } + } + } else { + ResourceServerPolicy.get({ + realm : $route.current.params.realm, + client : client.id, + id : $route.current.params.id, + }, function(data) { + var policy = angular.copy(data); + + if (delegate.onInitUpdate) { + delegate.onInitUpdate(policy); + } + + $scope.policy = angular.copy(policy); + $scope.changed = false; + + $scope.$watch('policy', function() { + if (!angular.equals($scope.policy, policy)) { + $scope.changed = true; + } + }, true); + + $scope.save = function() { + if (delegate.onUpdate) { + delegate.onUpdate(); + } + ResourceServerPolicy.update({realm : realm.realm, client : client.id, id : $scope.policy.id}, $scope.policy, function() { + $route.reload(); + if (delegate.isPermission()) { + Notifications.success("The permission has been updated."); + } else { + Notifications.success("The policy has been updated."); + } + }); + } + + $scope.reset = function() { + var freshPolicy = angular.copy(data); + + if (delegate.onInitUpdate) { + delegate.onInitUpdate(freshPolicy); + } + + $scope.policy = angular.copy(freshPolicy); + $scope.changed = false; + } + }); + + $scope.remove = function() { + var msg = ""; + + if ($scope.policy.dependentPolicies.length > 0) { + msg = "

This policy is being used by other policies:

"; + msg += "
    "; + for (i = 0; i < $scope.policy.dependentPolicies.length; i++) { + msg+= "
  • " + $scope.policy.dependentPolicies[i].name + "
  • "; + } + msg += "
"; + msg += "

If you remove this policy, the policies above will be affected and will not be associated with this policy anymore.

"; + } + + AuthzDialog.confirmDeleteWithMsg($scope.policy.name, "Policy", msg, function() { + ResourceServerPolicy.delete({realm : $scope.realm.realm, client : $scope.client.id, id : $scope.policy.id}, null, function() { + if (delegate.isPermission()) { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission"); + Notifications.success("The permission has been deleted."); + } else { + $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy"); + Notifications.success("The policy has been deleted."); + } + }); + }); + } + } + }); + } + + return PolicyController; +}); + +module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $location, realm, clients, roles, ResourceServer, client, ResourceServerResource, ResourceServerScope, User, Notifications) { + $scope.realm = realm; + $scope.client = client; + $scope.clients = clients; + $scope.roles = roles; + $scope.authzRequest = {}; + $scope.authzRequest.resources = []; + $scope.authzRequest.context = {}; + $scope.authzRequest.context.attributes = {}; + $scope.authzRequest.roleIds = []; + $scope.newResource = {}; + $scope.resultUrl = resourceUrl + '/partials/authz/policy/resource-server-policy-evaluate-result.html'; + + ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) { + $scope.scopes = data; + }); + + $scope.addContextAttribute = function() { + if (!$scope.newContextAttribute.value || $scope.newContextAttribute.value == '') { + Notifications.error("You must provide a value to a context attribute."); + return; + } + + $scope.authzRequest.context.attributes[$scope.newContextAttribute.key] = $scope.newContextAttribute.value; + delete $scope.newContextAttribute; + } + + $scope.removeContextAttribute = function(key) { + delete $scope.authzRequest.context.attributes[key]; + } + + $scope.getContextAttribute = function(key) { + for (i = 0; i < $scope.defaultContextAttributes.length; i++) { + if ($scope.defaultContextAttributes[i].key == key) { + return $scope.defaultContextAttributes[i]; + } + } + + return $scope.authzRequest.context.attributes[key]; + } + + $scope.getContextAttributeName = function(key) { + var attribute = $scope.getContextAttribute(key); + + if (!attribute.name) { + return key; + } + + return attribute.name; + } + + $scope.defaultContextAttributes = [ + { + key : "custom", + name : "Custom Attribute...", + custom: true + }, + { + key : "kc.authz.context.authc.method", + name : "Authentication Method", + values: [ + { + key : "pwd", + name : "Password" + }, + { + key : "otp", + name : "One-Time Password" + }, + { + key : "kbr", + name : "Kerberos" + } + ] + }, + { + key : "kc.authz.context.authc.realm", + name : "Realm" + }, + { + key : "kc.authz.context.time.date_time", + name : "Date/Time (MM/dd/yyyy hh:mm:ss)" + }, + { + key : "kc.authz.context.client.network.ip_address", + name : "Client IPv4 Address" + }, + { + key : "kc.authz.context.client.network.host", + name : "Client Host" + }, + { + key : "kc.authz.context.client.user_agent", + name : "Client/User Agent" + } + ]; + + $scope.isDefaultContextAttribute = function() { + if (!$scope.newContextAttribute) { + return true; + } + + if ($scope.newContextAttribute.custom) { + return false; + } + + if (!$scope.getContextAttribute($scope.newContextAttribute.key).custom) { + return true; + } + + return false; + } + + $scope.selectDefaultContextAttribute = function() { + $scope.newContextAttribute = angular.copy($scope.newContextAttribute); + } + + $scope.setApplyToResourceType = function() { + if ($scope.applyResourceType) { + ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) { + $scope.scopes = data; + }); + } + + delete $scope.newResource; + $scope.authzRequest.resources = []; + } + + $scope.addResource = function() { + var resource = {}; + + resource.id = $scope.newResource._id; + + for (i = 0; i < $scope.resources.length; i++) { + if ($scope.resources[i]._id == resource.id) { + resource.name = $scope.resources[i].name; + break; + } + } + + resource.scopes = $scope.newResource.scopes; + + $scope.authzRequest.resources.push(resource); + + delete $scope.newResource; + } + + $scope.removeResource = function(index) { + $scope.authzRequest.resources.splice(index, 1); + } + + $scope.resolveScopes = function() { + if ($scope.newResource._id) { + $scope.newResource.scopes = []; + $scope.scopes = []; + ResourceServerResource.get({ + realm: $route.current.params.realm, + client : client.id, + rsrid: $scope.newResource._id + }, function (data) { + $scope.scopes = data.scopes; + }); + } else { + ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) { + $scope.scopes = data; + }); + } + } + + $scope.save = function() { + $scope.authzRequest.entitlements = false; + if ($scope.applyResourceType) { + if (!$scope.newResource) { + $scope.newResource = {}; + } + $scope.authzRequest.resources[0].scopes = $scope.newResource.scopes; + } + + $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/evaluate' + , $scope.authzRequest).success(function(data) { + $scope.evaluationResult = data; + $scope.showResultTab(); + }); + } + + $scope.entitlements = function() { + $scope.authzRequest.entitlements = true; + $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/evaluate' + , $scope.authzRequest).success(function(data) { + $scope.evaluationResult = data; + $scope.showResultTab(); + }); + } + + $scope.showResultTab = function() { + $scope.showResult = true; + } + + $scope.showRequestTab = function() { + $scope.showResult = false; + } + + User.query({realm: $route.current.params.realm}, function(data) { + $scope.users = data; + }); + + ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) { + $scope.resources = data; + }); + + ResourceServer.get({ + realm : $route.current.params.realm, + client : client.id + }, function(data) { + $scope.server = data; + }); +}); \ No newline at end of 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 new file mode 100644 index 00000000000..c74db283868 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js @@ -0,0 +1,117 @@ +module.factory('ResourceServer', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server', { + realm : '@realm', + client: '@client' + }, { + 'update' : {method : 'PUT'}, + 'settings' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/settings', method : 'GET'} + }); +}); + +module.factory('ResourceServerResource', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/resource/:rsrid', { + realm : '@realm', + client: '@client', + rsrid : '@rsrid' + }, { + 'update' : {method : 'PUT'} + }); +}); + +module.factory('ResourceServerScope', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/scope/:id', { + realm : '@realm', + client: '@client', + id : '@id' + }, { + 'update' : {method : 'PUT'} + }); +}); + +module.factory('ResourceServerPolicy', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/:id', { + realm : '@realm', + client: '@client', + id : '@id' + }, { + 'update' : {method : 'PUT'} + }); +}); + +module.factory('PolicyProvider', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/providers', { + realm : '@realm', + client: '@client' + }); +}); + +module.service('AuthzDialog', function($modal) { + var dialog = {}; + + var openDialog = function(title, message, btns, template) { + var controller = function($scope, $modalInstance, $sce, title, message, btns) { + $scope.title = title; + $scope.message = $sce.trustAsHtml(message); + $scope.btns = btns; + + $scope.ok = function () { + $modalInstance.close(); + }; + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + }; + + return $modal.open({ + templateUrl: resourceUrl + template, + controller: controller, + resolve: { + title: function() { + return title; + }, + message: function() { + return message; + }, + btns: function() { + return btns; + } + } + }).result; + } + + dialog.confirmDeleteWithMsg = function(name, type, msg, success) { + var title = 'Delete ' + type; + msg += 'Are you sure you want to permanently delete the ' + type + ' ' + name + ' ?'; + var btns = { + ok: { + label: 'Delete', + cssClass: 'btn btn-danger' + }, + cancel: { + label: 'Cancel', + cssClass: 'btn btn-default' + } + } + + openDialog(title, msg, btns, '/templates/authz/kc-authz-modal.html').then(success); + }; + + dialog.confirmDelete = function(name, type, success) { + var title = 'Delete ' + type; + var msg = 'Are you sure you want to permanently delete the ' + type + ' ' + name + ' ?'; + var btns = { + ok: { + label: 'Delete', + cssClass: 'btn btn-danger' + }, + cancel: { + label: 'Cancel', + cssClass: 'btn btn-default' + } + } + + openDialog(title, msg, btns, '/templates/authz/kc-authz-modal.html').then(success); + } + + return dialog; +}); \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/ace.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/ace.js new file mode 100644 index 00000000000..e0996510022 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/ace.js @@ -0,0 +1,11 @@ +(function(){function o(n){var i=e;n&&(e[n]||(e[n]={}),i=e[n]);if(!i.define||!i.define.packaged)t.original=i.define,i.define=t,i.define.packaged=!0;if(!i.require||!i.require.packaged)r.original=i.require,i.require=r,i.require.packaged=!0}var ACE_NAMESPACE="",e=function(){return this}();!e&&typeof window!="undefined"&&(e=window);if(!ACE_NAMESPACE&&typeof requirejs!="undefined")return;var t=function(e,n,r){if(typeof e!="string"){t.original?t.original.apply(this,arguments):(console.error("dropping module because define wasn't a string."),console.trace());return}arguments.length==2&&(r=n),t.modules[e]||(t.payloads[e]=r,t.modules[e]=null)};t.modules={},t.payloads={};var n=function(e,t,n){if(typeof t=="string"){var i=s(e,t);if(i!=undefined)return n&&n(),i}else if(Object.prototype.toString.call(t)==="[object Array]"){var o=[];for(var u=0,a=t.length;u1&&u(t,"")>-1&&(a=RegExp(this.source,r.replace.call(o(this),"g","")),r.replace.call(e.slice(t.index),a,function(){for(var e=1;et.index&&this.lastIndex--}return t},s||(RegExp.prototype.test=function(e){var t=r.exec.call(this,e);return t&&this.global&&!t[0].length&&this.lastIndex>t.index&&this.lastIndex--,!!t})}),define("ace/lib/es5-shim",["require","exports","module"],function(e,t,n){function r(){}function w(e){try{return Object.defineProperty(e,"sentinel",{}),"sentinel"in e}catch(t){}}function H(e){return e=+e,e!==e?e=0:e!==0&&e!==1/0&&e!==-1/0&&(e=(e>0||-1)*Math.floor(Math.abs(e))),e}function B(e){var t=typeof e;return e===null||t==="undefined"||t==="boolean"||t==="number"||t==="string"}function j(e){var t,n,r;if(B(e))return e;n=e.valueOf;if(typeof n=="function"){t=n.call(e);if(B(t))return t}r=e.toString;if(typeof r=="function"){t=r.call(e);if(B(t))return t}throw new TypeError}Function.prototype.bind||(Function.prototype.bind=function(t){var n=this;if(typeof n!="function")throw new TypeError("Function.prototype.bind called on incompatible "+n);var i=u.call(arguments,1),s=function(){if(this instanceof s){var e=n.apply(this,i.concat(u.call(arguments)));return Object(e)===e?e:this}return n.apply(t,i.concat(u.call(arguments)))};return n.prototype&&(r.prototype=n.prototype,s.prototype=new r,r.prototype=null),s});var i=Function.prototype.call,s=Array.prototype,o=Object.prototype,u=s.slice,a=i.bind(o.toString),f=i.bind(o.hasOwnProperty),l,c,h,p,d;if(d=f(o,"__defineGetter__"))l=i.bind(o.__defineGetter__),c=i.bind(o.__defineSetter__),h=i.bind(o.__lookupGetter__),p=i.bind(o.__lookupSetter__);if([1,2].splice(0).length!=2)if(!function(){function e(e){var t=new Array(e+2);return t[0]=t[1]=0,t}var t=[],n;t.splice.apply(t,e(20)),t.splice.apply(t,e(26)),n=t.length,t.splice(5,0,"XXX"),n+1==t.length;if(n+1==t.length)return!0}())Array.prototype.splice=function(e,t){var n=this.length;e>0?e>n&&(e=n):e==void 0?e=0:e<0&&(e=Math.max(n+e,0)),e+ta)for(h=l;h--;)this[f+h]=this[a+h];if(s&&e===c)this.length=c,this.push.apply(this,i);else{this.length=c+s;for(h=0;h>>0;if(a(t)!="[object Function]")throw new TypeError;while(++s>>0,s=Array(i),o=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var u=0;u>>0,s=[],o,u=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var f=0;f>>0,s=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var o=0;o>>0,s=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var o=0;o>>0;if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");if(!i&&arguments.length==1)throw new TypeError("reduce of empty array with no initial value");var s=0,o;if(arguments.length>=2)o=arguments[1];else do{if(s in r){o=r[s++];break}if(++s>=i)throw new TypeError("reduce of empty array with no initial value")}while(!0);for(;s>>0;if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");if(!i&&arguments.length==1)throw new TypeError("reduceRight of empty array with no initial value");var s,o=i-1;if(arguments.length>=2)s=arguments[1];else do{if(o in r){s=r[o--];break}if(--o<0)throw new TypeError("reduceRight of empty array with no initial value")}while(!0);do o in this&&(s=t.call(void 0,s,r[o],o,n));while(o--);return s});if(!Array.prototype.indexOf||[0,1].indexOf(1,2)!=-1)Array.prototype.indexOf=function(t){var n=g&&a(this)=="[object String]"?this.split(""):F(this),r=n.length>>>0;if(!r)return-1;var i=0;arguments.length>1&&(i=H(arguments[1])),i=i>=0?i:Math.max(0,r+i);for(;i>>0;if(!r)return-1;var i=r-1;arguments.length>1&&(i=Math.min(i,H(arguments[1]))),i=i>=0?i:r-Math.abs(i);for(;i>=0;i--)if(i in n&&t===n[i])return i;return-1};Object.getPrototypeOf||(Object.getPrototypeOf=function(t){return t.__proto__||(t.constructor?t.constructor.prototype:o)});if(!Object.getOwnPropertyDescriptor){var y="Object.getOwnPropertyDescriptor called on a non-object: ";Object.getOwnPropertyDescriptor=function(t,n){if(typeof t!="object"&&typeof t!="function"||t===null)throw new TypeError(y+t);if(!f(t,n))return;var r,i,s;r={enumerable:!0,configurable:!0};if(d){var u=t.__proto__;t.__proto__=o;var i=h(t,n),s=p(t,n);t.__proto__=u;if(i||s)return i&&(r.get=i),s&&(r.set=s),r}return r.value=t[n],r}}Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(t){return Object.keys(t)});if(!Object.create){var b;Object.prototype.__proto__===null?b=function(){return{__proto__:null}}:b=function(){var e={};for(var t in e)e[t]=null;return e.constructor=e.hasOwnProperty=e.propertyIsEnumerable=e.isPrototypeOf=e.toLocaleString=e.toString=e.valueOf=e.__proto__=null,e},Object.create=function(t,n){var r;if(t===null)r=b();else{if(typeof t!="object")throw new TypeError("typeof prototype["+typeof t+"] != 'object'");var i=function(){};i.prototype=t,r=new i,r.__proto__=t}return n!==void 0&&Object.defineProperties(r,n),r}}if(Object.defineProperty){var E=w({}),S=typeof document=="undefined"||w(document.createElement("div"));if(!E||!S)var x=Object.defineProperty}if(!Object.defineProperty||x){var T="Property description must be an object: ",N="Object.defineProperty called on non-object: ",C="getters & setters can not be defined on this javascript engine";Object.defineProperty=function(t,n,r){if(typeof t!="object"&&typeof t!="function"||t===null)throw new TypeError(N+t);if(typeof r!="object"&&typeof r!="function"||r===null)throw new TypeError(T+r);if(x)try{return x.call(Object,t,n,r)}catch(i){}if(f(r,"value"))if(d&&(h(t,n)||p(t,n))){var s=t.__proto__;t.__proto__=o,delete t[n],t[n]=r.value,t.__proto__=s}else t[n]=r.value;else{if(!d)throw new TypeError(C);f(r,"get")&&l(t,n,r.get),f(r,"set")&&c(t,n,r.set)}return t}}Object.defineProperties||(Object.defineProperties=function(t,n){for(var r in n)f(n,r)&&Object.defineProperty(t,r,n[r]);return t}),Object.seal||(Object.seal=function(t){return t}),Object.freeze||(Object.freeze=function(t){return t});try{Object.freeze(function(){})}catch(k){Object.freeze=function(t){return function(n){return typeof n=="function"?n:t(n)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(t){return t}),Object.isSealed||(Object.isSealed=function(t){return!1}),Object.isFrozen||(Object.isFrozen=function(t){return!1}),Object.isExtensible||(Object.isExtensible=function(t){if(Object(t)===t)throw new TypeError;var n="";while(f(t,n))n+="?";t[n]=!0;var r=f(t,n);return delete t[n],r});if(!Object.keys){var L=!0,A=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],O=A.length;for(var M in{toString:null})L=!1;Object.keys=function I(e){if(typeof e!="object"&&typeof e!="function"||e===null)throw new TypeError("Object.keys called on a non-object");var I=[];for(var t in e)f(e,t)&&I.push(t);if(L)for(var n=0,r=O;n=0?parseFloat((i.match(/(?:MSIE |Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]):parseFloat((i.match(/(?:Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]),t.isOldIE=t.isIE&&t.isIE<9,t.isGecko=t.isMozilla=(window.Controllers||window.controllers)&&window.navigator.product==="Gecko",t.isOldGecko=t.isGecko&&parseInt((i.match(/rv\:(\d+)/)||[])[1],10)<4,t.isOpera=window.opera&&Object.prototype.toString.call(window.opera)=="[object Opera]",t.isWebKit=parseFloat(i.split("WebKit/")[1])||undefined,t.isChrome=parseFloat(i.split(" Chrome/")[1])||undefined,t.isAIR=i.indexOf("AdobeAIR")>=0,t.isIPad=i.indexOf("iPad")>=0,t.isTouchPad=i.indexOf("TouchPad")>=0,t.isChromeOS=i.indexOf(" CrOS ")>=0}),define("ace/lib/event",["require","exports","module","ace/lib/keys","ace/lib/useragent"],function(e,t,n){"use strict";function a(e,t,n){var a=u(t);if(!i.isMac&&s){s.OSKey&&(a|=8);if(s.altGr){if((3&a)==3)return;s.altGr=0}if(n===18||n===17){var f="location"in t?t.location:t.keyLocation;if(n===17&&f===1)s[n]==1&&(o=t.timeStamp);else if(n===18&&a===3&&f===2){var l=t.timeStamp-o;l<50&&(s.altGr=!0)}}}n in r.MODIFIER_KEYS&&(n=-1),a&8&&n>=91&&n<=93&&(n=-1);if(!a&&n===13){var f="location"in t?t.location:t.keyLocation;if(f===3){e(t,a,-n);if(t.defaultPrevented)return}}if(i.isChromeOS&&a&8){e(t,a,n);if(t.defaultPrevented)return;a&=-9}return!!a||n in r.FUNCTION_KEYS||n in r.PRINTABLE_KEYS?e(t,a,n):!1}function f(){s=Object.create(null),s.count=0,s.lastT=0}var r=e("./keys"),i=e("./useragent"),s=null,o=0;t.addListener=function(e,t,n){if(e.addEventListener)return e.addEventListener(t,n,!1);if(e.attachEvent){var r=function(){n.call(e,window.event)};n._wrapper=r,e.attachEvent("on"+t,r)}},t.removeListener=function(e,t,n){if(e.removeEventListener)return e.removeEventListener(t,n,!1);e.detachEvent&&e.detachEvent("on"+t,n._wrapper||n)},t.stopEvent=function(e){return t.stopPropagation(e),t.preventDefault(e),!1},t.stopPropagation=function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0},t.preventDefault=function(e){e.preventDefault?e.preventDefault():e.returnValue=!1},t.getButton=function(e){return e.type=="dblclick"?0:e.type=="contextmenu"||i.isMac&&e.ctrlKey&&!e.altKey&&!e.shiftKey?2:e.preventDefault?e.button:{1:0,2:2,4:1}[e.button]},t.capture=function(e,n,r){function i(e){n&&n(e),r&&r(e),t.removeListener(document,"mousemove",n,!0),t.removeListener(document,"mouseup",i,!0),t.removeListener(document,"dragstart",i,!0)}return t.addListener(document,"mousemove",n,!0),t.addListener(document,"mouseup",i,!0),t.addListener(document,"dragstart",i,!0),i},t.addTouchMoveListener=function(e,n){if("ontouchmove"in e){var r,i;t.addListener(e,"touchstart",function(e){var t=e.changedTouches[0];r=t.clientX,i=t.clientY}),t.addListener(e,"touchmove",function(e){var t=1,s=e.changedTouches[0];e.wheelX=-(s.clientX-r)/t,e.wheelY=-(s.clientY-i)/t,r=s.clientX,i=s.clientY,n(e)})}},t.addMouseWheelListener=function(e,n){"onmousewheel"in e?t.addListener(e,"mousewheel",function(e){var t=8;e.wheelDeltaX!==undefined?(e.wheelX=-e.wheelDeltaX/t,e.wheelY=-e.wheelDeltaY/t):(e.wheelX=0,e.wheelY=-e.wheelDelta/t),n(e)}):"onwheel"in e?t.addListener(e,"wheel",function(e){var t=.35;switch(e.deltaMode){case e.DOM_DELTA_PIXEL:e.wheelX=e.deltaX*t||0,e.wheelY=e.deltaY*t||0;break;case e.DOM_DELTA_LINE:case e.DOM_DELTA_PAGE:e.wheelX=(e.deltaX||0)*5,e.wheelY=(e.deltaY||0)*5}n(e)}):t.addListener(e,"DOMMouseScroll",function(e){e.axis&&e.axis==e.HORIZONTAL_AXIS?(e.wheelX=(e.detail||0)*5,e.wheelY=0):(e.wheelX=0,e.wheelY=(e.detail||0)*5),n(e)})},t.addMultiMouseDownListener=function(e,n,r,s){function c(e){t.getButton(e)!==0?o=0:e.detail>1?(o++,o>4&&(o=1)):o=1;if(i.isIE){var c=Math.abs(e.clientX-u)>5||Math.abs(e.clientY-a)>5;if(!f||c)o=1;f&&clearTimeout(f),f=setTimeout(function(){f=null},n[o-1]||600),o==1&&(u=e.clientX,a=e.clientY)}e._clicks=o,r[s]("mousedown",e);if(o>4)o=0;else if(o>1)return r[s](l[o],e)}function h(e){o=2,f&&clearTimeout(f),f=setTimeout(function(){f=null},n[o-1]||600),r[s]("mousedown",e),r[s](l[o],e)}var o=0,u,a,f,l={2:"dblclick",3:"tripleclick",4:"quadclick"};Array.isArray(e)||(e=[e]),e.forEach(function(e){t.addListener(e,"mousedown",c),i.isOldIE&&t.addListener(e,"dblclick",h)})};var u=!i.isMac||!i.isOpera||"KeyboardEvent"in window?function(e){return 0|(e.ctrlKey?1:0)|(e.altKey?2:0)|(e.shiftKey?4:0)|(e.metaKey?8:0)}:function(e){return 0|(e.metaKey?1:0)|(e.altKey?2:0)|(e.shiftKey?4:0)|(e.ctrlKey?8:0)};t.getModifierString=function(e){return r.KEY_MODS[u(e)]},t.addCommandKeyListener=function(e,n){var r=t.addListener;if(i.isOldGecko||i.isOpera&&!("KeyboardEvent"in window)){var o=null;r(e,"keydown",function(e){o=e.keyCode}),r(e,"keypress",function(e){return a(n,e,o)})}else{var u=null;r(e,"keydown",function(e){var t=e.keyCode;s[t]=(s[t]||0)+1,t==91||t==92?s.OSKey=!0:s.OSKey&&e.timeStamp-s.lastT>200&&s.count==1&&f(),s[t]==1&&s.count++,s.lastT=e.timeStamp;var r=a(n,e,t);return u=e.defaultPrevented,r}),r(e,"keypress",function(e){u&&(e.ctrlKey||e.altKey||e.shiftKey||e.metaKey)&&(t.stopEvent(e),u=null)}),r(e,"keyup",function(e){var t=e.keyCode;s[t]?s.count=Math.max(s.count-1,0):f();if(t==91||t==92)s.OSKey=!1;s[t]=null}),s||(f(),r(window,"focus",f))}};if(typeof window=="object"&&window.postMessage&&!i.isOldIE){var l=1;t.nextTick=function(e,n){n=n||window;var r="zero-timeout-message-"+l;t.addListener(n,"message",function i(s){s.data==r&&(t.stopPropagation(s),t.removeListener(n,"message",i),e())}),n.postMessage(r,"*")}}t.nextFrame=typeof window=="object"&&(window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame),t.nextFrame?t.nextFrame=t.nextFrame.bind(window):t.nextFrame=function(e){setTimeout(e,17)}}),define("ace/lib/lang",["require","exports","module"],function(e,t,n){"use strict";t.last=function(e){return e[e.length-1]},t.stringReverse=function(e){return e.split("").reverse().join("")},t.stringRepeat=function(e,t){var n="";while(t>0){t&1&&(n+=e);if(t>>=1)e+=e}return n};var r=/^\s\s*/,i=/\s\s*$/;t.stringTrimLeft=function(e){return e.replace(r,"")},t.stringTrimRight=function(e){return e.replace(i,"")},t.copyObject=function(e){var t={};for(var n in e)t[n]=e[n];return t},t.copyArray=function(e){var t=[];for(var n=0,r=e.length;n1),e.preventDefault()},this.startSelect=function(e,t){e=e||this.editor.renderer.screenToTextCoordinates(this.x,this.y);var n=this.editor;n.$blockScrolling++,this.mousedownEvent.getShiftKey()?n.selection.selectToPosition(e):t||n.selection.moveToPosition(e),t||this.select(),n.renderer.scroller.setCapture&&n.renderer.scroller.setCapture(),n.setStyle("ace_selecting"),this.setState("select"),n.$blockScrolling--},this.select=function(){var e,t=this.editor,n=t.renderer.screenToTextCoordinates(this.x,this.y);t.$blockScrolling++;if(this.$clickSelection){var r=this.$clickSelection.comparePoint(n);if(r==-1)e=this.$clickSelection.end;else if(r==1)e=this.$clickSelection.start;else{var i=f(this.$clickSelection,n);n=i.cursor,e=i.anchor}t.selection.setSelectionAnchor(e.row,e.column)}t.selection.selectToPosition(n),t.$blockScrolling--,t.renderer.scrollCursorIntoView()},this.extendSelectionBy=function(e){var t,n=this.editor,r=n.renderer.screenToTextCoordinates(this.x,this.y),i=n.selection[e](r.row,r.column);n.$blockScrolling++;if(this.$clickSelection){var s=this.$clickSelection.comparePoint(i.start),o=this.$clickSelection.comparePoint(i.end);if(s==-1&&o<=0){t=this.$clickSelection.end;if(i.end.row!=r.row||i.end.column!=r.column)r=i.start}else if(o==1&&s>=0){t=this.$clickSelection.start;if(i.start.row!=r.row||i.start.column!=r.column)r=i.end}else if(s==-1&&o==1)r=i.end,t=i.start;else{var u=f(this.$clickSelection,r);r=u.cursor,t=u.anchor}n.selection.setSelectionAnchor(t.row,t.column)}n.selection.selectToPosition(r),n.$blockScrolling--,n.renderer.scrollCursorIntoView()},this.selectEnd=this.selectAllEnd=this.selectByWordsEnd=this.selectByLinesEnd=function(){this.$clickSelection=null,this.editor.unsetStyle("ace_selecting"),this.editor.renderer.scroller.releaseCapture&&this.editor.renderer.scroller.releaseCapture()},this.focusWait=function(){var e=a(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y),t=Date.now();(e>o||t-this.mousedownEvent.time>this.$focusTimout)&&this.startSelect(this.mousedownEvent.getDocumentPosition())},this.onDoubleClick=function(e){var t=e.getDocumentPosition(),n=this.editor,r=n.session,i=r.getBracketRange(t);i?(i.isEmpty()&&(i.start.column--,i.end.column++),this.setState("select")):(i=n.selection.getWordRange(t.row,t.column),this.setState("selectByWords")),this.$clickSelection=i,this.select()},this.onTripleClick=function(e){var t=e.getDocumentPosition(),n=this.editor;this.setState("selectByLines");var r=n.getSelectionRange();r.isMultiLine()&&r.contains(t.row,t.column)?(this.$clickSelection=n.selection.getLineRange(r.start.row),this.$clickSelection.end=n.selection.getLineRange(r.end.row).end):this.$clickSelection=n.selection.getLineRange(t.row),this.select()},this.onQuadClick=function(e){var t=this.editor;t.selectAll(),this.$clickSelection=t.getSelectionRange(),this.setState("selectAll")},this.onMouseWheel=function(e){if(e.getAccelKey())return;e.getShiftKey()&&e.wheelY&&!e.wheelX&&(e.wheelX=e.wheelY,e.wheelY=0);var t=e.domEvent.timeStamp,n=t-(this.$lastScrollTime||0),r=this.editor,i=r.renderer.isScrollableBy(e.wheelX*e.speed,e.wheelY*e.speed);if(i||n<200)return this.$lastScrollTime=t,r.renderer.scrollBy(e.wheelX*e.speed,e.wheelY*e.speed),e.stop()},this.onTouchMove=function(e){var t=e.domEvent.timeStamp,n=t-(this.$lastScrollTime||0),r=this.editor,i=r.renderer.isScrollableBy(e.wheelX*e.speed,e.wheelY*e.speed);if(i||n<200)return this.$lastScrollTime=t,r.renderer.scrollBy(e.wheelX*e.speed,e.wheelY*e.speed),e.stop()}}).call(u.prototype),t.DefaultHandlers=u}),define("ace/tooltip",["require","exports","module","ace/lib/oop","ace/lib/dom"],function(e,t,n){"use strict";function s(e){this.isOpen=!1,this.$element=null,this.$parentNode=e}var r=e("./lib/oop"),i=e("./lib/dom");(function(){this.$init=function(){return this.$element=i.createElement("div"),this.$element.className="ace_tooltip",this.$element.style.display="none",this.$parentNode.appendChild(this.$element),this.$element},this.getElement=function(){return this.$element||this.$init()},this.setText=function(e){i.setInnerText(this.getElement(),e)},this.setHtml=function(e){this.getElement().innerHTML=e},this.setPosition=function(e,t){this.getElement().style.left=e+"px",this.getElement().style.top=t+"px"},this.setClassName=function(e){i.addCssClass(this.getElement(),e)},this.show=function(e,t,n){e!=null&&this.setText(e),t!=null&&n!=null&&this.setPosition(t,n),this.isOpen||(this.getElement().style.display="block",this.isOpen=!0)},this.hide=function(){this.isOpen&&(this.getElement().style.display="none",this.isOpen=!1)},this.getHeight=function(){return this.getElement().offsetHeight},this.getWidth=function(){return this.getElement().offsetWidth}}).call(s.prototype),t.Tooltip=s}),define("ace/mouse/default_gutter_handler",["require","exports","module","ace/lib/dom","ace/lib/oop","ace/lib/event","ace/tooltip"],function(e,t,n){"use strict";function u(e){function l(){var r=u.getDocumentPosition().row,s=n.$annotations[r];if(!s)return c();var o=t.session.getLength();if(r==o){var a=t.renderer.pixelToScreenCoordinates(0,u.y).row,l=u.$pos;if(a>t.session.documentToScreenRow(l.row,l.column))return c()}if(f==s)return;f=s.text.join("
"),i.setHtml(f),i.show(),t.on("mousewheel",c);if(e.$tooltipFollowsMouse)h(u);else{var p=u.domEvent.target,d=p.getBoundingClientRect(),v=i.getElement().style;v.left=d.right+"px",v.top=d.bottom+"px"}}function c(){o&&(o=clearTimeout(o)),f&&(i.hide(),f=null,t.removeEventListener("mousewheel",c))}function h(e){i.setPosition(e.x,e.y)}var t=e.editor,n=t.renderer.$gutterLayer,i=new a(t.container);e.editor.setDefaultHandler("guttermousedown",function(r){if(!t.isFocused()||r.getButton()!=0)return;var i=n.getRegion(r);if(i=="foldWidgets")return;var s=r.getDocumentPosition().row,o=t.session.selection;if(r.getShiftKey())o.selectTo(s,0);else{if(r.domEvent.detail==2)return t.selectAll(),r.preventDefault();e.$clickSelection=t.selection.getLineRange(s)}return e.setState("selectByLines"),e.captureMouse(r),r.preventDefault()});var o,u,f;e.editor.setDefaultHandler("guttermousemove",function(t){var n=t.domEvent.target||t.domEvent.srcElement;if(r.hasCssClass(n,"ace_fold-widget"))return c();f&&e.$tooltipFollowsMouse&&h(t),u=t;if(o)return;o=setTimeout(function(){o=null,u&&!e.isMousePressed?l():c()},50)}),s.addListener(t.renderer.$gutter,"mouseout",function(e){u=null;if(!f||o)return;o=setTimeout(function(){o=null,c()},50)}),t.on("changeSession",c)}function a(e){o.call(this,e)}var r=e("../lib/dom"),i=e("../lib/oop"),s=e("../lib/event"),o=e("../tooltip").Tooltip;i.inherits(a,o),function(){this.setPosition=function(e,t){var n=window.innerWidth||document.documentElement.clientWidth,r=window.innerHeight||document.documentElement.clientHeight,i=this.getWidth(),s=this.getHeight();e+=15,t+=15,e+i>n&&(e-=e+i-n),t+s>r&&(t-=20+s),o.prototype.setPosition.call(this,e,t)}}.call(a.prototype),t.GutterHandler=u}),define("ace/mouse/mouse_event",["require","exports","module","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=t.MouseEvent=function(e,t){this.domEvent=e,this.editor=t,this.x=this.clientX=e.clientX,this.y=this.clientY=e.clientY,this.$pos=null,this.$inSelection=null,this.propagationStopped=!1,this.defaultPrevented=!1};(function(){this.stopPropagation=function(){r.stopPropagation(this.domEvent),this.propagationStopped=!0},this.preventDefault=function(){r.preventDefault(this.domEvent),this.defaultPrevented=!0},this.stop=function(){this.stopPropagation(),this.preventDefault()},this.getDocumentPosition=function(){return this.$pos?this.$pos:(this.$pos=this.editor.renderer.screenToTextCoordinates(this.clientX,this.clientY),this.$pos)},this.inSelection=function(){if(this.$inSelection!==null)return this.$inSelection;var e=this.editor,t=e.getSelectionRange();if(t.isEmpty())this.$inSelection=!1;else{var n=this.getDocumentPosition();this.$inSelection=t.contains(n.row,n.column)}return this.$inSelection},this.getButton=function(){return r.getButton(this.domEvent)},this.getShiftKey=function(){return this.domEvent.shiftKey},this.getAccelKey=i.isMac?function(){return this.domEvent.metaKey}:function(){return this.domEvent.ctrlKey}}).call(s.prototype)}),define("ace/mouse/dragdrop_handler",["require","exports","module","ace/lib/dom","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";function f(e){function T(e,n){var r=Date.now(),i=!n||e.row!=n.row,s=!n||e.column!=n.column;if(!S||i||s)t.$blockScrolling+=1,t.moveCursorToPosition(e),t.$blockScrolling-=1,S=r,x={x:p,y:d};else{var o=l(x.x,x.y,p,d);o>a?S=null:r-S>=u&&(t.renderer.scrollCursorIntoView(),S=null)}}function N(e,n){var r=Date.now(),i=t.renderer.layerConfig.lineHeight,s=t.renderer.layerConfig.characterWidth,u=t.renderer.scroller.getBoundingClientRect(),a={x:{left:p-u.left,right:u.right-p},y:{top:d-u.top,bottom:u.bottom-d}},f=Math.min(a.x.left,a.x.right),l=Math.min(a.y.top,a.y.bottom),c={row:e.row,column:e.column};f/s<=2&&(c.column+=a.x.left=o&&t.renderer.scrollCursorIntoView(c):E=r:E=null}function C(){var e=g;g=t.renderer.screenToTextCoordinates(p,d),T(g,e),N(g,e)}function k(){m=t.selection.toOrientedRange(),h=t.session.addMarker(m,"ace_selection",t.getSelectionStyle()),t.clearSelection(),t.isFocused()&&t.renderer.$cursorLayer.setBlinking(!1),clearInterval(v),C(),v=setInterval(C,20),y=0,i.addListener(document,"mousemove",O)}function L(){clearInterval(v),t.session.removeMarker(h),h=null,t.$blockScrolling+=1,t.selection.fromOrientedRange(m),t.$blockScrolling-=1,t.isFocused()&&!w&&t.renderer.$cursorLayer.setBlinking(!t.getReadOnly()),m=null,g=null,y=0,E=null,S=null,i.removeListener(document,"mousemove",O)}function O(){A==null&&(A=setTimeout(function(){A!=null&&h&&L()},20))}function M(e){var t=e.types;return!t||Array.prototype.some.call(t,function(e){return e=="text/plain"||e=="Text"})}function _(e){var t=["copy","copymove","all","uninitialized"],n=["move","copymove","linkmove","all","uninitialized"],r=s.isMac?e.altKey:e.ctrlKey,i="uninitialized";try{i=e.dataTransfer.effectAllowed.toLowerCase()}catch(e){}var o="none";return r&&t.indexOf(i)>=0?o="copy":n.indexOf(i)>=0?o="move":t.indexOf(i)>=0&&(o="copy"),o}var t=e.editor,n=r.createElement("img");n.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",s.isOpera&&(n.style.cssText="width:1px;height:1px;position:fixed;top:0;left:0;z-index:2147483647;opacity:0;");var f=["dragWait","dragWaitEnd","startDrag","dragReadyEnd","onMouseDrag"];f.forEach(function(t){e[t]=this[t]},this),t.addEventListener("mousedown",this.onMouseDown.bind(e));var c=t.container,h,p,d,v,m,g,y=0,b,w,E,S,x;this.onDragStart=function(e){if(this.cancelDrag||!c.draggable){var r=this;return setTimeout(function(){r.startSelect(),r.captureMouse(e)},0),e.preventDefault()}m=t.getSelectionRange();var i=e.dataTransfer;i.effectAllowed=t.getReadOnly()?"copy":"copyMove",s.isOpera&&(t.container.appendChild(n),n.scrollTop=0),i.setDragImage&&i.setDragImage(n,0,0),s.isOpera&&t.container.removeChild(n),i.clearData(),i.setData("Text",t.session.getTextRange()),w=!0,this.setState("drag")},this.onDragEnd=function(e){c.draggable=!1,w=!1,this.setState(null);if(!t.getReadOnly()){var n=e.dataTransfer.dropEffect;!b&&n=="move"&&t.session.remove(t.getSelectionRange()),t.renderer.$cursorLayer.setBlinking(!0)}this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle("")},this.onDragEnter=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||k(),y++,e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragOver=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||(k(),y++),A!==null&&(A=null),e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragLeave=function(e){y--;if(y<=0&&h)return L(),b=null,i.preventDefault(e)},this.onDrop=function(e){if(!g)return;var n=e.dataTransfer;if(w)switch(b){case"move":m.contains(g.row,g.column)?m={start:g,end:g}:m=t.moveText(m,g);break;case"copy":m=t.moveText(m,g,!0)}else{var r=n.getData("Text");m={start:g,end:t.session.insert(g,r)},t.focus(),b=null}return L(),i.preventDefault(e)},i.addListener(c,"dragstart",this.onDragStart.bind(e)),i.addListener(c,"dragend",this.onDragEnd.bind(e)),i.addListener(c,"dragenter",this.onDragEnter.bind(e)),i.addListener(c,"dragover",this.onDragOver.bind(e)),i.addListener(c,"dragleave",this.onDragLeave.bind(e)),i.addListener(c,"drop",this.onDrop.bind(e));var A=null}function l(e,t,n,r){return Math.sqrt(Math.pow(n-e,2)+Math.pow(r-t,2))}var r=e("../lib/dom"),i=e("../lib/event"),s=e("../lib/useragent"),o=200,u=200,a=5;(function(){this.dragWait=function(){var e=Date.now()-this.mousedownEvent.time;e>this.editor.getDragDelay()&&this.startDrag()},this.dragWaitEnd=function(){var e=this.editor.container;e.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()),this.selectEnd()},this.dragReadyEnd=function(e){this.editor.renderer.$cursorLayer.setBlinking(!this.editor.getReadOnly()),this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle(""),this.dragWaitEnd()},this.startDrag=function(){this.cancelDrag=!1;var e=this.editor,t=e.container;t.draggable=!0,e.renderer.$cursorLayer.setBlinking(!1),e.setStyle("ace_dragging");var n=s.isWin?"default":"move";e.renderer.setCursorStyle(n),this.setState("dragReady")},this.onMouseDrag=function(e){var t=this.editor.container;if(s.isIE&&this.state=="dragReady"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>3&&t.dragDrop()}if(this.state==="dragWait"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>0&&(t.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()))}},this.onMouseDown=function(e){if(!this.$dragEnabled)return;this.mousedownEvent=e;var t=this.editor,n=e.inSelection(),r=e.getButton(),i=e.domEvent.detail||1;if(i===1&&r===0&&n){if(e.editor.inMultiSelectMode&&(e.getAccelKey()||e.getShiftKey()))return;this.mousedownEvent.time=Date.now();var o=e.domEvent.target||e.domEvent.srcElement;"unselectable"in o&&(o.unselectable="on");if(t.getDragDelay()){if(s.isWebKit){this.cancelDrag=!0;var u=t.container;u.draggable=!0}this.setState("dragWait")}else this.startDrag();this.captureMouse(e,this.onMouseDrag.bind(this)),e.defaultPrevented=!0}}}).call(f.prototype),t.DragdropHandler=f}),define("ace/lib/net",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("./dom");t.get=function(e,t){var n=new XMLHttpRequest;n.open("GET",e,!0),n.onreadystatechange=function(){n.readyState===4&&t(n.responseText)},n.send(null)},t.loadScript=function(e,t){var n=r.getDocumentHead(),i=document.createElement("script");i.src=e,n.appendChild(i),i.onload=i.onreadystatechange=function(e,n){if(n||!i.readyState||i.readyState=="loaded"||i.readyState=="complete")i=i.onload=i.onreadystatechange=null,n||t()}},t.qualifyURL=function(e){var t=document.createElement("a");return t.href=e,t.href}}),define("ace/lib/event_emitter",["require","exports","module"],function(e,t,n){"use strict";var r={},i=function(){this.propagationStopped=!0},s=function(){this.defaultPrevented=!0};r._emit=r._dispatchEvent=function(e,t){this._eventRegistry||(this._eventRegistry={}),this._defaultHandlers||(this._defaultHandlers={});var n=this._eventRegistry[e]||[],r=this._defaultHandlers[e];if(!n.length&&!r)return;if(typeof t!="object"||!t)t={};t.type||(t.type=e),t.stopPropagation||(t.stopPropagation=i),t.preventDefault||(t.preventDefault=s),n=n.slice();for(var o=0;o1&&(i=n[n.length-2]);var o=a[t+"Path"];return o==null?o=a.basePath:r=="/"&&(t=r=""),o&&o.slice(-1)!="/"&&(o+="/"),o+t+r+i+this.get("suffix")},t.setModuleUrl=function(e,t){return a.$moduleUrls[e]=t},t.$loading={},t.loadModule=function(n,r){var i,o;Array.isArray(n)&&(o=n[0],n=n[1]);try{i=e(n)}catch(u){}if(i&&!t.$loading[n])return r&&r(i);t.$loading[n]||(t.$loading[n]=[]),t.$loading[n].push(r);if(t.$loading[n].length>1)return;var a=function(){e([n],function(e){t._emit("load.module",{name:n,module:e});var r=t.$loading[n];t.$loading[n]=null,r.forEach(function(t){t&&t(e)})})};if(!t.get("packaged"))return a();s.loadScript(t.moduleUrl(n,o),a)},t.init=f}),define("ace/mouse/mouse_handler",["require","exports","module","ace/lib/event","ace/lib/useragent","ace/mouse/default_handlers","ace/mouse/default_gutter_handler","ace/mouse/mouse_event","ace/mouse/dragdrop_handler","ace/config"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=e("./default_handlers").DefaultHandlers,o=e("./default_gutter_handler").GutterHandler,u=e("./mouse_event").MouseEvent,a=e("./dragdrop_handler").DragdropHandler,f=e("../config"),l=function(e){var t=this;this.editor=e,new s(this),new o(this),new a(this);var n=function(t){var n=!document.hasFocus||!document.hasFocus()||!e.isFocused()&&document.activeElement==(e.textInput&&e.textInput.getElement());n&&window.focus(),e.focus()},u=e.renderer.getMouseEventTarget();r.addListener(u,"click",this.onMouseEvent.bind(this,"click")),r.addListener(u,"mousemove",this.onMouseMove.bind(this,"mousemove")),r.addMultiMouseDownListener([u,e.renderer.scrollBarV&&e.renderer.scrollBarV.inner,e.renderer.scrollBarH&&e.renderer.scrollBarH.inner,e.textInput&&e.textInput.getElement()].filter(Boolean),[400,300,250],this,"onMouseEvent"),r.addMouseWheelListener(e.container,this.onMouseWheel.bind(this,"mousewheel")),r.addTouchMoveListener(e.container,this.onTouchMove.bind(this,"touchmove"));var f=e.renderer.$gutter;r.addListener(f,"mousedown",this.onMouseEvent.bind(this,"guttermousedown")),r.addListener(f,"click",this.onMouseEvent.bind(this,"gutterclick")),r.addListener(f,"dblclick",this.onMouseEvent.bind(this,"gutterdblclick")),r.addListener(f,"mousemove",this.onMouseEvent.bind(this,"guttermousemove")),r.addListener(u,"mousedown",n),r.addListener(f,"mousedown",n),i.isIE&&e.renderer.scrollBarV&&(r.addListener(e.renderer.scrollBarV.element,"mousedown",n),r.addListener(e.renderer.scrollBarH.element,"mousedown",n)),e.on("mousemove",function(n){if(t.state||t.$dragDelay||!t.$dragEnabled)return;var r=e.renderer.screenToTextCoordinates(n.x,n.y),i=e.session.selection.getRange(),s=e.renderer;!i.isEmpty()&&i.insideStart(r.row,r.column)?s.setCursorStyle("default"):s.setCursorStyle("")})};(function(){this.onMouseEvent=function(e,t){this.editor._emit(e,new u(t,this.editor))},this.onMouseMove=function(e,t){var n=this.editor._eventRegistry&&this.editor._eventRegistry.mousemove;if(!n||!n.length)return;this.editor._emit(e,new u(t,this.editor))},this.onMouseWheel=function(e,t){var n=new u(t,this.editor);n.speed=this.$scrollSpeed*2,n.wheelX=t.wheelX,n.wheelY=t.wheelY,this.editor._emit(e,n)},this.onTouchMove=function(e,t){var n=new u(t,this.editor);n.speed=1,n.wheelX=t.wheelX,n.wheelY=t.wheelY,this.editor._emit(e,n)},this.setState=function(e){this.state=e},this.captureMouse=function(e,t){this.x=e.x,this.y=e.y,this.isMousePressed=!0;var n=this.editor.renderer;n.$keepTextAreaAtCursor&&(n.$keepTextAreaAtCursor=null);var s=this,o=function(e){if(!e)return;if(i.isWebKit&&!e.which&&s.releaseMouse)return s.releaseMouse();s.x=e.clientX,s.y=e.clientY,t&&t(e),s.mouseEvent=new u(e,s.editor),s.$mouseMoved=!0},a=function(e){clearInterval(l),f(),s[s.state+"End"]&&s[s.state+"End"](e),s.state="",n.$keepTextAreaAtCursor==null&&(n.$keepTextAreaAtCursor=!0,n.$moveTextAreaToCursor()),s.isMousePressed=!1,s.$onCaptureMouseMove=s.releaseMouse=null,e&&s.onMouseEvent("mouseup",e)},f=function(){s[s.state]&&s[s.state](),s.$mouseMoved=!1};if(i.isOldIE&&e.domEvent.type=="dblclick")return setTimeout(function(){a(e)});s.$onCaptureMouseMove=o,s.releaseMouse=r.capture(this.editor.container,o,a);var l=setInterval(f,20)},this.releaseMouse=null,this.cancelContextMenu=function(){var e=function(t){if(t&&t.domEvent&&t.domEvent.type!="contextmenu")return;this.editor.off("nativecontextmenu",e),t&&t.domEvent&&r.stopEvent(t.domEvent)}.bind(this);setTimeout(e,10),this.editor.on("nativecontextmenu",e)}}).call(l.prototype),f.defineOptions(l.prototype,"mouseHandler",{scrollSpeed:{initialValue:2},dragDelay:{initialValue:i.isMac?150:0},dragEnabled:{initialValue:!0},focusTimout:{initialValue:0},tooltipFollowsMouse:{initialValue:!0}}),t.MouseHandler=l}),define("ace/mouse/fold_handler",["require","exports","module"],function(e,t,n){"use strict";function r(e){e.on("click",function(t){var n=t.getDocumentPosition(),r=e.session,i=r.getFoldAt(n.row,n.column,1);i&&(t.getAccelKey()?r.removeFold(i):r.expandFold(i),t.stop())}),e.on("gutterclick",function(t){var n=e.renderer.$gutterLayer.getRegion(t);if(n=="foldWidgets"){var r=t.getDocumentPosition().row,i=e.session;i.foldWidgets&&i.foldWidgets[r]&&e.session.onFoldWidgetClick(r,t),e.isFocused()||e.focus(),t.stop()}}),e.on("gutterdblclick",function(t){var n=e.renderer.$gutterLayer.getRegion(t);if(n=="foldWidgets"){var r=t.getDocumentPosition().row,i=e.session,s=i.getParentFoldRangeData(r,!0),o=s.range||s.firstRange;if(o){r=o.start.row;var u=i.getFoldAt(r,i.getLine(r).length,1);u?i.removeFold(u):(i.addFold("...",o),e.renderer.scrollCursorIntoView({row:o.start.row,column:0}))}t.stop()}})}t.FoldHandler=r}),define("ace/keyboard/keybinding",["require","exports","module","ace/lib/keys","ace/lib/event"],function(e,t,n){"use strict";var r=e("../lib/keys"),i=e("../lib/event"),s=function(e){this.$editor=e,this.$data={editor:e},this.$handlers=[],this.setDefaultHandler(e.commands)};(function(){this.setDefaultHandler=function(e){this.removeKeyboardHandler(this.$defaultHandler),this.$defaultHandler=e,this.addKeyboardHandler(e,0)},this.setKeyboardHandler=function(e){var t=this.$handlers;if(t[t.length-1]==e)return;while(t[t.length-1]&&t[t.length-1]!=this.$defaultHandler)this.removeKeyboardHandler(t[t.length-1]);this.addKeyboardHandler(e,1)},this.addKeyboardHandler=function(e,t){if(!e)return;typeof e=="function"&&!e.handleKeyboard&&(e.handleKeyboard=e);var n=this.$handlers.indexOf(e);n!=-1&&this.$handlers.splice(n,1),t==undefined?this.$handlers.push(e):this.$handlers.splice(t,0,e),n==-1&&e.attach&&e.attach(this.$editor)},this.removeKeyboardHandler=function(e){var t=this.$handlers.indexOf(e);return t==-1?!1:(this.$handlers.splice(t,1),e.detach&&e.detach(this.$editor),!0)},this.getKeyboardHandler=function(){return this.$handlers[this.$handlers.length-1]},this.getStatusText=function(){var e=this.$data,t=e.editor;return this.$handlers.map(function(n){return n.getStatusText&&n.getStatusText(t,e)||""}).filter(Boolean).join(" ")},this.$callKeyboardHandlers=function(e,t,n,r){var s,o=!1,u=this.$editor.commands;for(var a=this.$handlers.length;a--;){s=this.$handlers[a].handleKeyboard(this.$data,e,t,n,r);if(!s||!s.command)continue;s.command=="null"?o=!0:o=u.exec(s.command,this.$editor,s.args,r),o&&r&&e!=-1&&s.passEvent!=1&&s.command.passEvent!=1&&i.stopEvent(r);if(o)break}return!o&&e==-1&&(s={command:"insertstring"},o=u.exec("insertstring",this.$editor,t)),o&&this.$editor._signal("keyboardActivity",s),o},this.onCommandKey=function(e,t,n){var i=r.keyCodeToString(n);this.$callKeyboardHandlers(t,i,n,e)},this.onTextInput=function(e){this.$callKeyboardHandlers(-1,e)}}).call(s.prototype),t.KeyBinding=s}),define("ace/range",["require","exports","module"],function(e,t,n){"use strict";var r=function(e,t){return e.row-t.row||e.column-t.column},i=function(e,t,n,r){this.start={row:e,column:t},this.end={row:n,column:r}};(function(){this.isEqual=function(e){return this.start.row===e.start.row&&this.end.row===e.end.row&&this.start.column===e.start.column&&this.end.column===e.end.column},this.toString=function(){return"Range: ["+this.start.row+"/"+this.start.column+"] -> ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(e,t){return this.compare(e,t)==0},this.compareRange=function(e){var t,n=e.end,r=e.start;return t=this.compare(n.row,n.column),t==1?(t=this.compare(r.row,r.column),t==1?2:t==0?1:0):t==-1?-2:(t=this.compare(r.row,r.column),t==-1?-1:t==1?42:0)},this.comparePoint=function(e){return this.compare(e.row,e.column)},this.containsRange=function(e){return this.comparePoint(e.start)==0&&this.comparePoint(e.end)==0},this.intersects=function(e){var t=this.compareRange(e);return t==-1||t==0||t==1},this.isEnd=function(e,t){return this.end.row==e&&this.end.column==t},this.isStart=function(e,t){return this.start.row==e&&this.start.column==t},this.setStart=function(e,t){typeof e=="object"?(this.start.column=e.column,this.start.row=e.row):(this.start.row=e,this.start.column=t)},this.setEnd=function(e,t){typeof e=="object"?(this.end.column=e.column,this.end.row=e.row):(this.end.row=e,this.end.column=t)},this.inside=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)||this.isStart(e,t)?!1:!0:!1},this.insideStart=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)?!1:!0:!1},this.insideEnd=function(e,t){return this.compare(e,t)==0?this.isStart(e,t)?!1:!0:!1},this.compare=function(e,t){return!this.isMultiLine()&&e===this.start.row?tthis.end.column?1:0:ethis.end.row?1:this.start.row===e?t>=this.start.column?0:-1:this.end.row===e?t<=this.end.column?0:1:0},this.compareStart=function(e,t){return this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.compareEnd=function(e,t){return this.end.row==e&&this.end.column==t?1:this.compare(e,t)},this.compareInside=function(e,t){return this.end.row==e&&this.end.column==t?1:this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.clipRows=function(e,t){if(this.end.row>t)var n={row:t+1,column:0};else if(this.end.rowt)var r={row:t+1,column:0};else if(this.start.rowt.row||e.row==t.row&&e.column>t.column},this.getRange=function(){var e=this.anchor,t=this.lead;return this.isEmpty()?o.fromPoints(t,t):this.isBackwards()?o.fromPoints(t,e):o.fromPoints(e,t)},this.clearSelection=function(){this.$isEmpty||(this.$isEmpty=!0,this._emit("changeSelection"))},this.selectAll=function(){var e=this.doc.getLength()-1;this.setSelectionAnchor(0,0),this.moveCursorTo(e,this.doc.getLine(e).length)},this.setRange=this.setSelectionRange=function(e,t){t?(this.setSelectionAnchor(e.end.row,e.end.column),this.selectTo(e.start.row,e.start.column)):(this.setSelectionAnchor(e.start.row,e.start.column),this.selectTo(e.end.row,e.end.column)),this.getRange().isEmpty()&&(this.$isEmpty=!0),this.$desiredColumn=null},this.$moveSelection=function(e){var t=this.lead;this.$isEmpty&&this.setSelectionAnchor(t.row,t.column),e.call(this)},this.selectTo=function(e,t){this.$moveSelection(function(){this.moveCursorTo(e,t)})},this.selectToPosition=function(e){this.$moveSelection(function(){this.moveCursorToPosition(e)})},this.moveTo=function(e,t){this.clearSelection(),this.moveCursorTo(e,t)},this.moveToPosition=function(e){this.clearSelection(),this.moveCursorToPosition(e)},this.selectUp=function(){this.$moveSelection(this.moveCursorUp)},this.selectDown=function(){this.$moveSelection(this.moveCursorDown)},this.selectRight=function(){this.$moveSelection(this.moveCursorRight)},this.selectLeft=function(){this.$moveSelection(this.moveCursorLeft)},this.selectLineStart=function(){this.$moveSelection(this.moveCursorLineStart)},this.selectLineEnd=function(){this.$moveSelection(this.moveCursorLineEnd)},this.selectFileEnd=function(){this.$moveSelection(this.moveCursorFileEnd)},this.selectFileStart=function(){this.$moveSelection(this.moveCursorFileStart)},this.selectWordRight=function(){this.$moveSelection(this.moveCursorWordRight)},this.selectWordLeft=function(){this.$moveSelection(this.moveCursorWordLeft)},this.getWordRange=function(e,t){if(typeof t=="undefined"){var n=e||this.lead;e=n.row,t=n.column}return this.session.getWordRange(e,t)},this.selectWord=function(){this.setSelectionRange(this.getWordRange())},this.selectAWord=function(){var e=this.getCursor(),t=this.session.getAWordRange(e.row,e.column);this.setSelectionRange(t)},this.getLineRange=function(e,t){var n=typeof e=="number"?e:this.lead.row,r,i=this.session.getFoldLine(n);return i?(n=i.start.row,r=i.end.row):r=n,t===!0?new o(n,0,r,this.session.getLine(r).length):new o(n,0,r+1,0)},this.selectLine=function(){this.setSelectionRange(this.getLineRange())},this.moveCursorUp=function(){this.moveCursorBy(-1,0)},this.moveCursorDown=function(){this.moveCursorBy(1,0)},this.moveCursorLeft=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,-1))this.moveCursorTo(t.start.row,t.start.column);else if(e.column===0)e.row>0&&this.moveCursorTo(e.row-1,this.doc.getLine(e.row-1).length);else{var n=this.session.getTabSize();this.session.isTabStop(e)&&this.doc.getLine(e.row).slice(e.column-n,e.column).split(" ").length-1==n?this.moveCursorBy(0,-n):this.moveCursorBy(0,-1)}},this.moveCursorRight=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,1))this.moveCursorTo(t.end.row,t.end.column);else if(this.lead.column==this.doc.getLine(this.lead.row).length)this.lead.row0&&(t.column=r)}}this.moveCursorTo(t.row,t.column)},this.moveCursorFileEnd=function(){var e=this.doc.getLength()-1,t=this.doc.getLine(e).length;this.moveCursorTo(e,t)},this.moveCursorFileStart=function(){this.moveCursorTo(0,0)},this.moveCursorLongWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t),i;this.session.nonTokenRe.lastIndex=0,this.session.tokenRe.lastIndex=0;var s=this.session.getFoldAt(e,t,1);if(s){this.moveCursorTo(s.end.row,s.end.column);return}if(i=this.session.nonTokenRe.exec(r))t+=this.session.nonTokenRe.lastIndex,this.session.nonTokenRe.lastIndex=0,r=n.substring(t);if(t>=n.length){this.moveCursorTo(e,n.length),this.moveCursorRight(),e0&&this.moveCursorWordLeft();return}if(o=this.session.tokenRe.exec(s))t-=this.session.tokenRe.lastIndex,this.session.tokenRe.lastIndex=0;this.moveCursorTo(e,t)},this.$shortWordEndIndex=function(e){var t,n=0,r,i=/\s/,s=this.session.tokenRe;s.lastIndex=0;if(t=this.session.tokenRe.exec(e))n=this.session.tokenRe.lastIndex;else{while((r=e[n])&&i.test(r))n++;if(n<1){s.lastIndex=0;while((r=e[n])&&!s.test(r)){s.lastIndex=0,n++;if(i.test(r)){if(n>2){n--;break}while((r=e[n])&&i.test(r))n++;if(n>2)break}}}}return s.lastIndex=0,n},this.moveCursorShortWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t),i=this.session.getFoldAt(e,t,1);if(i)return this.moveCursorTo(i.end.row,i.end.column);if(t==n.length){var s=this.doc.getLength();do e++,r=this.doc.getLine(e);while(e0&&/^\s*$/.test(r));t=r.length,/\s+$/.test(r)||(r="")}var s=i.stringReverse(r),o=this.$shortWordEndIndex(s);return this.moveCursorTo(e,t-o)},this.moveCursorWordRight=function(){this.session.$selectLongWords?this.moveCursorLongWordRight():this.moveCursorShortWordRight()},this.moveCursorWordLeft=function(){this.session.$selectLongWords?this.moveCursorLongWordLeft():this.moveCursorShortWordLeft()},this.moveCursorBy=function(e,t){var n=this.session.documentToScreenPosition(this.lead.row,this.lead.column);t===0&&(this.$desiredColumn?n.column=this.$desiredColumn:this.$desiredColumn=n.column);var r=this.session.screenToDocumentPosition(n.row+e,n.column);e!==0&&t===0&&r.row===this.lead.row&&r.column===this.lead.column&&this.session.lineWidgets&&this.session.lineWidgets[r.row]&&(r.row>0||e>0)&&r.row++,this.moveCursorTo(r.row,r.column+t,t===0)},this.moveCursorToPosition=function(e){this.moveCursorTo(e.row,e.column)},this.moveCursorTo=function(e,t,n){var r=this.session.getFoldAt(e,t,1);r&&(e=r.start.row,t=r.start.column),this.$keepDesiredColumnOnChange=!0,this.lead.setPosition(e,t),this.$keepDesiredColumnOnChange=!1,n||(this.$desiredColumn=null)},this.moveCursorToScreen=function(e,t,n){var r=this.session.screenToDocumentPosition(e,t);this.moveCursorTo(r.row,r.column,n)},this.detach=function(){this.lead.detach(),this.anchor.detach(),this.session=this.doc=null},this.fromOrientedRange=function(e){this.setSelectionRange(e,e.cursor==e.start),this.$desiredColumn=e.desiredColumn||this.$desiredColumn},this.toOrientedRange=function(e){var t=this.getRange();return e?(e.start.column=t.start.column,e.start.row=t.start.row,e.end.column=t.end.column,e.end.row=t.end.row):e=t,e.cursor=this.isBackwards()?e.start:e.end,e.desiredColumn=this.$desiredColumn,e},this.getRangeOfMovements=function(e){var t=this.getCursor();try{e(this);var n=this.getCursor();return o.fromPoints(t,n)}catch(r){return o.fromPoints(t,t)}finally{this.moveCursorToPosition(t)}},this.toJSON=function(){if(this.rangeCount)var e=this.ranges.map(function(e){var t=e.clone();return t.isBackwards=e.cursor==e.start,t});else{var e=this.getRange();e.isBackwards=this.isBackwards()}return e},this.fromJSON=function(e){if(e.start==undefined){if(this.rangeList){this.toSingleRange(e[0]);for(var t=e.length;t--;){var n=o.fromPoints(e[t].start,e[t].end);e[t].isBackwards&&(n.cursor=n.start),this.addRange(n,!0)}return}e=e[0]}this.rangeList&&this.toSingleRange(e),this.setSelectionRange(e,e.isBackwards)},this.isEqual=function(e){if((e.length||this.rangeCount)&&e.length!=this.rangeCount)return!1;if(!e.length||!this.ranges)return this.getRange().isEqual(e);for(var t=this.ranges.length;t--;)if(!this.ranges[t].isEqual(e[t]))return!1;return!0}}).call(u.prototype),t.Selection=u}),define("ace/tokenizer",["require","exports","module","ace/config"],function(e,t,n){"use strict";var r=e("./config"),i=2e3,s=function(e){this.states=e,this.regExps={},this.matchMappings={};for(var t in this.states){var n=this.states[t],r=[],i=0,s=this.matchMappings[t]={defaultToken:"text"},o="g",u=[];for(var a=0;a1?f.onMatch=this.$applyToken:f.onMatch=f.token),c>1&&(/\\\d/.test(f.regex)?l=f.regex.replace(/\\([0-9]+)/g,function(e,t){return"\\"+(parseInt(t,10)+i+1)}):(c=1,l=this.removeCapturingGroups(f.regex)),!f.splitRegex&&typeof f.token!="string"&&u.push(f)),s[i]=a,i+=c,r.push(l),f.onMatch||(f.onMatch=null)}r.length||(s[0]=0,r.push("$")),u.forEach(function(e){e.splitRegex=this.createSplitterRegexp(e.regex,o)},this),this.regExps[t]=new RegExp("("+r.join(")|(")+")|($)",o)}};(function(){this.$setMaxTokenCount=function(e){i=e|0},this.$applyToken=function(e){var t=this.splitRegex.exec(e).slice(1),n=this.token.apply(this,t);if(typeof n=="string")return[{type:n,value:e}];var r=[];for(var i=0,s=n.length;il){var g=e.substring(l,m-v.length);h.type==p?h.value+=g:(h.type&&f.push(h),h={type:p,value:g})}for(var y=0;yi){c>2*e.length&&this.reportError("infinite loop with in ace tokenizer",{startState:t,line:e});while(l1&&n[0]!==r&&n.unshift("#tmp",r),{tokens:f,state:n.length?n:r}},this.reportError=r.reportError}).call(s.prototype),t.Tokenizer=s}),define("ace/mode/text_highlight_rules",["require","exports","module","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../lib/lang"),i=function(){this.$rules={start:[{token:"empty_line",regex:"^$"},{defaultToken:"text"}]}};(function(){this.addRules=function(e,t){if(!t){for(var n in e)this.$rules[n]=e[n];return}for(var n in e){var r=e[n];for(var i=0;i=this.$rowTokens.length){this.$row+=1,e||(e=this.$session.getLength());if(this.$row>=e)return this.$row=e-1,null;this.$rowTokens=this.$session.getTokens(this.$row),this.$tokenIndex=0}return this.$rowTokens[this.$tokenIndex]},this.getCurrentToken=function(){return this.$rowTokens[this.$tokenIndex]},this.getCurrentTokenRow=function(){return this.$row},this.getCurrentTokenColumn=function(){var e=this.$rowTokens,t=this.$tokenIndex,n=e[t].start;if(n!==undefined)return n;n=0;while(t>0)t-=1,n+=e[t].value.length;return n},this.getCurrentTokenPosition=function(){return{row:this.$row,column:this.getCurrentTokenColumn()}}}).call(r.prototype),t.TokenIterator=r}),define("ace/mode/text",["require","exports","module","ace/tokenizer","ace/mode/text_highlight_rules","ace/mode/behaviour","ace/unicode","ace/lib/lang","ace/token_iterator","ace/range"],function(e,t,n){"use strict";var r=e("../tokenizer").Tokenizer,i=e("./text_highlight_rules").TextHighlightRules,s=e("./behaviour").Behaviour,o=e("../unicode"),u=e("../lib/lang"),a=e("../token_iterator").TokenIterator,f=e("../range").Range,l=function(){this.HighlightRules=i,this.$behaviour=new s};(function(){this.tokenRe=new RegExp("^["+o.packages.L+o.packages.Mn+o.packages.Mc+o.packages.Nd+o.packages.Pc+"\\$_]+","g"),this.nonTokenRe=new RegExp("^(?:[^"+o.packages.L+o.packages.Mn+o.packages.Mc+o.packages.Nd+o.packages.Pc+"\\$_]|\\s])+","g"),this.getTokenizer=function(){return this.$tokenizer||(this.$highlightRules=this.$highlightRules||new this.HighlightRules,this.$tokenizer=new r(this.$highlightRules.getRules())),this.$tokenizer},this.lineCommentStart="",this.blockComment="",this.toggleCommentLines=function(e,t,n,r){function w(e){for(var t=n;t<=r;t++)e(i.getLine(t),t)}var i=t.doc,s=!0,o=!0,a=Infinity,f=t.getTabSize(),l=!1;if(!this.lineCommentStart){if(!this.blockComment)return!1;var c=this.blockComment.start,h=this.blockComment.end,p=new RegExp("^(\\s*)(?:"+u.escapeRegExp(c)+")"),d=new RegExp("(?:"+u.escapeRegExp(h)+")\\s*$"),v=function(e,t){if(g(e,t))return;if(!s||/\S/.test(e))i.insertInLine({row:t,column:e.length},h),i.insertInLine({row:t,column:a},c)},m=function(e,t){var n;(n=e.match(d))&&i.removeInLine(t,e.length-n[0].length,e.length),(n=e.match(p))&&i.removeInLine(t,n[1].length,n[0].length)},g=function(e,n){if(p.test(e))return!0;var r=t.getTokens(n);for(var i=0;i2?r%f!=f-1:r%f==0}}var E=Infinity;w(function(e,t){var n=e.search(/\S/);n!==-1?(ne.length&&(E=e.length)}),a==Infinity&&(a=E,s=!1,o=!1),l&&a%f!=0&&(a=Math.floor(a/f)*f),w(o?m:v)},this.toggleBlockComment=function(e,t,n,r){var i=this.blockComment;if(!i)return;!i.start&&i[0]&&(i=i[0]);var s=new a(t,r.row,r.column),o=s.getCurrentToken(),u=t.selection,l=t.selection.toOrientedRange(),c,h;if(o&&/comment/.test(o.type)){var p,d;while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.start);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;p=new f(m,g,m,g+i.start.length);break}o=s.stepBackward()}var s=new a(t,r.row,r.column),o=s.getCurrentToken();while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.end);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;d=new f(m,g,m,g+i.end.length);break}o=s.stepForward()}d&&t.remove(d),p&&(t.remove(p),c=p.start.row,h=-i.start.length)}else h=i.start.length,c=n.start.row,t.insert(n.end,i.end),t.insert(n.start,i.start);l.start.row==c&&(l.start.column+=h),l.end.row==c&&(l.end.column+=h),t.selection.fromOrientedRange(l)},this.getNextLineIndent=function(e,t,n){return this.$getIndent(t)},this.checkOutdent=function(e,t,n){return!1},this.autoOutdent=function(e,t,n){},this.$getIndent=function(e){return e.match(/^\s*/)[0]},this.createWorker=function(e){return null},this.createModeDelegates=function(e){this.$embeds=[],this.$modes={};for(var t in e)e[t]&&(this.$embeds.push(t),this.$modes[t]=new e[t]);var n=["toggleBlockComment","toggleCommentLines","getNextLineIndent","checkOutdent","autoOutdent","transformAction","getCompletions"];for(var t=0;t=0&&t.row=0&&t.column<=e[t.row].length}function s(e,t){t.action!="insert"&&t.action!="remove"&&r(t,"delta.action must be 'insert' or 'remove'"),t.lines instanceof Array||r(t,"delta.lines must be an Array"),(!t.start||!t.end)&&r(t,"delta.start/end must be an present");var n=t.start;i(e,t.start)||r(t,"delta.start must be contained in document");var s=t.end;t.action=="remove"&&!i(e,s)&&r(t,"delta.end must contained in document for 'remove' actions");var o=s.row-n.row,u=s.column-(o==0?n.column:0);(o!=t.lines.length-1||t.lines[o].length!=u)&&r(t,"delta.range must match delta lines")}t.applyDelta=function(e,t,n){var r=t.start.row,i=t.start.column,s=e[r]||"";switch(t.action){case"insert":var o=t.lines;if(o.length===1)e[r]=s.substring(0,i)+t.lines[0]+s.substring(i);else{var u=[r,1].concat(t.lines);e.splice.apply(e,u),e[r]=s.substring(0,i)+e[r],e[r+t.lines.length-1]+=s.substring(i)}break;case"remove":var a=t.end.column,f=t.end.row;r===f?e[r]=s.substring(0,i)+s.substring(a):e.splice(r,f-r+1,s.substring(0,i)+e[f].substring(a))}}}),define("ace/anchor",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/event_emitter").EventEmitter,s=t.Anchor=function(e,t,n){this.$onChange=this.onChange.bind(this),this.attach(e),typeof n=="undefined"?this.setPosition(t.row,t.column):this.setPosition(t,n)};(function(){function e(e,t,n){var r=n?e.column<=t.column:e.columnthis.row)return;var n=t(e,{row:this.row,column:this.column},this.$insertRight);this.setPosition(n.row,n.column,!0)},this.setPosition=function(e,t,n){var r;n?r={row:e,column:t}:r=this.$clipPositionToDocument(e,t);if(this.row==r.row&&this.column==r.column)return;var i={row:this.row,column:this.column};this.row=r.row,this.column=r.column,this._signal("change",{old:i,value:r})},this.detach=function(){this.document.removeEventListener("change",this.$onChange)},this.attach=function(e){this.document=e||this.document,this.document.on("change",this.$onChange)},this.$clipPositionToDocument=function(e,t){var n={};return e>=this.document.getLength()?(n.row=Math.max(0,this.document.getLength()-1),n.column=this.document.getLine(n.row).length):e<0?(n.row=0,n.column=0):(n.row=e,n.column=Math.min(this.document.getLine(n.row).length,Math.max(0,t))),t<0&&(n.column=0),n}}).call(s.prototype)}),define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./apply_delta").applyDelta,s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=e("./anchor").Anchor,a=function(e){this.$lines=[""],e.length===0?this.$lines=[""]:Array.isArray(e)?this.insertMergedLines({row:0,column:0},e):this.insert({row:0,column:0},e)};(function(){r.implement(this,s),this.setValue=function(e){var t=this.getLength()-1;this.remove(new o(0,0,t,this.getLine(t).length)),this.insert({row:0,column:0},e)},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(e,t){return new u(this,e,t)},"aaa".split(/a/).length===0?this.$split=function(e){return e.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(e){return e.split(/\r\n|\r|\n/)},this.$detectNewLine=function(e){var t=e.match(/^.*?(\r\n|\r|\n)/m);this.$autoNewLine=t?t[1]:"\n",this._signal("changeNewLineMode")},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";default:return this.$autoNewLine||"\n"}},this.$autoNewLine="",this.$newLineMode="auto",this.setNewLineMode=function(e){if(this.$newLineMode===e)return;this.$newLineMode=e,this._signal("changeNewLineMode")},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(e){return e=="\r\n"||e=="\r"||e=="\n"},this.getLine=function(e){return this.$lines[e]||""},this.getLines=function(e,t){return this.$lines.slice(e,t+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(e){return this.getLinesForRange(e).join(this.getNewLineCharacter())},this.getLinesForRange=function(e){var t;if(e.start.row===e.end.row)t=[this.getLine(e.start.row).substring(e.start.column,e.end.column)];else{t=this.getLines(e.start.row,e.end.row),t[0]=(t[0]||"").substring(e.start.column);var n=t.length-1;e.end.row-e.start.row==n&&(t[n]=t[n].substring(0,e.end.column))}return t},this.insertLines=function(e,t){return console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."),this.insertFullLines(e,t)},this.removeLines=function(e,t){return console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."),this.removeFullLines(e,t)},this.insertNewLine=function(e){return console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."),this.insertMergedLines(e,["",""])},this.insert=function(e,t){return this.getLength()<=1&&this.$detectNewLine(t),this.insertMergedLines(e,this.$split(t))},this.insertInLine=function(e,t){var n=this.clippedPos(e.row,e.column),r=this.pos(e.row,e.column+t.length);return this.applyDelta({start:n,end:r,action:"insert",lines:[t]},!0),this.clonePos(r)},this.clippedPos=function(e,t){var n=this.getLength();e===undefined?e=n:e<0?e=0:e>=n&&(e=n-1,t=undefined);var r=this.getLine(e);return t==undefined&&(t=r.length),t=Math.min(Math.max(t,0),r.length),{row:e,column:t}},this.clonePos=function(e){return{row:e.row,column:e.column}},this.pos=function(e,t){return{row:e,column:t}},this.$clipPosition=function(e){var t=this.getLength();return e.row>=t?(e.row=Math.max(0,t-1),e.column=this.getLine(t-1).length):(e.row=Math.max(0,e.row),e.column=Math.min(Math.max(e.column,0),this.getLine(e.row).length)),e},this.insertFullLines=function(e,t){e=Math.min(Math.max(e,0),this.getLength());var n=0;e0,r=t=0&&this.applyDelta({start:this.pos(e,this.getLine(e).length),end:this.pos(e+1,0),action:"remove",lines:["",""]})},this.replace=function(e,t){e instanceof o||(e=o.fromPoints(e.start,e.end));if(t.length===0&&e.isEmpty())return e.start;if(t==this.getTextRange(e))return e.end;this.remove(e);var n;return t?n=this.insert(e.start,t):n=e.start,n},this.applyDeltas=function(e){for(var t=0;t=0;t--)this.revertDelta(e[t])},this.applyDelta=function(e,t){var n=e.action=="insert";if(n?e.lines.length<=1&&!e.lines[0]:!o.comparePoints(e.start,e.end))return;n&&e.lines.length>2e4&&this.$splitAndapplyLargeDelta(e,2e4),i(this.$lines,e,t),this._signal("change",e)},this.$splitAndapplyLargeDelta=function(e,t){var n=e.lines,r=n.length,i=e.start.row,s=e.start.column,o=0,u=0;do{o=u,u+=t-1;var a=n.slice(o,u);if(u>r){e.lines=a,e.start.row=i+o,e.start.column=s;break}a.push(""),this.applyDelta({start:this.pos(i+o,s),end:this.pos(i+u,s=0),action:e.action,lines:a},!0)}while(!0)},this.revertDelta=function(e){this.applyDelta({start:this.clonePos(e.start),end:this.clonePos(e.end),action:e.action=="insert"?"remove":"insert",lines:e.lines.slice()})},this.indexToPosition=function(e,t){var n=this.$lines||this.getAllLines(),r=this.getNewLineCharacter().length;for(var i=t||0,s=n.length;i20){n.running=setTimeout(n.$worker,20);break}}n.currentLine=t,s<=r&&n.fireUpdateEvent(s,r)}};(function(){r.implement(this,i),this.setTokenizer=function(e){this.tokenizer=e,this.lines=[],this.states=[],this.start(0)},this.setDocument=function(e){this.doc=e,this.lines=[],this.states=[],this.stop()},this.fireUpdateEvent=function(e,t){var n={first:e,last:t};this._signal("update",{data:n})},this.start=function(e){this.currentLine=Math.min(e||0,this.currentLine,this.doc.getLength()),this.lines.splice(this.currentLine,this.lines.length),this.states.splice(this.currentLine,this.states.length),this.stop(),this.running=setTimeout(this.$worker,700)},this.scheduleStart=function(){this.running||(this.running=setTimeout(this.$worker,700))},this.$updateOnChange=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.lines[t]=null;else if(e.action=="remove")this.lines.splice(t,n+1,null),this.states.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.lines.splice.apply(this.lines,r),this.states.splice.apply(this.states,r)}this.currentLine=Math.min(t,this.currentLine,this.doc.getLength()),this.stop()},this.stop=function(){this.running&&clearTimeout(this.running),this.running=!1},this.getTokens=function(e){return this.lines[e]||this.$tokenizeRow(e)},this.getState=function(e){return this.currentLine==e&&this.$tokenizeRow(e),this.states[e]||"start"},this.$tokenizeRow=function(e){var t=this.doc.getLine(e),n=this.states[e-1],r=this.tokenizer.getLineTokens(t,n,e);return this.states[e]+""!=r.state+""?(this.states[e]=r.state,this.lines[e+1]=null,this.currentLine>e+1&&(this.currentLine=e+1)):this.currentLine==e&&(this.currentLine=e+1),this.lines[e]=r.tokens}}).call(s.prototype),t.BackgroundTokenizer=s}),define("ace/search_highlight",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(e,t,n){this.setRegexp(e),this.clazz=t,this.type=n||"text"};(function(){this.MAX_RANGES=500,this.setRegexp=function(e){if(this.regExp+""==e+"")return;this.regExp=e,this.cache=[]},this.update=function(e,t,n,i){if(!this.regExp)return;var o=i.firstRow,u=i.lastRow;for(var a=o;a<=u;a++){var f=this.cache[a];f==null&&(f=r.getMatchOffsets(n.getLine(a),this.regExp),f.length>this.MAX_RANGES&&(f=f.slice(0,this.MAX_RANGES)),f=f.map(function(e){return new s(a,e.offset,a,e.offset+e.length)}),this.cache[a]=f.length?f:"");for(var l=f.length;l--;)t.drawSingleLineMarker(e,f[l].toScreenRange(n),this.clazz,i)}}}).call(o.prototype),t.SearchHighlight=o}),define("ace/edit_session/fold_line",["require","exports","module","ace/range"],function(e,t,n){"use strict";function i(e,t){this.foldData=e,Array.isArray(t)?this.folds=t:t=this.folds=[t];var n=t[t.length-1];this.range=new r(t[0].start.row,t[0].start.column,n.end.row,n.end.column),this.start=this.range.start,this.end=this.range.end,this.folds.forEach(function(e){e.setFoldLine(this)},this)}var r=e("../range").Range;(function(){this.shiftRow=function(e){this.start.row+=e,this.end.row+=e,this.folds.forEach(function(t){t.start.row+=e,t.end.row+=e})},this.addFold=function(e){if(e.sameRow){if(e.start.rowthis.endRow)throw new Error("Can't add a fold to this FoldLine as it has no connection");this.folds.push(e),this.folds.sort(function(e,t){return-e.range.compareEnd(t.start.row,t.start.column)}),this.range.compareEnd(e.start.row,e.start.column)>0?(this.end.row=e.end.row,this.end.column=e.end.column):this.range.compareStart(e.end.row,e.end.column)<0&&(this.start.row=e.start.row,this.start.column=e.start.column)}else if(e.start.row==this.end.row)this.folds.push(e),this.end.row=e.end.row,this.end.column=e.end.column;else{if(e.end.row!=this.start.row)throw new Error("Trying to add fold to FoldRow that doesn't have a matching row");this.folds.unshift(e),this.start.row=e.start.row,this.start.column=e.start.column}e.foldLine=this},this.containsRow=function(e){return e>=this.start.row&&e<=this.end.row},this.walk=function(e,t,n){var r=0,i=this.folds,s,o,u,a=!0;t==null&&(t=this.end.row,n=this.end.column);for(var f=0;f0)continue;var a=i(e,o.start);return u===0?t&&a!==0?-s-2:s:a>0||a===0&&!t?s:-s-1}return-s-1},this.add=function(e){var t=!e.isEmpty(),n=this.pointIndex(e.start,t);n<0&&(n=-n-1);var r=this.pointIndex(e.end,t,n);return r<0?r=-r-1:r++,this.ranges.splice(n,r-n,e)},this.addList=function(e){var t=[];for(var n=e.length;n--;)t.push.apply(t,this.add(e[n]));return t},this.substractPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges.splice(t,1)},this.merge=function(){var e=[],t=this.ranges;t=t.sort(function(e,t){return i(e.start,t.start)});var n=t[0],r;for(var s=1;s=0},this.containsPoint=function(e){return this.pointIndex(e)>=0},this.rangeAtPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges[t]},this.clipRows=function(e,t){var n=this.ranges;if(n[0].start.row>t||n[n.length-1].start.rowr)break;l.start.row==r&&l.start.column>=t.column&&(l.start.column!=t.column||!this.$insertRight)&&(l.start.column+=o,l.start.row+=s);if(l.end.row==r&&l.end.column>=t.column){if(l.end.column==t.column&&this.$insertRight)continue;l.end.column==t.column&&o>0&&al.start.column&&l.end.column==u[a+1].start.column&&(l.end.column-=o),l.end.column+=o,l.end.row+=s}}if(s!=0&&a=e)return i;if(i.end.row>e)return null}return null},this.getNextFoldLine=function(e,t){var n=this.$foldData,r=0;t&&(r=n.indexOf(t)),r==-1&&(r=0);for(r;r=e)return i}return null},this.getFoldedRowCount=function(e,t){var n=this.$foldData,r=t-e+1;for(var i=0;i=t){u=e?r-=t-u:r=0);break}o>=e&&(u>=e?r-=o-u:r-=o-e+1)}return r},this.$addFoldLine=function(e){return this.$foldData.push(e),this.$foldData.sort(function(e,t){return e.start.row-t.start.row}),e},this.addFold=function(e,t){var n=this.$foldData,r=!1,o;e instanceof s?o=e:(o=new s(t,e),o.collapseChildren=t.collapseChildren),this.$clipRangeToDocument(o.range);var u=o.start.row,a=o.start.column,f=o.end.row,l=o.end.column;if(u0&&(this.removeFolds(p),p.forEach(function(e){o.addSubFold(e)}));for(var d=0;d0&&this.foldAll(e.start.row+1,e.end.row,e.collapseChildren-1),e.subFolds=[]},this.expandFolds=function(e){e.forEach(function(e){this.expandFold(e)},this)},this.unfold=function(e,t){var n,i;e==null?(n=new r(0,0,this.getLength(),0),t=!0):typeof e=="number"?n=new r(e,0,e,this.getLine(e).length):"row"in e?n=r.fromPoints(e,e):n=e,i=this.getFoldsInRangeList(n);if(t)this.removeFolds(i);else{var s=i;while(s.length)this.expandFolds(s),s=this.getFoldsInRangeList(n)}if(i.length)return i},this.isRowFolded=function(e,t){return!!this.getFoldLine(e,t)},this.getRowFoldEnd=function(e,t){var n=this.getFoldLine(e,t);return n?n.end.row:e},this.getRowFoldStart=function(e,t){var n=this.getFoldLine(e,t);return n?n.start.row:e},this.getFoldDisplayLine=function(e,t,n,r,i){r==null&&(r=e.start.row),i==null&&(i=0),t==null&&(t=e.end.row),n==null&&(n=this.getLine(t).length);var s=this.doc,o="";return e.walk(function(e,t,n,u){if(t=e){i=s.end.row;try{var o=this.addFold("...",s);o&&(o.collapseChildren=n)}catch(u){}}}},this.$foldStyles={manual:1,markbegin:1,markbeginend:1},this.$foldStyle="markbegin",this.setFoldStyle=function(e){if(!this.$foldStyles[e])throw new Error("invalid fold style: "+e+"["+Object.keys(this.$foldStyles).join(", ")+"]");if(this.$foldStyle==e)return;this.$foldStyle=e,e=="manual"&&this.unfold();var t=this.$foldMode;this.$setFolding(null),this.$setFolding(t)},this.$setFolding=function(e){if(this.$foldMode==e)return;this.$foldMode=e,this.off("change",this.$updateFoldWidgets),this.off("tokenizerUpdate",this.$tokenizerUpdateFoldWidgets),this._signal("changeAnnotation");if(!e||this.$foldStyle=="manual"){this.foldWidgets=null;return}this.foldWidgets=[],this.getFoldWidget=e.getFoldWidget.bind(e,this,this.$foldStyle),this.getFoldWidgetRange=e.getFoldWidgetRange.bind(e,this,this.$foldStyle),this.$updateFoldWidgets=this.updateFoldWidgets.bind(this),this.$tokenizerUpdateFoldWidgets=this.tokenizerUpdateFoldWidgets.bind(this),this.on("change",this.$updateFoldWidgets),this.on("tokenizerUpdate",this.$tokenizerUpdateFoldWidgets)},this.getParentFoldRangeData=function(e,t){var n=this.foldWidgets;if(!n||t&&n[e])return{};var r=e-1,i;while(r>=0){var s=n[r];s==null&&(s=n[r]=this.getFoldWidget(r));if(s=="start"){var o=this.getFoldWidgetRange(r);i||(i=o);if(o&&o.end.row>=e)break}r--}return{range:r!==-1&&o,firstRange:i}},this.onFoldWidgetClick=function(e,t){t=t.domEvent;var n={children:t.shiftKey,all:t.ctrlKey||t.metaKey,siblings:t.altKey},r=this.$toggleFoldWidget(e,n);if(!r){var i=t.target||t.srcElement;i&&/ace_fold-widget/.test(i.className)&&(i.className+=" ace_invalid")}},this.$toggleFoldWidget=function(e,t){if(!this.getFoldWidget)return;var n=this.getFoldWidget(e),r=this.getLine(e),i=n==="end"?-1:1,s=this.getFoldAt(e,i===-1?0:r.length,i);if(s){t.children||t.all?this.removeFold(s):this.expandFold(s);return}var o=this.getFoldWidgetRange(e,!0);if(o&&!o.isMultiLine()){s=this.getFoldAt(o.start.row,o.start.column,1);if(s&&o.isEqual(s.range)){this.removeFold(s);return}}if(t.siblings){var u=this.getParentFoldRangeData(e);if(u.range)var a=u.range.start.row+1,f=u.range.end.row;this.foldAll(a,f,t.all?1e4:0)}else t.children?(f=o?o.end.row:this.getLength(),this.foldAll(e+1,f,t.all?1e4:0)):o&&(t.all&&(o.collapseChildren=1e4),this.addFold("...",o));return o},this.toggleFoldWidget=function(e){var t=this.selection.getCursor().row;t=this.getRowFoldStart(t);var n=this.$toggleFoldWidget(t,{});if(n)return;var r=this.getParentFoldRangeData(t,!0);n=r.range||r.firstRange;if(n){t=n.start.row;var i=this.getFoldAt(t,this.getLine(t).length,1);i?this.removeFold(i):this.addFold("...",n)}},this.updateFoldWidgets=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.foldWidgets[t]=null;else if(e.action=="remove")this.foldWidgets.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.foldWidgets.splice.apply(this.foldWidgets,r)}},this.tokenizerUpdateFoldWidgets=function(e){var t=e.data;t.first!=t.last&&this.foldWidgets.length>t.first&&this.foldWidgets.splice(t.first,this.foldWidgets.length)}}var r=e("../range").Range,i=e("./fold_line").FoldLine,s=e("./fold").Fold,o=e("../token_iterator").TokenIterator;t.Folding=u}),define("ace/edit_session/bracket_match",["require","exports","module","ace/token_iterator","ace/range"],function(e,t,n){"use strict";function s(){this.findMatchingBracket=function(e,t){if(e.column==0)return null;var n=t||this.getLine(e.row).charAt(e.column-1);if(n=="")return null;var r=n.match(/([\(\[\{])|([\)\]\}])/);return r?r[1]?this.$findClosingBracket(r[1],e):this.$findOpeningBracket(r[2],e):null},this.getBracketRange=function(e){var t=this.getLine(e.row),n=!0,r,s=t.charAt(e.column-1),o=s&&s.match(/([\(\[\{])|([\)\]\}])/);o||(s=t.charAt(e.column),e={row:e.row,column:e.column+1},o=s&&s.match(/([\(\[\{])|([\)\]\}])/),n=!1);if(!o)return null;if(o[1]){var u=this.$findClosingBracket(o[1],e);if(!u)return null;r=i.fromPoints(e,u),n||(r.end.column++,r.start.column--),r.cursor=r.end}else{var u=this.$findOpeningBracket(o[2],e);if(!u)return null;r=i.fromPoints(u,e),n||(r.start.column++,r.end.column--),r.cursor=r.start}return r},this.$brackets={")":"(","(":")","]":"[","[":"]","{":"}","}":"{"},this.$findOpeningBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("rparen",".paren").replace(/\b(?:end)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn()-2,f=u.value;for(;;){while(a>=0){var l=f.charAt(a);if(l==i){s-=1;if(s==0)return{row:o.getCurrentTokenRow(),column:a+o.getCurrentTokenColumn()}}else l==e&&(s+=1);a-=1}do u=o.stepBackward();while(u&&!n.test(u.type));if(u==null)break;f=u.value,a=f.length-1}return null},this.$findClosingBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("lparen",".paren").replace(/\b(?:start|begin)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn();for(;;){var f=u.value,l=f.length;while(a=4352&&e<=4447||e>=4515&&e<=4519||e>=4602&&e<=4607||e>=9001&&e<=9002||e>=11904&&e<=11929||e>=11931&&e<=12019||e>=12032&&e<=12245||e>=12272&&e<=12283||e>=12288&&e<=12350||e>=12353&&e<=12438||e>=12441&&e<=12543||e>=12549&&e<=12589||e>=12593&&e<=12686||e>=12688&&e<=12730||e>=12736&&e<=12771||e>=12784&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=13054||e>=13056&&e<=19903||e>=19968&&e<=42124||e>=42128&&e<=42182||e>=43360&&e<=43388||e>=44032&&e<=55203||e>=55216&&e<=55238||e>=55243&&e<=55291||e>=63744&&e<=64255||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=65281&&e<=65376||e>=65504&&e<=65510}r.implement(this,o),this.setDocument=function(e){this.doc&&this.doc.removeListener("change",this.$onChange),this.doc=e,e.on("change",this.$onChange),this.bgTokenizer&&this.bgTokenizer.setDocument(this.getDocument()),this.resetCaches()},this.getDocument=function(){return this.doc},this.$resetRowCache=function(e){if(!e){this.$docRowCache=[],this.$screenRowCache=[];return}var t=this.$docRowCache.length,n=this.$getRowCacheIndex(this.$docRowCache,e)+1;t>n&&(this.$docRowCache.splice(n,t),this.$screenRowCache.splice(n,t))},this.$getRowCacheIndex=function(e,t){var n=0,r=e.length-1;while(n<=r){var i=n+r>>1,s=e[i];if(t>s)n=i+1;else{if(!(t=t)break}return r=n[s],r?(r.index=s,r.start=i-r.value.length,r):null},this.setUndoManager=function(e){this.$undoManager=e,this.$deltas=[],this.$deltasDoc=[],this.$deltasFold=[],this.$informUndoManager&&this.$informUndoManager.cancel();if(e){var t=this;this.$syncInformUndoManager=function(){t.$informUndoManager.cancel(),t.$deltasFold.length&&(t.$deltas.push({group:"fold",deltas:t.$deltasFold}),t.$deltasFold=[]),t.$deltasDoc.length&&(t.$deltas.push({group:"doc",deltas:t.$deltasDoc}),t.$deltasDoc=[]),t.$deltas.length>0&&e.execute({action:"aceupdate",args:[t.$deltas,t],merge:t.mergeUndoDeltas}),t.mergeUndoDeltas=!1,t.$deltas=[]},this.$informUndoManager=i.delayedCall(this.$syncInformUndoManager)}},this.markUndoGroup=function(){this.$syncInformUndoManager&&this.$syncInformUndoManager()},this.$defaultUndoManager={undo:function(){},redo:function(){},reset:function(){}},this.getUndoManager=function(){return this.$undoManager||this.$defaultUndoManager},this.getTabString=function(){return this.getUseSoftTabs()?i.stringRepeat(" ",this.getTabSize()):" "},this.setUseSoftTabs=function(e){this.setOption("useSoftTabs",e)},this.getUseSoftTabs=function(){return this.$useSoftTabs&&!this.$mode.$indentWithTabs},this.setTabSize=function(e){this.setOption("tabSize",e)},this.getTabSize=function(){return this.$tabSize},this.isTabStop=function(e){return this.$useSoftTabs&&e.column%this.$tabSize===0},this.$overwrite=!1,this.setOverwrite=function(e){this.setOption("overwrite",e)},this.getOverwrite=function(){return this.$overwrite},this.toggleOverwrite=function(){this.setOverwrite(!this.$overwrite)},this.addGutterDecoration=function(e,t){this.$decorations[e]||(this.$decorations[e]=""),this.$decorations[e]+=" "+t,this._signal("changeBreakpoint",{})},this.removeGutterDecoration=function(e,t){this.$decorations[e]=(this.$decorations[e]||"").replace(" "+t,""),this._signal("changeBreakpoint",{})},this.getBreakpoints=function(){return this.$breakpoints},this.setBreakpoints=function(e){this.$breakpoints=[];for(var t=0;t0&&(r=!!n.charAt(t-1).match(this.tokenRe)),r||(r=!!n.charAt(t).match(this.tokenRe));if(r)var i=this.tokenRe;else if(/^\s+$/.test(n.slice(t-1,t+1)))var i=/\s/;else var i=this.nonTokenRe;var s=t;if(s>0){do s--;while(s>=0&&n.charAt(s).match(i));s++}var o=t;while(oe&&(e=t.screenWidth)}),this.lineWidgetWidth=e},this.$computeWidth=function(e){if(this.$modified||e){this.$modified=!1;if(this.$useWrapMode)return this.screenWidth=this.$wrapLimit;var t=this.doc.getAllLines(),n=this.$rowLengthCache,r=0,i=0,s=this.$foldData[i],o=s?s.start.row:Infinity,u=t.length;for(var a=0;ao){a=s.end.row+1;if(a>=u)break;s=this.$foldData[i++],o=s?s.start.row:Infinity}n[a]==null&&(n[a]=this.$getStringScreenWidth(t[a])[0]),n[a]>r&&(r=n[a])}this.screenWidth=r}},this.getLine=function(e){return this.doc.getLine(e)},this.getLines=function(e,t){return this.doc.getLines(e,t)},this.getLength=function(){return this.doc.getLength()},this.getTextRange=function(e){return this.doc.getTextRange(e||this.selection.getRange())},this.insert=function(e,t){return this.doc.insert(e,t)},this.remove=function(e){return this.doc.remove(e)},this.removeFullLines=function(e,t){return this.doc.removeFullLines(e,t)},this.undoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;var n=null;for(var r=e.length-1;r!=-1;r--){var i=e[r];i.group=="doc"?(this.doc.revertDeltas(i.deltas),n=this.$getUndoSelection(i.deltas,!0,n)):i.deltas.forEach(function(e){this.addFolds(e.folds)},this)}return this.$fromUndo=!1,n&&this.$undoSelect&&!t&&this.selection.setSelectionRange(n),n},this.redoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;var n=null;for(var r=0;re.end.column&&(s.start.column+=u),s.end.row==e.end.row&&s.end.column>e.end.column&&(s.end.column+=u)),o&&s.start.row>=e.end.row&&(s.start.row+=o,s.end.row+=o)}s.end=this.insert(s.start,r);if(i.length){var a=e.start,l=s.start,o=l.row-a.row,u=l.column-a.column;this.addFolds(i.map(function(e){return e=e.clone(),e.start.row==a.row&&(e.start.column+=u),e.end.row==a.row&&(e.end.column+=u),e.start.row+=o,e.end.row+=o,e}))}return s},this.indentRows=function(e,t,n){n=n.replace(/\t/g,this.getTabString());for(var r=e;r<=t;r++)this.doc.insertInLine({row:r,column:0},n)},this.outdentRows=function(e){var t=e.collapseRows(),n=new f(0,0,0,0),r=this.getTabSize();for(var i=t.start.row;i<=t.end.row;++i){var s=this.getLine(i);n.start.row=i,n.end.row=i;for(var o=0;o0){var r=this.getRowFoldEnd(t+n);if(r>this.doc.getLength()-1)return 0;var i=r-t}else{e=this.$clipRowToDocument(e),t=this.$clipRowToDocument(t);var i=t-e+1}var s=new f(e,0,t,Number.MAX_VALUE),o=this.getFoldsInRange(s).map(function(e){return e=e.clone(),e.start.row+=i,e.end.row+=i,e}),u=n==0?this.doc.getLines(e,t):this.doc.removeFullLines(e,t);return this.doc.insertFullLines(e+i,u),o.length&&this.addFolds(o),i},this.moveLinesUp=function(e,t){return this.$moveLines(e,t,-1)},this.moveLinesDown=function(e,t){return this.$moveLines(e,t,1)},this.duplicateLines=function(e,t){return this.$moveLines(e,t,0)},this.$clipRowToDocument=function(e){return Math.max(0,Math.min(e,this.doc.getLength()-1))},this.$clipColumnToRow=function(e,t){return t<0?0:Math.min(this.doc.getLine(e).length,t)},this.$clipPositionToDocument=function(e,t){t=Math.max(0,t);if(e<0)e=0,t=0;else{var n=this.doc.getLength();e>=n?(e=n-1,t=this.doc.getLine(n-1).length):t=Math.min(this.doc.getLine(e).length,t)}return{row:e,column:t}},this.$clipRangeToDocument=function(e){e.start.row<0?(e.start.row=0,e.start.column=0):e.start.column=this.$clipColumnToRow(e.start.row,e.start.column);var t=this.doc.getLength()-1;return e.end.row>t?(e.end.row=t,e.end.column=this.doc.getLine(t).length):e.end.column=this.$clipColumnToRow(e.end.row,e.end.column),e},this.$wrapLimit=80,this.$useWrapMode=!1,this.$wrapLimitRange={min:null,max:null},this.setUseWrapMode=function(e){if(e!=this.$useWrapMode){this.$useWrapMode=e,this.$modified=!0,this.$resetRowCache(0);if(e){var t=this.getLength();this.$wrapData=Array(t),this.$updateWrapData(0,t-1)}this._signal("changeWrapMode")}},this.getUseWrapMode=function(){return this.$useWrapMode},this.setWrapLimitRange=function(e,t){if(this.$wrapLimitRange.min!==e||this.$wrapLimitRange.max!==t)this.$wrapLimitRange={min:e,max:t},this.$modified=!0,this.$useWrapMode&&this._signal("changeWrapMode")},this.adjustWrapLimit=function(e,t){var n=this.$wrapLimitRange;n.max<0&&(n={min:t,max:t});var r=this.$constrainWrapLimit(e,n.min,n.max);return r!=this.$wrapLimit&&r>1?(this.$wrapLimit=r,this.$modified=!0,this.$useWrapMode&&(this.$updateWrapData(0,this.getLength()-1),this.$resetRowCache(0),this._signal("changeWrapLimit")),!0):!1},this.$constrainWrapLimit=function(e,t,n){return t&&(e=Math.max(t,e)),n&&(e=Math.min(n,e)),e},this.getWrapLimit=function(){return this.$wrapLimit},this.setWrapLimit=function(e){this.setWrapLimitRange(e,e)},this.getWrapLimitRange=function(){return{min:this.$wrapLimitRange.min,max:this.$wrapLimitRange.max}},this.$updateInternalDataOnChange=function(e){var t=this.$useWrapMode,n=e.action,r=e.start,i=e.end,s=r.row,o=i.row,u=o-s,a=null;this.$updating=!0;if(u!=0)if(n==="remove"){this[t?"$wrapData":"$rowLengthCache"].splice(s,u);var f=this.$foldData;a=this.getFoldsInRange(e),this.removeFolds(a);var l=this.getFoldLine(i.row),c=0;if(l){l.addRemoveChars(i.row,i.column,r.column-i.column),l.shiftRow(-u);var h=this.getFoldLine(s);h&&h!==l&&(h.merge(l),l=h),c=f.indexOf(l)+1}for(c;c=i.row&&l.shiftRow(-u)}o=s}else{var p=Array(u);p.unshift(s,0);var d=t?this.$wrapData:this.$rowLengthCache;d.splice.apply(d,p);var f=this.$foldData,l=this.getFoldLine(s),c=0;if(l){var v=l.range.compareInside(r.row,r.column);v==0?(l=l.split(r.row,r.column),l&&(l.shiftRow(u),l.addRemoveChars(o,0,i.column-r.column))):v==-1&&(l.addRemoveChars(s,0,i.column-r.column),l.shiftRow(u)),c=f.indexOf(l)+1}for(c;c=s&&l.shiftRow(u)}}else{u=Math.abs(e.start.column-e.end.column),n==="remove"&&(a=this.getFoldsInRange(e),this.removeFolds(a),u=-u);var l=this.getFoldLine(s);l&&l.addRemoveChars(s,r.column,u)}return t&&this.$wrapData.length!=this.doc.getLength()&&console.error("doc.getLength() and $wrapData.length have to be the same!"),this.$updating=!1,t?this.$updateWrapData(s,o):this.$updateRowLengthCache(s,o),a},this.$updateRowLengthCache=function(e,t,n){this.$rowLengthCache[e]=null,this.$rowLengthCache[t]=null},this.$updateWrapData=function(e,t){var r=this.doc.getAllLines(),i=this.getTabSize(),s=this.$wrapData,o=this.$wrapLimit,a,f,l=e;t=Math.min(t,r.length-1);while(l<=t)f=this.getFoldLine(l,f),f?(a=[],f.walk(function(e,t,i,s){var o;if(e!=null){o=this.$getDisplayTokens(e,a.length),o[0]=n;for(var f=1;fr-b){var w=a+r-b;if(e[w-1]>=p&&e[w]>=p){y(w);continue}if(e[w]==n||e[w]==u){for(w;w!=a-1;w--)if(e[w]==n)break;if(w>a){y(w);continue}w=a+r;for(w;w>2)),a-1);while(w>E&&e[w]E&&e[w]E&&e[w]==l)w--}else while(w>E&&e[w]E){y(++w);continue}w=a+r,e[w]==t&&w--,y(w-b)}return s},this.$getDisplayTokens=function(n,r){var i=[],s;r=r||0;for(var o=0;o39&&u<48||u>57&&u<64?i.push(l):u>=4352&&m(u)?i.push(e,t):i.push(e)}return i},this.$getStringScreenWidth=function(e,t,n){if(t==0)return[0,0];t==null&&(t=Infinity),n=n||0;var r,i;for(i=0;i=4352&&m(r)?n+=2:n+=1;if(n>t)break}return[n,i]},this.lineWidgets=null,this.getRowLength=function(e){if(this.lineWidgets)var t=this.lineWidgets[e]&&this.lineWidgets[e].rowCount||0;else t=0;return!this.$useWrapMode||!this.$wrapData[e]?1+t:this.$wrapData[e].length+1+t},this.getRowLineCount=function(e){return!this.$useWrapMode||!this.$wrapData[e]?1:this.$wrapData[e].length+1},this.getRowWrapIndent=function(e){if(this.$useWrapMode){var t=this.screenToDocumentPosition(e,Number.MAX_VALUE),n=this.$wrapData[t.row];return n.length&&n[0]=0)var o=a[f],r=this.$docRowCache[f],c=e>a[l-1];else var c=!l;var h=this.getLength()-1,p=this.getNextFoldLine(r),d=p?p.start.row:Infinity;while(o<=e){u=this.getRowLength(r);if(o+u>e||r>=h)break;o+=u,r++,r>d&&(r=p.end.row+1,p=this.getNextFoldLine(r,p),d=p?p.start.row:Infinity),c&&(this.$docRowCache.push(r),this.$screenRowCache.push(o))}if(p&&p.start.row<=r)n=this.getFoldDisplayLine(p),r=p.start.row;else{if(o+u<=e||r>h)return{row:h,column:this.getLine(h).length};n=this.getLine(r),p=null}var v=0;if(this.$useWrapMode){var m=this.$wrapData[r];if(m){var g=Math.floor(e-o);s=m[g],g>0&&m.length&&(v=m.indent,i=m[g-1]||m[m.length-1],n=n.substring(i))}}return i+=this.$getStringScreenWidth(n,t-v)[1],this.$useWrapMode&&i>=s&&(i=s-1),p?p.idxToPosition(i):{row:r,column:i}},this.documentToScreenPosition=function(e,t){if(typeof t=="undefined")var n=this.$clipPositionToDocument(e.row,e.column);else n=this.$clipPositionToDocument(e,t);e=n.row,t=n.column;var r=0,i=null,s=null;s=this.getFoldAt(e,t,1),s&&(e=s.start.row,t=s.start.column);var o,u=0,a=this.$docRowCache,f=this.$getRowCacheIndex(a,e),l=a.length;if(l&&f>=0)var u=a[f],r=this.$screenRowCache[f],c=e>a[l-1];else var c=!l;var h=this.getNextFoldLine(u),p=h?h.start.row:Infinity;while(u=p){o=h.end.row+1;if(o>e)break;h=this.getNextFoldLine(o,h),p=h?h.start.row:Infinity}else o=u+1;r+=this.getRowLength(u),u=o,c&&(this.$docRowCache.push(u),this.$screenRowCache.push(r))}var d="";h&&u>=p?(d=this.getFoldDisplayLine(h,e,t),i=h.start.row):(d=this.getLine(e).substring(0,t),i=e);var v=0;if(this.$useWrapMode){var m=this.$wrapData[i];if(m){var g=0;while(d.length>=m[g])r++,g++;d=d.substring(m[g-1]||0,d.length),v=g>0?m.indent:0}}return{row:r,column:v+this.$getStringScreenWidth(d)[0]}},this.documentToScreenColumn=function(e,t){return this.documentToScreenPosition(e,t).column},this.documentToScreenRow=function(e,t){return this.documentToScreenPosition(e,t).row},this.getScreenLength=function(){var e=0,t=null;if(!this.$useWrapMode){e=this.getLength();var n=this.$foldData;for(var r=0;ro&&(s=t.end.row+1,t=this.$foldData[r++],o=t?t.start.row:Infinity)}}return this.lineWidgets&&(e+=this.$getWidgetScreenLength()),e},this.$setFontMetrics=function(e){if(!this.$enableVarChar)return;this.$getStringScreenWidth=function(t,n,r){if(n===0)return[0,0];n||(n=Infinity),r=r||0;var i,s;for(s=0;sn)break}return[r,s]}},this.destroy=function(){this.bgTokenizer&&(this.bgTokenizer.setDocument(null),this.bgTokenizer=null),this.$stopWorker()}}).call(p.prototype),e("./edit_session/folding").Folding.call(p.prototype),e("./edit_session/bracket_match").BracketMatch.call(p.prototype),s.defineOptions(p.prototype,"session",{wrap:{set:function(e){!e||e=="off"?e=!1:e=="free"?e=!0:e=="printMargin"?e=-1:typeof e=="string"&&(e=parseInt(e,10)||!1);if(this.$wrap==e)return;this.$wrap=e;if(!e)this.setUseWrapMode(!1);else{var t=typeof e=="number"?e:null;this.setWrapLimitRange(t,t),this.setUseWrapMode(!0)}},get:function(){return this.getUseWrapMode()?this.$wrap==-1?"printMargin":this.getWrapLimitRange().min?this.$wrap:"free":"off"},handlesSet:!0},wrapMethod:{set:function(e){e=e=="auto"?this.$mode.type!="text":e!="text",e!=this.$wrapAsCode&&(this.$wrapAsCode=e,this.$useWrapMode&&(this.$modified=!0,this.$resetRowCache(0),this.$updateWrapData(0,this.getLength()-1)))},initialValue:"auto"},indentedSoftWrap:{initialValue:!0},firstLineNumber:{set:function(){this._signal("changeBreakpoint")},initialValue:1},useWorker:{set:function(e){this.$useWorker=e,this.$stopWorker(),e&&this.$startWorker()},initialValue:!0},useSoftTabs:{initialValue:!0},tabSize:{set:function(e){if(isNaN(e)||this.$tabSize===e)return;this.$modified=!0,this.$rowLengthCache=[],this.$tabSize=e,this._signal("changeTabSize")},initialValue:4,handlesSet:!0},overwrite:{set:function(e){this._signal("changeOverwrite")},initialValue:!1},newLineMode:{set:function(e){this.doc.setNewLineMode(e)},get:function(){return this.doc.getNewLineMode()},handlesSet:!0},mode:{set:function(e){this.setMode(e)},get:function(){return this.$modeId}}}),t.EditSession=p}),define("ace/search",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(){this.$options={}};(function(){this.set=function(e){return i.mixin(this.$options,e),this},this.getOptions=function(){return r.copyObject(this.$options)},this.setOptions=function(e){this.$options=e},this.find=function(e){var t=this.$options,n=this.$matchIterator(e,t);if(!n)return!1;var r=null;return n.forEach(function(e,n,i){if(!e.start){var o=e.offset+(i||0);r=new s(n,o,n,o+e.length);if(!e.length&&t.start&&t.start.start&&t.skipCurrent!=0&&r.isEqual(t.start))return r=null,!1}else r=e;return!0}),r},this.findAll=function(e){var t=this.$options;if(!t.needle)return[];this.$assembleRegExp(t);var n=t.range,i=n?e.getLines(n.start.row,n.end.row):e.doc.getAllLines(),o=[],u=t.re;if(t.$isMultiLine){var a=u.length,f=i.length-a,l;e:for(var c=u.offset||0;c<=f;c++){for(var h=0;hv)continue;o.push(l=new s(c,v,c+a-1,m)),a>2&&(c=c+a-2)}}else for(var g=0;gE&&o[h].end.row==n.end.row)h--;o=o.slice(g,h+1);for(g=0,h=o.length;g=0;u--)if(i(o[u],t,s))return!0};else var u=function(e,t,s){var o=r.getMatchOffsets(e,n);for(var u=0;u=o;r--)if(n(e.getLine(r),r))return;if(t.wrap==0)return;for(r=u,o=s.row;r>=o;r--)if(n(e.getLine(r),r))return}:function(n){var r=s.row,i=e.getLine(r).substr(s.column);if(n(i,r,s.column))return;for(r+=1;r<=u;r++)if(n(e.getLine(r),r))return;if(t.wrap==0)return;for(r=o,u=s.row;r<=u;r++)if(n(e.getLine(r),r))return};return{forEach:a}}}).call(o.prototype),t.Search=o}),define("ace/keyboard/hash_handler",["require","exports","module","ace/lib/keys","ace/lib/useragent"],function(e,t,n){"use strict";function o(e,t){this.platform=t||(i.isMac?"mac":"win"),this.commands={},this.commandKeyBinding={},this.addCommands(e),this.$singleCommand=!0}function u(e,t){o.call(this,e,t),this.$singleCommand=!1}var r=e("../lib/keys"),i=e("../lib/useragent"),s=r.KEY_MODS;u.prototype=o.prototype,function(){function e(e){return typeof e=="object"&&e.bindKey&&e.bindKey.position||0}this.addCommand=function(e){this.commands[e.name]&&this.removeCommand(e),this.commands[e.name]=e,e.bindKey&&this._buildKeyHash(e)},this.removeCommand=function(e,t){var n=e&&(typeof e=="string"?e:e.name);e=this.commands[n],t||delete this.commands[n];var r=this.commandKeyBinding;for(var i in r){var s=r[i];if(s==e)delete r[i];else if(Array.isArray(s)){var o=s.indexOf(e);o!=-1&&(s.splice(o,1),s.length==1&&(r[i]=s[0]))}}},this.bindKey=function(e,t,n){typeof e=="object"&&e&&(n==undefined&&(n=e.position),e=e[this.platform]);if(!e)return;if(typeof t=="function")return this.addCommand({exec:t,bindKey:e,name:t.name||e});e.split("|").forEach(function(e){var r="";if(e.indexOf(" ")!=-1){var i=e.split(/\s+/);e=i.pop(),i.forEach(function(e){var t=this.parseKeys(e),n=s[t.hashId]+t.key;r+=(r?" ":"")+n,this._addCommandToBinding(r,"chainKeys")},this),r+=" "}var o=this.parseKeys(e),u=s[o.hashId]+o.key;this._addCommandToBinding(r+u,t,n)},this)},this._addCommandToBinding=function(t,n,r){var i=this.commandKeyBinding,s;if(!n)delete i[t];else if(!i[t]||this.$singleCommand)i[t]=n;else{Array.isArray(i[t])?(s=i[t].indexOf(n))!=-1&&i[t].splice(s,1):i[t]=[i[t]],typeof r!="number"&&(r||n.isDefault?r=-100:r=e(n));var o=i[t];for(s=0;sr)break}o.splice(s,0,n)}},this.addCommands=function(e){e&&Object.keys(e).forEach(function(t){var n=e[t];if(!n)return;if(typeof n=="string")return this.bindKey(n,t);typeof n=="function"&&(n={exec:n});if(typeof n!="object")return;n.name||(n.name=t),this.addCommand(n)},this)},this.removeCommands=function(e){Object.keys(e).forEach(function(t){this.removeCommand(e[t])},this)},this.bindKeys=function(e){Object.keys(e).forEach(function(t){this.bindKey(t,e[t])},this)},this._buildKeyHash=function(e){this.bindKey(e.bindKey,e)},this.parseKeys=function(e){var t=e.toLowerCase().split(/[\-\+]([\-\+])?/).filter(function(e){return e}),n=t.pop(),i=r[n];if(r.FUNCTION_KEYS[i])n=r.FUNCTION_KEYS[i].toLowerCase();else{if(!t.length)return{key:n,hashId:-1};if(t.length==1&&t[0]=="shift")return{key:n.toUpperCase(),hashId:-1}}var s=0;for(var o=t.length;o--;){var u=r.KEY_MODS[t[o]];if(u==null)return typeof console!="undefined"&&console.error("invalid modifier "+t[o]+" in "+e),!1;s|=u}return{key:n,hashId:s}},this.findKeyCommand=function(t,n){var r=s[t]+n;return this.commandKeyBinding[r]},this.handleKeyboard=function(e,t,n,r){if(r<0)return;var i=s[t]+n,o=this.commandKeyBinding[i];e.$keyChain&&(e.$keyChain+=" "+i,o=this.commandKeyBinding[e.$keyChain]||o);if(o)if(o=="chainKeys"||o[o.length-1]=="chainKeys")return e.$keyChain=e.$keyChain||i,{command:"null"};if(e.$keyChain)if(!!t&&t!=4||n.length!=1){if(t==-1||r>0)e.$keyChain=""}else e.$keyChain=e.$keyChain.slice(0,-i.length-1);return{command:o}},this.getStatusText=function(e,t){return t.$keyChain||""}}.call(o.prototype),t.HashHandler=o,t.MultiHashHandler=u}),define("ace/commands/command_manager",["require","exports","module","ace/lib/oop","ace/keyboard/hash_handler","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../keyboard/hash_handler").MultiHashHandler,s=e("../lib/event_emitter").EventEmitter,o=function(e,t){i.call(this,t,e),this.byName=this.commands,this.setDefaultHandler("exec",function(e){return e.command.exec(e.editor,e.args||{})})};r.inherits(o,i),function(){r.implement(this,s),this.exec=function(e,t,n){if(Array.isArray(e)){for(var r=e.length;r--;)if(this.exec(e[r],t,n))return!0;return!1}typeof e=="string"&&(e=this.commands[e]);if(!e)return!1;if(t&&t.$readOnly&&!e.readOnly)return!1;var i={editor:t,command:e,args:n};return i.returnValue=this._emit("exec",i),this._signal("afterExec",i),i.returnValue===!1?!1:!0},this.toggleRecording=function(e){if(this.$inReplay)return;return e&&e._emit("changeStatus"),this.recording?(this.macro.pop(),this.removeEventListener("exec",this.$addCommandToMacro),this.macro.length||(this.macro=this.oldMacro),this.recording=!1):(this.$addCommandToMacro||(this.$addCommandToMacro=function(e){this.macro.push([e.command,e.args])}.bind(this)),this.oldMacro=this.macro,this.macro=[],this.on("exec",this.$addCommandToMacro),this.recording=!0)},this.replay=function(e){if(this.$inReplay||!this.macro)return;if(this.recording)return this.toggleRecording(e);try{this.$inReplay=!0,this.macro.forEach(function(t){typeof t=="string"?this.exec(t,e):this.exec(t[0],e,t[1])},this)}finally{this.$inReplay=!1}},this.trimMacro=function(e){return e.map(function(e){return typeof e[0]!="string"&&(e[0]=e[0].name),e[1]||(e=e[0]),e})}}.call(o.prototype),t.CommandManager=o}),define("ace/commands/default_commands",["require","exports","module","ace/lib/lang","ace/config","ace/range"],function(e,t,n){"use strict";function o(e,t){return{win:e,mac:t}}var r=e("../lib/lang"),i=e("../config"),s=e("../range").Range;t.commands=[{name:"showSettingsMenu",bindKey:o("Ctrl-,","Command-,"),exec:function(e){i.loadModule("ace/ext/settings_menu",function(t){t.init(e),e.showSettingsMenu()})},readOnly:!0},{name:"goToNextError",bindKey:o("Alt-E","Ctrl-E"),exec:function(e){i.loadModule("ace/ext/error_marker",function(t){t.showErrorMarker(e,1)})},scrollIntoView:"animate",readOnly:!0},{name:"goToPreviousError",bindKey:o("Alt-Shift-E","Ctrl-Shift-E"),exec:function(e){i.loadModule("ace/ext/error_marker",function(t){t.showErrorMarker(e,-1)})},scrollIntoView:"animate",readOnly:!0},{name:"selectall",bindKey:o("Ctrl-A","Command-A"),exec:function(e){e.selectAll()},readOnly:!0},{name:"centerselection",bindKey:o(null,"Ctrl-L"),exec:function(e){e.centerSelection()},readOnly:!0},{name:"gotoline",bindKey:o("Ctrl-L","Command-L"),exec:function(e){var t=parseInt(prompt("Enter line number:"),10);isNaN(t)||e.gotoLine(t)},readOnly:!0},{name:"fold",bindKey:o("Alt-L|Ctrl-F1","Command-Alt-L|Command-F1"),exec:function(e){e.session.toggleFold(!1)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"unfold",bindKey:o("Alt-Shift-L|Ctrl-Shift-F1","Command-Alt-Shift-L|Command-Shift-F1"),exec:function(e){e.session.toggleFold(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleFoldWidget",bindKey:o("F2","F2"),exec:function(e){e.session.toggleFoldWidget()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleParentFoldWidget",bindKey:o("Alt-F2","Alt-F2"),exec:function(e){e.session.toggleFoldWidget(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"foldall",bindKey:o(null,"Ctrl-Command-Option-0"),exec:function(e){e.session.foldAll()},scrollIntoView:"center",readOnly:!0},{name:"foldOther",bindKey:o("Alt-0","Command-Option-0"),exec:function(e){e.session.foldAll(),e.session.unfold(e.selection.getAllRanges())},scrollIntoView:"center",readOnly:!0},{name:"unfoldall",bindKey:o("Alt-Shift-0","Command-Option-Shift-0"),exec:function(e){e.session.unfold()},scrollIntoView:"center",readOnly:!0},{name:"findnext",bindKey:o("Ctrl-K","Command-G"),exec:function(e){e.findNext()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"findprevious",bindKey:o("Ctrl-Shift-K","Command-Shift-G"),exec:function(e){e.findPrevious()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"selectOrFindNext",bindKey:o("Alt-K","Ctrl-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findNext()},readOnly:!0},{name:"selectOrFindPrevious",bindKey:o("Alt-Shift-K","Ctrl-Shift-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findPrevious()},readOnly:!0},{name:"find",bindKey:o("Ctrl-F","Command-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e)})},readOnly:!0},{name:"overwrite",bindKey:"Insert",exec:function(e){e.toggleOverwrite()},readOnly:!0},{name:"selecttostart",bindKey:o("Ctrl-Shift-Home","Command-Shift-Up"),exec:function(e){e.getSelection().selectFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotostart",bindKey:o("Ctrl-Home","Command-Home|Command-Up"),exec:function(e){e.navigateFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectup",bindKey:o("Shift-Up","Shift-Up"),exec:function(e){e.getSelection().selectUp()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golineup",bindKey:o("Up","Up|Ctrl-P"),exec:function(e,t){e.navigateUp(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttoend",bindKey:o("Ctrl-Shift-End","Command-Shift-Down"),exec:function(e){e.getSelection().selectFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotoend",bindKey:o("Ctrl-End","Command-End|Command-Down"),exec:function(e){e.navigateFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectdown",bindKey:o("Shift-Down","Shift-Down"),exec:function(e){e.getSelection().selectDown()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golinedown",bindKey:o("Down","Down|Ctrl-N"),exec:function(e,t){e.navigateDown(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordleft",bindKey:o("Ctrl-Shift-Left","Option-Shift-Left"),exec:function(e){e.getSelection().selectWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordleft",bindKey:o("Ctrl-Left","Option-Left"),exec:function(e){e.navigateWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolinestart",bindKey:o("Alt-Shift-Left","Command-Shift-Left"),exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolinestart",bindKey:o("Alt-Left|Home","Command-Left|Home|Ctrl-A"),exec:function(e){e.navigateLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectleft",bindKey:o("Shift-Left","Shift-Left"),exec:function(e){e.getSelection().selectLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoleft",bindKey:o("Left","Left|Ctrl-B"),exec:function(e,t){e.navigateLeft(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordright",bindKey:o("Ctrl-Shift-Right","Option-Shift-Right"),exec:function(e){e.getSelection().selectWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordright",bindKey:o("Ctrl-Right","Option-Right"),exec:function(e){e.navigateWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolineend",bindKey:o("Alt-Shift-Right","Command-Shift-Right"),exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolineend",bindKey:o("Alt-Right|End","Command-Right|End|Ctrl-E"),exec:function(e){e.navigateLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectright",bindKey:o("Shift-Right","Shift-Right"),exec:function(e){e.getSelection().selectRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoright",bindKey:o("Right","Right|Ctrl-F"),exec:function(e,t){e.navigateRight(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectpagedown",bindKey:"Shift-PageDown",exec:function(e){e.selectPageDown()},readOnly:!0},{name:"pagedown",bindKey:o(null,"Option-PageDown"),exec:function(e){e.scrollPageDown()},readOnly:!0},{name:"gotopagedown",bindKey:o("PageDown","PageDown|Ctrl-V"),exec:function(e){e.gotoPageDown()},readOnly:!0},{name:"selectpageup",bindKey:"Shift-PageUp",exec:function(e){e.selectPageUp()},readOnly:!0},{name:"pageup",bindKey:o(null,"Option-PageUp"),exec:function(e){e.scrollPageUp()},readOnly:!0},{name:"gotopageup",bindKey:"PageUp",exec:function(e){e.gotoPageUp()},readOnly:!0},{name:"scrollup",bindKey:o("Ctrl-Up",null),exec:function(e){e.renderer.scrollBy(0,-2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"scrolldown",bindKey:o("Ctrl-Down",null),exec:function(e){e.renderer.scrollBy(0,2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"selectlinestart",bindKey:"Shift-Home",exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectlineend",bindKey:"Shift-End",exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"togglerecording",bindKey:o("Ctrl-Alt-E","Command-Option-E"),exec:function(e){e.commands.toggleRecording(e)},readOnly:!0},{name:"replaymacro",bindKey:o("Ctrl-Shift-E","Command-Shift-E"),exec:function(e){e.commands.replay(e)},readOnly:!0},{name:"jumptomatching",bindKey:o("Ctrl-P","Ctrl-P"),exec:function(e){e.jumpToMatching()},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"selecttomatching",bindKey:o("Ctrl-Shift-P","Ctrl-Shift-P"),exec:function(e){e.jumpToMatching(!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"expandToMatching",bindKey:o("Ctrl-Shift-M","Ctrl-Shift-M"),exec:function(e){e.jumpToMatching(!0,!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"passKeysToBrowser",bindKey:o(null,null),exec:function(){},passEvent:!0,readOnly:!0},{name:"copy",exec:function(e){},readOnly:!0},{name:"cut",exec:function(e){var t=e.getSelectionRange();e._emit("cut",t),e.selection.isEmpty()||(e.session.remove(t),e.clearSelection())},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"paste",exec:function(e,t){e.$handlePaste(t)},scrollIntoView:"cursor"},{name:"removeline",bindKey:o("Ctrl-D","Command-D"),exec:function(e){e.removeLines()},scrollIntoView:"cursor",multiSelectAction:"forEachLine"},{name:"duplicateSelection",bindKey:o("Ctrl-Shift-D","Command-Shift-D"),exec:function(e){e.duplicateSelection()},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"sortlines",bindKey:o("Ctrl-Alt-S","Command-Alt-S"),exec:function(e){e.sortLines()},scrollIntoView:"selection",multiSelectAction:"forEachLine"},{name:"togglecomment",bindKey:o("Ctrl-/","Command-/"),exec:function(e){e.toggleCommentLines()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"toggleBlockComment",bindKey:o("Ctrl-Shift-/","Command-Shift-/"),exec:function(e){e.toggleBlockComment()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"modifyNumberUp",bindKey:o("Ctrl-Shift-Up","Alt-Shift-Up"),exec:function(e){e.modifyNumber(1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"modifyNumberDown",bindKey:o("Ctrl-Shift-Down","Alt-Shift-Down"),exec:function(e){e.modifyNumber(-1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"replace",bindKey:o("Ctrl-H","Command-Option-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e,!0)})}},{name:"undo",bindKey:o("Ctrl-Z","Command-Z"),exec:function(e){e.undo()}},{name:"redo",bindKey:o("Ctrl-Shift-Z|Ctrl-Y","Command-Shift-Z|Command-Y"),exec:function(e){e.redo()}},{name:"copylinesup",bindKey:o("Alt-Shift-Up","Command-Option-Up"),exec:function(e){e.copyLinesUp()},scrollIntoView:"cursor"},{name:"movelinesup",bindKey:o("Alt-Up","Option-Up"),exec:function(e){e.moveLinesUp()},scrollIntoView:"cursor"},{name:"copylinesdown",bindKey:o("Alt-Shift-Down","Command-Option-Down"),exec:function(e){e.copyLinesDown()},scrollIntoView:"cursor"},{name:"movelinesdown",bindKey:o("Alt-Down","Option-Down"),exec:function(e){e.moveLinesDown()},scrollIntoView:"cursor"},{name:"del",bindKey:o("Delete","Delete|Ctrl-D|Shift-Delete"),exec:function(e){e.remove("right")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"backspace",bindKey:o("Shift-Backspace|Backspace","Ctrl-Backspace|Shift-Backspace|Backspace|Ctrl-H"),exec:function(e){e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"cut_or_delete",bindKey:o("Shift-Delete",null),exec:function(e){if(!e.selection.isEmpty())return!1;e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolinestart",bindKey:o("Alt-Backspace","Command-Backspace"),exec:function(e){e.removeToLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolineend",bindKey:o("Alt-Delete","Ctrl-K"),exec:function(e){e.removeToLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordleft",bindKey:o("Ctrl-Backspace","Alt-Backspace|Ctrl-Alt-Backspace"),exec:function(e){e.removeWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordright",bindKey:o("Ctrl-Delete","Alt-Delete"),exec:function(e){e.removeWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"outdent",bindKey:o("Shift-Tab","Shift-Tab"),exec:function(e){e.blockOutdent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"indent",bindKey:o("Tab","Tab"),exec:function(e){e.indent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"blockoutdent",bindKey:o("Ctrl-[","Ctrl-["),exec:function(e){e.blockOutdent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"blockindent",bindKey:o("Ctrl-]","Ctrl-]"),exec:function(e){e.blockIndent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"insertstring",exec:function(e,t){e.insert(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"inserttext",exec:function(e,t){e.insert(r.stringRepeat(t.text||"",t.times||1))},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"splitline",bindKey:o(null,"Ctrl-O"),exec:function(e){e.splitLine()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"transposeletters",bindKey:o("Ctrl-T","Ctrl-T"),exec:function(e){e.transposeLetters()},multiSelectAction:function(e){e.transposeSelections(1)},scrollIntoView:"cursor"},{name:"touppercase",bindKey:o("Ctrl-U","Ctrl-U"),exec:function(e){e.toUpperCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"tolowercase",bindKey:o("Ctrl-Shift-U","Ctrl-Shift-U"),exec:function(e){e.toLowerCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"expandtoline",bindKey:o("Ctrl-Shift-L","Command-Shift-L"),exec:function(e){var t=e.selection.getRange();t.start.column=t.end.column=0,t.end.row++,e.selection.setRange(t,!1)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"joinlines",bindKey:o(null,null),exec:function(e){var t=e.selection.isBackwards(),n=t?e.selection.getSelectionLead():e.selection.getSelectionAnchor(),i=t?e.selection.getSelectionAnchor():e.selection.getSelectionLead(),o=e.session.doc.getLine(n.row).length,u=e.session.doc.getTextRange(e.selection.getRange()),a=u.replace(/\n\s*/," ").length,f=e.session.doc.getLine(n.row);for(var l=n.row+1;l<=i.row+1;l++){var c=r.stringTrimLeft(r.stringTrimRight(e.session.doc.getLine(l)));c.length!==0&&(c=" "+c),f+=c}i.row+10?(e.selection.moveCursorTo(n.row,n.column),e.selection.selectTo(n.row,n.column+a)):(o=e.session.doc.getLine(n.row).length>o?o+1:o,e.selection.moveCursorTo(n.row,o))},multiSelectAction:"forEach",readOnly:!0},{name:"invertSelection",bindKey:o(null,null),exec:function(e){var t=e.session.doc.getLength()-1,n=e.session.doc.getLine(t).length,r=e.selection.rangeList.ranges,i=[];r.length<1&&(r=[e.selection.getRange()]);for(var o=0;o0&&this.$blockScrolling--;var n=t&&t.scrollIntoView;if(n){switch(n){case"center-animate":n="animate";case"center":this.renderer.scrollCursorIntoView(null,.5);break;case"animate":case"cursor":this.renderer.scrollCursorIntoView();break;case"selectionPart":var r=this.selection.getRange(),i=this.renderer.layerConfig;(r.start.row>=i.lastRow||r.end.row<=i.firstRow)&&this.renderer.scrollSelectionIntoView(this.selection.anchor,this.selection.lead);break;default:}n=="animate"&&this.renderer.animateScrolling(this.curOp.scrollTop)}this.prevOp=this.curOp,this.curOp=null}},this.$mergeableCommands=["backspace","del","insertstring"],this.$historyTracker=function(e){if(!this.$mergeUndoDeltas)return;var t=this.prevOp,n=this.$mergeableCommands,r=t.command&&e.command.name==t.command.name;if(e.command.name=="insertstring"){var i=e.args;this.mergeNextCommand===undefined&&(this.mergeNextCommand=!0),r=r&&this.mergeNextCommand&&(!/\s/.test(i)||/\s/.test(t.args)),this.mergeNextCommand=!0}else r=r&&n.indexOf(e.command.name)!==-1;this.$mergeUndoDeltas!="always"&&Date.now()-this.sequenceStartTime>2e3&&(r=!1),r?this.session.mergeUndoDeltas=!0:n.indexOf(e.command.name)!==-1&&(this.sequenceStartTime=Date.now())},this.setKeyboardHandler=function(e,t){if(e&&typeof e=="string"){this.$keybindingId=e;var n=this;g.loadModule(["keybinding",e],function(r){n.$keybindingId==e&&n.keyBinding.setKeyboardHandler(r&&r.handler),t&&t()})}else this.$keybindingId=null,this.keyBinding.setKeyboardHandler(e),t&&t()},this.getKeyboardHandler=function(){return this.keyBinding.getKeyboardHandler()},this.setSession=function(e){if(this.session==e)return;this.curOp&&this.endOperation(),this.curOp={};var t=this.session;if(t){this.session.off("change",this.$onDocumentChange),this.session.off("changeMode",this.$onChangeMode),this.session.off("tokenizerUpdate",this.$onTokenizerUpdate),this.session.off("changeTabSize",this.$onChangeTabSize),this.session.off("changeWrapLimit",this.$onChangeWrapLimit),this.session.off("changeWrapMode",this.$onChangeWrapMode),this.session.off("changeFold",this.$onChangeFold),this.session.off("changeFrontMarker",this.$onChangeFrontMarker),this.session.off("changeBackMarker",this.$onChangeBackMarker),this.session.off("changeBreakpoint",this.$onChangeBreakpoint),this.session.off("changeAnnotation",this.$onChangeAnnotation),this.session.off("changeOverwrite",this.$onCursorChange),this.session.off("changeScrollTop",this.$onScrollTopChange),this.session.off("changeScrollLeft",this.$onScrollLeftChange);var n=this.session.getSelection();n.off("changeCursor",this.$onCursorChange),n.off("changeSelection",this.$onSelectionChange)}this.session=e,e?(this.$onDocumentChange=this.onDocumentChange.bind(this),e.on("change",this.$onDocumentChange),this.renderer.setSession(e),this.$onChangeMode=this.onChangeMode.bind(this),e.on("changeMode",this.$onChangeMode),this.$onTokenizerUpdate=this.onTokenizerUpdate.bind(this),e.on("tokenizerUpdate",this.$onTokenizerUpdate),this.$onChangeTabSize=this.renderer.onChangeTabSize.bind(this.renderer),e.on("changeTabSize",this.$onChangeTabSize),this.$onChangeWrapLimit=this.onChangeWrapLimit.bind(this),e.on("changeWrapLimit",this.$onChangeWrapLimit),this.$onChangeWrapMode=this.onChangeWrapMode.bind(this),e.on("changeWrapMode",this.$onChangeWrapMode),this.$onChangeFold=this.onChangeFold.bind(this),e.on("changeFold",this.$onChangeFold),this.$onChangeFrontMarker=this.onChangeFrontMarker.bind(this),this.session.on("changeFrontMarker",this.$onChangeFrontMarker),this.$onChangeBackMarker=this.onChangeBackMarker.bind(this),this.session.on("changeBackMarker",this.$onChangeBackMarker),this.$onChangeBreakpoint=this.onChangeBreakpoint.bind(this),this.session.on("changeBreakpoint",this.$onChangeBreakpoint),this.$onChangeAnnotation=this.onChangeAnnotation.bind(this),this.session.on("changeAnnotation",this.$onChangeAnnotation),this.$onCursorChange=this.onCursorChange.bind(this),this.session.on("changeOverwrite",this.$onCursorChange),this.$onScrollTopChange=this.onScrollTopChange.bind(this),this.session.on("changeScrollTop",this.$onScrollTopChange),this.$onScrollLeftChange=this.onScrollLeftChange.bind(this),this.session.on("changeScrollLeft",this.$onScrollLeftChange),this.selection=e.getSelection(),this.selection.on("changeCursor",this.$onCursorChange),this.$onSelectionChange=this.onSelectionChange.bind(this),this.selection.on("changeSelection",this.$onSelectionChange),this.onChangeMode(),this.$blockScrolling+=1,this.onCursorChange(),this.$blockScrolling-=1,this.onScrollTopChange(),this.onScrollLeftChange(),this.onSelectionChange(),this.onChangeFrontMarker(),this.onChangeBackMarker(),this.onChangeBreakpoint(),this.onChangeAnnotation(),this.session.getUseWrapMode()&&this.renderer.adjustWrapLimit(),this.renderer.updateFull()):(this.selection=null,this.renderer.setSession(e)),this._signal("changeSession",{session:e,oldSession:t}),this.curOp=null,t&&t._signal("changeEditor",{oldEditor:this}),e&&e._signal("changeEditor",{editor:this})},this.getSession=function(){return this.session},this.setValue=function(e,t){return this.session.doc.setValue(e),t?t==1?this.navigateFileEnd():t==-1&&this.navigateFileStart():this.selectAll(),e},this.getValue=function(){return this.session.getValue()},this.getSelection=function(){return this.selection},this.resize=function(e){this.renderer.onResize(e)},this.setTheme=function(e,t){this.renderer.setTheme(e,t)},this.getTheme=function(){return this.renderer.getTheme()},this.setStyle=function(e){this.renderer.setStyle(e)},this.unsetStyle=function(e){this.renderer.unsetStyle(e)},this.getFontSize=function(){return this.getOption("fontSize")||i.computedStyle(this.container,"fontSize")},this.setFontSize=function(e){this.setOption("fontSize",e)},this.$highlightBrackets=function(){this.session.$bracketHighlight&&(this.session.removeMarker(this.session.$bracketHighlight),this.session.$bracketHighlight=null);if(this.$highlightPending)return;var e=this;this.$highlightPending=!0,setTimeout(function(){e.$highlightPending=!1;var t=e.session;if(!t||!t.bgTokenizer)return;var n=t.findMatchingBracket(e.getCursorPosition());if(n)var r=new p(n.row,n.column,n.row,n.column+1);else if(t.$mode.getMatching)var r=t.$mode.getMatching(e.session);r&&(t.$bracketHighlight=t.addMarker(r,"ace_bracket","text"))},50)},this.$highlightTags=function(){if(this.$highlightTagPending)return;var e=this;this.$highlightTagPending=!0,setTimeout(function(){e.$highlightTagPending=!1;var t=e.session;if(!t||!t.bgTokenizer)return;var n=e.getCursorPosition(),r=new y(e.session,n.row,n.column),i=r.getCurrentToken();if(!i||!/\b(?:tag-open|tag-name)/.test(i.type)){t.removeMarker(t.$tagHighlight),t.$tagHighlight=null;return}if(i.type.indexOf("tag-open")!=-1){i=r.stepForward();if(!i)return}var s=i.value,o=0,u=r.stepBackward();if(u.value=="<"){do u=i,i=r.stepForward(),i&&i.value===s&&i.type.indexOf("tag-name")!==-1&&(u.value==="<"?o++:u.value==="=0)}else{do i=u,u=r.stepBackward(),i&&i.value===s&&i.type.indexOf("tag-name")!==-1&&(u.value==="<"?o++:u.value==="1)&&(t=!1)}if(e.$highlightLineMarker&&!t)e.removeMarker(e.$highlightLineMarker.id),e.$highlightLineMarker=null;else if(!e.$highlightLineMarker&&t){var n=new p(t.row,t.column,t.row,Infinity);n.id=e.addMarker(n,"ace_active-line","screenLine"),e.$highlightLineMarker=n}else t&&(e.$highlightLineMarker.start.row=t.row,e.$highlightLineMarker.end.row=t.row,e.$highlightLineMarker.start.column=t.column,e._signal("changeBackMarker"))},this.onSelectionChange=function(e){var t=this.session;t.$selectionMarker&&t.removeMarker(t.$selectionMarker),t.$selectionMarker=null;if(!this.selection.isEmpty()){var n=this.selection.getRange(),r=this.getSelectionStyle();t.$selectionMarker=t.addMarker(n,"ace_selection",r)}else this.$updateHighlightActiveLine();var i=this.$highlightSelectedWord&&this.$getSelectionHighLightRegexp();this.session.highlight(i),this._signal("changeSelection")},this.$getSelectionHighLightRegexp=function(){var e=this.session,t=this.getSelectionRange();if(t.isEmpty()||t.isMultiLine())return;var n=t.start.column-1,r=t.end.column+1,i=e.getLine(t.start.row),s=i.length,o=i.substring(Math.max(n,0),Math.min(r,s));if(n>=0&&/^[\w\d]/.test(o)||r<=s&&/[\w\d]$/.test(o))return;o=i.substring(t.start.column,t.end.column);if(!/^[\w\d]+$/.test(o))return;var u=this.$search.$assembleRegExp({wholeWord:!0,caseSensitive:!0,needle:o});return u},this.onChangeFrontMarker=function(){this.renderer.updateFrontMarkers()},this.onChangeBackMarker=function(){this.renderer.updateBackMarkers()},this.onChangeBreakpoint=function(){this.renderer.updateBreakpoints()},this.onChangeAnnotation=function(){this.renderer.setAnnotations(this.session.getAnnotations())},this.onChangeMode=function(e){this.renderer.updateText(),this._emit("changeMode",e)},this.onChangeWrapLimit=function(){this.renderer.updateFull()},this.onChangeWrapMode=function(){this.renderer.onResize(!0)},this.onChangeFold=function(){this.$updateHighlightActiveLine(),this.renderer.updateFull()},this.getSelectedText=function(){return this.session.getTextRange(this.getSelectionRange())},this.getCopyText=function(){var e=this.getSelectedText();return this._signal("copy",e),e},this.onCopy=function(){this.commands.exec("copy",this)},this.onCut=function(){this.commands.exec("cut",this)},this.onPaste=function(e,t){var n={text:e,event:t};this.commands.exec("paste",this,n)},this.$handlePaste=function(e){typeof e=="string"&&(e={text:e}),this._signal("paste",e);var t=e.text;if(!this.inMultiSelectMode||this.inVirtualSelectionMode)this.insert(t);else{var n=t.split(/\r\n|\r|\n/),r=this.selection.rangeList.ranges;if(n.length>r.length||n.length<2||!n[1])return this.commands.exec("insertstring",this,t);for(var i=r.length;i--;){var s=r[i];s.isEmpty()||this.session.remove(s),this.session.insert(s.start,n[i])}}},this.execCommand=function(e,t){return this.commands.exec(e,this,t)},this.insert=function(e,t){var n=this.session,r=n.getMode(),i=this.getCursorPosition();if(this.getBehavioursEnabled()&&!t){var s=r.transformAction(n.getState(i.row),"insertion",this,n,e);s&&(e!==s.text&&(this.session.mergeUndoDeltas=!1,this.$mergeNextCommand=!1),e=s.text)}e==" "&&(e=this.session.getTabString());if(!this.selection.isEmpty()){var o=this.getSelectionRange();i=this.session.remove(o),this.clearSelection()}else if(this.session.getOverwrite()){var o=new p.fromPoints(i,i);o.end.column+=e.length,this.session.remove(o)}if(e=="\n"||e=="\r\n"){var u=n.getLine(i.row);if(i.column>u.search(/\S|$/)){var a=u.substr(i.column).search(/\S|$/);n.doc.removeInLine(i.row,i.column,i.column+a)}}this.clearSelection();var f=i.column,l=n.getState(i.row),u=n.getLine(i.row),c=r.checkOutdent(l,u,e),h=n.insert(i,e);s&&s.selection&&(s.selection.length==2?this.selection.setSelectionRange(new p(i.row,f+s.selection[0],i.row,f+s.selection[1])):this.selection.setSelectionRange(new p(i.row+s.selection[0],s.selection[1],i.row+s.selection[2],s.selection[3])));if(n.getDocument().isNewLine(e)){var d=r.getNextLineIndent(l,u.slice(0,i.column),n.getTabString());n.insert({row:i.row+1,column:0},d)}c&&r.autoOutdent(l,n,i.row)},this.onTextInput=function(e){this.keyBinding.onTextInput(e)},this.onCommandKey=function(e,t,n){this.keyBinding.onCommandKey(e,t,n)},this.setOverwrite=function(e){this.session.setOverwrite(e)},this.getOverwrite=function(){return this.session.getOverwrite()},this.toggleOverwrite=function(){this.session.toggleOverwrite()},this.setScrollSpeed=function(e){this.setOption("scrollSpeed",e)},this.getScrollSpeed=function(){return this.getOption("scrollSpeed")},this.setDragDelay=function(e){this.setOption("dragDelay",e)},this.getDragDelay=function(){return this.getOption("dragDelay")},this.setSelectionStyle=function(e){this.setOption("selectionStyle",e)},this.getSelectionStyle=function(){return this.getOption("selectionStyle")},this.setHighlightActiveLine=function(e){this.setOption("highlightActiveLine",e)},this.getHighlightActiveLine=function(){return this.getOption("highlightActiveLine")},this.setHighlightGutterLine=function(e){this.setOption("highlightGutterLine",e)},this.getHighlightGutterLine=function(){return this.getOption("highlightGutterLine")},this.setHighlightSelectedWord=function(e){this.setOption("highlightSelectedWord",e)},this.getHighlightSelectedWord=function(){return this.$highlightSelectedWord},this.setAnimatedScroll=function(e){this.renderer.setAnimatedScroll(e)},this.getAnimatedScroll=function(){return this.renderer.getAnimatedScroll()},this.setShowInvisibles=function(e){this.renderer.setShowInvisibles(e)},this.getShowInvisibles=function(){return this.renderer.getShowInvisibles()},this.setDisplayIndentGuides=function(e){this.renderer.setDisplayIndentGuides(e)},this.getDisplayIndentGuides=function(){return this.renderer.getDisplayIndentGuides()},this.setShowPrintMargin=function(e){this.renderer.setShowPrintMargin(e)},this.getShowPrintMargin=function(){return this.renderer.getShowPrintMargin()},this.setPrintMarginColumn=function(e){this.renderer.setPrintMarginColumn(e)},this.getPrintMarginColumn=function(){return this.renderer.getPrintMarginColumn()},this.setReadOnly=function(e){this.setOption("readOnly",e)},this.getReadOnly=function(){return this.getOption("readOnly")},this.setBehavioursEnabled=function(e){this.setOption("behavioursEnabled",e)},this.getBehavioursEnabled=function(){return this.getOption("behavioursEnabled")},this.setWrapBehavioursEnabled=function(e){this.setOption("wrapBehavioursEnabled",e)},this.getWrapBehavioursEnabled=function(){return this.getOption("wrapBehavioursEnabled")},this.setShowFoldWidgets=function(e){this.setOption("showFoldWidgets",e)},this.getShowFoldWidgets=function(){return this.getOption("showFoldWidgets")},this.setFadeFoldWidgets=function(e){this.setOption("fadeFoldWidgets",e)},this.getFadeFoldWidgets=function(){return this.getOption("fadeFoldWidgets")},this.remove=function(e){this.selection.isEmpty()&&(e=="left"?this.selection.selectLeft():this.selection.selectRight());var t=this.getSelectionRange();if(this.getBehavioursEnabled()){var n=this.session,r=n.getState(t.start.row),i=n.getMode().transformAction(r,"deletion",this,n,t);if(t.end.column===0){var s=n.getTextRange(t);if(s[s.length-1]=="\n"){var o=n.getLine(t.end.row);/^\s+$/.test(o)&&(t.end.column=o.length)}}i&&(t=i)}this.session.remove(t),this.clearSelection()},this.removeWordRight=function(){this.selection.isEmpty()&&this.selection.selectWordRight(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeWordLeft=function(){this.selection.isEmpty()&&this.selection.selectWordLeft(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineStart=function(){this.selection.isEmpty()&&this.selection.selectLineStart(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineEnd=function(){this.selection.isEmpty()&&this.selection.selectLineEnd();var e=this.getSelectionRange();e.start.column==e.end.column&&e.start.row==e.end.row&&(e.end.column=0,e.end.row++),this.session.remove(e),this.clearSelection()},this.splitLine=function(){this.selection.isEmpty()||(this.session.remove(this.getSelectionRange()),this.clearSelection());var e=this.getCursorPosition();this.insert("\n"),this.moveCursorToPosition(e)},this.transposeLetters=function(){if(!this.selection.isEmpty())return;var e=this.getCursorPosition(),t=e.column;if(t===0)return;var n=this.session.getLine(e.row),r,i;tt.toLowerCase()?1:0});var r=new p(0,0,0,0);for(var i=e.first;i<=e.last;i++){var s=t.getLine(i);r.start.row=i,r.end.row=i,r.end.column=s.length,t.replace(r,n[i-e.first])}},this.toggleCommentLines=function(){var e=this.session.getState(this.getCursorPosition().row),t=this.$getSelectedRows();this.session.getMode().toggleCommentLines(e,this.session,t.first,t.last)},this.toggleBlockComment=function(){var e=this.getCursorPosition(),t=this.session.getState(e.row),n=this.getSelectionRange();this.session.getMode().toggleBlockComment(t,this.session,n,e)},this.getNumberAt=function(e,t){var n=/[\-]?[0-9]+(?:\.[0-9]+)?/g;n.lastIndex=0;var r=this.session.getLine(e);while(n.lastIndex=t){var s={value:i[0],start:i.index,end:i.index+i[0].length};return s}}return null},this.modifyNumber=function(e){var t=this.selection.getCursor().row,n=this.selection.getCursor().column,r=new p(t,n-1,t,n),i=this.session.getTextRange(r);if(!isNaN(parseFloat(i))&&isFinite(i)){var s=this.getNumberAt(t,n);if(s){var o=s.value.indexOf(".")>=0?s.start+s.value.indexOf(".")+1:s.end,u=s.start+s.value.length-o,a=parseFloat(s.value);a*=Math.pow(10,u),o!==s.end&&np+1)break;p=d.last}l--,u=this.session.$moveLines(h,p,t?0:e),t&&e==-1&&(c=l+1);while(c<=l)o[c].moveBy(u,0),c++;t||(u=0),a+=u}i.fromOrientedRange(i.ranges[0]),i.rangeList.attach(this.session),this.inVirtualSelectionMode=!1}},this.$getSelectedRows=function(e){return e=(e||this.getSelectionRange()).collapseRows(),{first:this.session.getRowFoldStart(e.start.row),last:this.session.getRowFoldEnd(e.end.row)}},this.onCompositionStart=function(e){this.renderer.showComposition(this.getCursorPosition())},this.onCompositionUpdate=function(e){this.renderer.setCompositionText(e)},this.onCompositionEnd=function(){this.renderer.hideComposition()},this.getFirstVisibleRow=function(){return this.renderer.getFirstVisibleRow()},this.getLastVisibleRow=function(){return this.renderer.getLastVisibleRow()},this.isRowVisible=function(e){return e>=this.getFirstVisibleRow()&&e<=this.getLastVisibleRow()},this.isRowFullyVisible=function(e){return e>=this.renderer.getFirstFullyVisibleRow()&&e<=this.renderer.getLastFullyVisibleRow()},this.$getVisibleRowCount=function(){return this.renderer.getScrollBottomRow()-this.renderer.getScrollTopRow()+1},this.$moveByPage=function(e,t){var n=this.renderer,r=this.renderer.layerConfig,i=e*Math.floor(r.height/r.lineHeight);this.$blockScrolling++,t===!0?this.selection.$moveSelection(function(){this.moveCursorBy(i,0)}):t===!1&&(this.selection.moveCursorBy(i,0),this.selection.clearSelection()),this.$blockScrolling--;var s=n.scrollTop;n.scrollBy(0,i*r.lineHeight),t!=null&&n.scrollCursorIntoView(null,.5),n.animateScrolling(s)},this.selectPageDown=function(){this.$moveByPage(1,!0)},this.selectPageUp=function(){this.$moveByPage(-1,!0)},this.gotoPageDown=function(){this.$moveByPage(1,!1)},this.gotoPageUp=function(){this.$moveByPage(-1,!1)},this.scrollPageDown=function(){this.$moveByPage(1)},this.scrollPageUp=function(){this.$moveByPage(-1)},this.scrollToRow=function(e){this.renderer.scrollToRow(e)},this.scrollToLine=function(e,t,n,r){this.renderer.scrollToLine(e,t,n,r)},this.centerSelection=function(){var e=this.getSelectionRange(),t={row:Math.floor(e.start.row+(e.end.row-e.start.row)/2),column:Math.floor(e.start.column+(e.end.column-e.start.column)/2)};this.renderer.alignCursor(t,.5)},this.getCursorPosition=function(){return this.selection.getCursor()},this.getCursorPositionScreen=function(){return this.session.documentToScreenPosition(this.getCursorPosition())},this.getSelectionRange=function(){return this.selection.getRange()},this.selectAll=function(){this.$blockScrolling+=1,this.selection.selectAll(),this.$blockScrolling-=1},this.clearSelection=function(){this.selection.clearSelection()},this.moveCursorTo=function(e,t){this.selection.moveCursorTo(e,t)},this.moveCursorToPosition=function(e){this.selection.moveCursorToPosition(e)},this.jumpToMatching=function(e,t){var n=this.getCursorPosition(),r=new y(this.session,n.row,n.column),i=r.getCurrentToken(),s=i||r.stepForward();if(!s)return;var o,u=!1,a={},f=n.column-s.start,l,c={")":"(","(":"(","]":"[","[":"[","{":"{","}":"{"};do{if(s.value.match(/[{}()\[\]]/g))for(;f=0;--s)this.$tryReplace(n[s],e)&&r++;return this.selection.setSelectionRange(i),this.$blockScrolling-=1,r},this.$tryReplace=function(e,t){var n=this.session.getTextRange(e);return t=this.$search.replace(n,t),t!==null?(e.end=this.session.replace(e,t),e):null},this.getLastSearchOptions=function(){return this.$search.getOptions()},this.find=function(e,t,n){t||(t={}),typeof e=="string"||e instanceof RegExp?t.needle=e:typeof e=="object"&&r.mixin(t,e);var i=this.selection.getRange();t.needle==null&&(e=this.session.getTextRange(i)||this.$search.$options.needle,e||(i=this.session.getWordRange(i.start.row,i.start.column),e=this.session.getTextRange(i)),this.$search.set({needle:e})),this.$search.set(t),t.start||this.$search.set({start:i});var s=this.$search.find(this.session);if(t.preventScroll)return s;if(s)return this.revealRange(s,n),s;t.backwards?i.start=i.end:i.end=i.start,this.selection.setRange(i)},this.findNext=function(e,t){this.find({skipCurrent:!0,backwards:!1},e,t)},this.findPrevious=function(e,t){this.find(e,{skipCurrent:!0,backwards:!0},t)},this.revealRange=function(e,t){this.$blockScrolling+=1,this.session.unfold(e),this.selection.setSelectionRange(e),this.$blockScrolling-=1;var n=this.renderer.scrollTop;this.renderer.scrollSelectionIntoView(e.start,e.end,.5),t!==!1&&this.renderer.animateScrolling(n)},this.undo=function(){this.$blockScrolling++,this.session.getUndoManager().undo(),this.$blockScrolling--,this.renderer.scrollCursorIntoView(null,.5)},this.redo=function(){this.$blockScrolling++,this.session.getUndoManager().redo(),this.$blockScrolling--,this.renderer.scrollCursorIntoView(null,.5)},this.destroy=function(){this.renderer.destroy(),this._signal("destroy",this),this.session&&this.session.destroy()},this.setAutoScrollEditorIntoView=function(e){if(!e)return;var t,n=this,r=!1;this.$scrollAnchor||(this.$scrollAnchor=document.createElement("div"));var i=this.$scrollAnchor;i.style.cssText="position:absolute",this.container.insertBefore(i,this.container.firstChild);var s=this.on("changeSelection",function(){r=!0}),o=this.renderer.on("beforeRender",function(){r&&(t=n.renderer.container.getBoundingClientRect())}),u=this.renderer.on("afterRender",function(){if(r&&t&&(n.isFocused()||n.searchBox&&n.searchBox.isFocused())){var e=n.renderer,s=e.$cursorLayer.$pixelPos,o=e.layerConfig,u=s.top-o.offset;s.top>=0&&u+t.top<0?r=!0:s.topwindow.innerHeight?r=!1:r=null,r!=null&&(i.style.top=u+"px",i.style.left=s.left+"px",i.style.height=o.lineHeight+"px",i.scrollIntoView(r)),r=t=null}});this.setAutoScrollEditorIntoView=function(e){if(e)return;delete this.setAutoScrollEditorIntoView,this.off("changeSelection",s),this.renderer.off("afterRender",u),this.renderer.off("beforeRender",o)}},this.$resetCursorStyle=function(){var e=this.$cursorStyle||"ace",t=this.renderer.$cursorLayer;if(!t)return;t.setSmoothBlinking(/smooth/.test(e)),t.isBlinking=!this.$readOnly&&e!="wide",i.setCssClass(t.element,"ace_slim-cursors",/slim/.test(e))}}).call(b.prototype),g.defineOptions(b.prototype,"editor",{selectionStyle:{set:function(e){this.onSelectionChange(),this._signal("changeSelectionStyle",{data:e})},initialValue:"line"},highlightActiveLine:{set:function(){this.$updateHighlightActiveLine()},initialValue:!0},highlightSelectedWord:{set:function(e){this.$onSelectionChange()},initialValue:!0},readOnly:{set:function(e){this.$resetCursorStyle()},initialValue:!1},cursorStyle:{set:function(e){this.$resetCursorStyle()},values:["ace","slim","smooth","wide"],initialValue:"ace"},mergeUndoDeltas:{values:[!1,!0,"always"],initialValue:!0},behavioursEnabled:{initialValue:!0},wrapBehavioursEnabled:{initialValue:!0},autoScrollEditorIntoView:{set:function(e){this.setAutoScrollEditorIntoView(e)}},keyboardHandler:{set:function(e){this.setKeyboardHandler(e)},get:function(){return this.keybindingId},handlesSet:!0},hScrollBarAlwaysVisible:"renderer",vScrollBarAlwaysVisible:"renderer",highlightGutterLine:"renderer",animatedScroll:"renderer",showInvisibles:"renderer",showPrintMargin:"renderer",printMarginColumn:"renderer",printMargin:"renderer",fadeFoldWidgets:"renderer",showFoldWidgets:"renderer",showLineNumbers:"renderer",showGutter:"renderer",displayIndentGuides:"renderer",fontSize:"renderer",fontFamily:"renderer",maxLines:"renderer",minLines:"renderer",scrollPastEnd:"renderer",fixedWidthGutter:"renderer",theme:"renderer",scrollSpeed:"$mouseHandler",dragDelay:"$mouseHandler",dragEnabled:"$mouseHandler",focusTimout:"$mouseHandler",tooltipFollowsMouse:"$mouseHandler",firstLineNumber:"session",overwrite:"session",newLineMode:"session",useWorker:"session",useSoftTabs:"session",tabSize:"session",wrap:"session",indentedSoftWrap:"session",foldStyle:"session",mode:"session"}),t.Editor=b}),define("ace/undomanager",["require","exports","module"],function(e,t,n){"use strict";var r=function(){this.reset()};(function(){function e(e){return{action:e.action,start:e.start,end:e.end,lines:e.lines.length==1?null:e.lines,text:e.lines.length==1?e.lines[0]:null}}function t(e){return{action:e.action,start:e.start,end:e.end,lines:e.lines||[e.text]}}function n(e,t){var n=new Array(e.length);for(var r=0;r0},this.hasRedo=function(){return this.$redoStack.length>0},this.markClean=function(){this.dirtyCounter=0},this.isClean=function(){return this.dirtyCounter===0},this.$serializeDeltas=function(t){return n(t,e)},this.$deserializeDeltas=function(e){return n(e,t)}}).call(r.prototype),t.UndoManager=r}),define("ace/layer/gutter",["require","exports","module","ace/lib/dom","ace/lib/oop","ace/lib/lang","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=e("../lib/oop"),s=e("../lib/lang"),o=e("../lib/event_emitter").EventEmitter,u=function(e){this.element=r.createElement("div"),this.element.className="ace_layer ace_gutter-layer",e.appendChild(this.element),this.setShowFoldWidgets(this.$showFoldWidgets),this.gutterWidth=0,this.$annotations=[],this.$updateAnnotations=this.$updateAnnotations.bind(this),this.$cells=[]};(function(){i.implement(this,o),this.setSession=function(e){this.session&&this.session.removeEventListener("change",this.$updateAnnotations),this.session=e,e&&e.on("change",this.$updateAnnotations)},this.addGutterDecoration=function(e,t){window.console&&console.warn&&console.warn("deprecated use session.addGutterDecoration"),this.session.addGutterDecoration(e,t)},this.removeGutterDecoration=function(e,t){window.console&&console.warn&&console.warn("deprecated use session.removeGutterDecoration"),this.session.removeGutterDecoration(e,t)},this.setAnnotations=function(e){this.$annotations=[];for(var t=0;to&&(v=s.end.row+1,s=t.getNextFoldLine(v,s),o=s?s.start.row:Infinity);if(v>i){while(this.$cells.length>d+1)p=this.$cells.pop(),this.element.removeChild(p.element);break}p=this.$cells[++d],p||(p={element:null,textNode:null,foldWidget:null},p.element=r.createElement("div"),p.textNode=document.createTextNode(""),p.element.appendChild(p.textNode),this.element.appendChild(p.element),this.$cells[d]=p);var m="ace_gutter-cell ";a[v]&&(m+=a[v]),f[v]&&(m+=f[v]),this.$annotations[v]&&(m+=this.$annotations[v].className),p.element.className!=m&&(p.element.className=m);var g=t.getRowLength(v)*e.lineHeight+"px";g!=p.element.style.height&&(p.element.style.height=g);if(u){var y=u[v];y==null&&(y=u[v]=t.getFoldWidget(v))}if(y){p.foldWidget||(p.foldWidget=r.createElement("span"),p.element.appendChild(p.foldWidget));var m="ace_fold-widget ace_"+y;y=="start"&&v==o&&vn.right-t.right)return"foldWidgets"}}).call(u.prototype),t.Gutter=u}),define("ace/layer/marker",["require","exports","module","ace/range","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../range").Range,i=e("../lib/dom"),s=function(e){this.element=i.createElement("div"),this.element.className="ace_layer ace_marker-layer",e.appendChild(this.element)};(function(){function e(e,t,n,r){return(e?1:0)|(t?2:0)|(n?4:0)|(r?8:0)}this.$padding=0,this.setPadding=function(e){this.$padding=e},this.setSession=function(e){this.session=e},this.setMarkers=function(e){this.markers=e},this.update=function(e){var e=e||this.config;if(!e)return;this.config=e;var t=[];for(var n in this.markers){var r=this.markers[n];if(!r.range){r.update(t,this,this.session,e);continue}var i=r.range.clipRows(e.firstRow,e.lastRow);if(i.isEmpty())continue;i=i.toScreenRange(this.session);if(r.renderer){var s=this.$getTop(i.start.row,e),o=this.$padding+i.start.column*e.characterWidth;r.renderer(t,i,o,s,e)}else r.type=="fullLine"?this.drawFullLineMarker(t,i,r.clazz,e):r.type=="screenLine"?this.drawScreenLineMarker(t,i,r.clazz,e):i.isMultiLine()?r.type=="text"?this.drawTextMarker(t,i,r.clazz,e):this.drawMultiLineMarker(t,i,r.clazz,e):this.drawSingleLineMarker(t,i,r.clazz+" ace_start"+" ace_br15",e)}this.element.innerHTML=t.join("")},this.$getTop=function(e,t){return(e-t.firstRowScreen)*t.lineHeight},this.drawTextMarker=function(t,n,i,s,o){var u=this.session,a=n.start.row,f=n.end.row,l=a,c=0,h=0,p=u.getScreenLastRowColumn(l),d=new r(l,n.start.column,l,h);for(;l<=f;l++)d.start.row=d.end.row=l,d.start.column=l==a?n.start.column:u.getRowWrapIndent(l),d.end.column=p,c=h,h=p,p=l+1p,l==f),s,l==f?0:1,o)},this.drawMultiLineMarker=function(e,t,n,r,i){var s=this.$padding,o=r.lineHeight,u=this.$getTop(t.start.row,r),a=s+t.start.column*r.characterWidth;i=i||"",e.push("
"),u=this.$getTop(t.end.row,r);var f=t.end.column*r.characterWidth;e.push("
"),o=(t.end.row-t.start.row-1)*r.lineHeight;if(o<=0)return;u=this.$getTop(t.start.row+1,r);var l=(t.start.column?1:0)|(t.end.column?0:8);e.push("
")},this.drawSingleLineMarker=function(e,t,n,r,i,s){var o=r.lineHeight,u=(t.end.column+(i||0)-t.start.column)*r.characterWidth,a=this.$getTop(t.start.row,r),f=this.$padding+t.start.column*r.characterWidth;e.push("
")},this.drawFullLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;t.start.row!=t.end.row&&(o+=this.$getTop(t.end.row,r)-s),e.push("
")},this.drawScreenLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;e.push("
")}}).call(s.prototype),t.Marker=s}),define("ace/layer/text",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/useragent","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../lib/dom"),s=e("../lib/lang"),o=e("../lib/useragent"),u=e("../lib/event_emitter").EventEmitter,a=function(e){this.element=i.createElement("div"),this.element.className="ace_layer ace_text-layer",e.appendChild(this.element),this.$updateEolChar=this.$updateEolChar.bind(this)};(function(){r.implement(this,u),this.EOF_CHAR="\u00b6",this.EOL_CHAR_LF="\u00ac",this.EOL_CHAR_CRLF="\u00a4",this.EOL_CHAR=this.EOL_CHAR_LF,this.TAB_CHAR="\u2014",this.SPACE_CHAR="\u00b7",this.$padding=0,this.$updateEolChar=function(){var e=this.session.doc.getNewLineCharacter()=="\n"?this.EOL_CHAR_LF:this.EOL_CHAR_CRLF;if(this.EOL_CHAR!=e)return this.EOL_CHAR=e,!0},this.setPadding=function(e){this.$padding=e,this.element.style.padding="0 "+e+"px"},this.getLineHeight=function(){return this.$fontMetrics.$characterSize.height||0},this.getCharacterWidth=function(){return this.$fontMetrics.$characterSize.width||0},this.$setFontMetrics=function(e){this.$fontMetrics=e,this.$fontMetrics.on("changeCharacterSize",function(e){this._signal("changeCharacterSize",e)}.bind(this)),this.$pollSizeChanges()},this.checkForSizeChanges=function(){this.$fontMetrics.checkForSizeChanges()},this.$pollSizeChanges=function(){return this.$pollSizeChangesTimer=this.$fontMetrics.$pollSizeChanges()},this.setSession=function(e){this.session=e,e&&this.$computeTabString()},this.showInvisibles=!1,this.setShowInvisibles=function(e){return this.showInvisibles==e?!1:(this.showInvisibles=e,this.$computeTabString(),!0)},this.displayIndentGuides=!0,this.setDisplayIndentGuides=function(e){return this.displayIndentGuides==e?!1:(this.displayIndentGuides=e,this.$computeTabString(),!0)},this.$tabStrings=[],this.onChangeTabSize=this.$computeTabString=function(){var e=this.session.getTabSize();this.tabSize=e;var t=this.$tabStrings=[0];for(var n=1;n"+s.stringRepeat(this.TAB_CHAR,n)+""):t.push(s.stringRepeat(" ",n));if(this.displayIndentGuides){this.$indentGuideRe=/\s\S| \t|\t |\s$/;var r="ace_indent-guide",i="",o="";if(this.showInvisibles){r+=" ace_invisible",i=" ace_invisible_space",o=" ace_invisible_tab";var u=s.stringRepeat(this.SPACE_CHAR,this.tabSize),a=s.stringRepeat(this.TAB_CHAR,this.tabSize)}else var u=s.stringRepeat(" ",this.tabSize),a=u;this.$tabStrings[" "]=""+u+"",this.$tabStrings[" "]=""+a+""}},this.updateLines=function(e,t,n){(this.config.lastRow!=e.lastRow||this.config.firstRow!=e.firstRow)&&this.scrollLines(e),this.config=e;var r=Math.max(t,e.firstRow),i=Math.min(n,e.lastRow),s=this.element.childNodes,o=0;for(var u=e.firstRow;uf&&(u=a.end.row+1,a=this.session.getNextFoldLine(u,a),f=a?a.start.row:Infinity);if(u>i)break;var l=s[o++];if(l){var c=[];this.$renderLine(c,u,!this.$useLineGroups(),u==f?a:!1),l.style.height=e.lineHeight*this.session.getRowLength(u)+"px",l.innerHTML=c.join("")}u++}},this.scrollLines=function(e){var t=this.config;this.config=e;if(!t||t.lastRow0;r--)n.removeChild(n.firstChild);if(t.lastRow>e.lastRow)for(var r=this.session.getFoldedRowCount(e.lastRow+1,t.lastRow);r>0;r--)n.removeChild(n.lastChild);if(e.firstRowt.lastRow){var i=this.$renderLinesFragment(e,t.lastRow+1,e.lastRow);n.appendChild(i)}},this.$renderLinesFragment=function(e,t,n){var r=this.element.ownerDocument.createDocumentFragment(),s=t,o=this.session.getNextFoldLine(s),u=o?o.start.row:Infinity;for(;;){s>u&&(s=o.end.row+1,o=this.session.getNextFoldLine(s,o),u=o?o.start.row:Infinity);if(s>n)break;var a=i.createElement("div"),f=[];this.$renderLine(f,s,!1,s==u?o:!1),a.innerHTML=f.join("");if(this.$useLineGroups())a.className="ace_line_group",r.appendChild(a),a.style.height=e.lineHeight*this.session.getRowLength(s)+"px";else while(a.firstChild)r.appendChild(a.firstChild);s++}return r},this.update=function(e){this.config=e;var t=[],n=e.firstRow,r=e.lastRow,i=n,s=this.session.getNextFoldLine(i),o=s?s.start.row:Infinity;for(;;){i>o&&(i=s.end.row+1,s=this.session.getNextFoldLine(i,s),o=s?s.start.row:Infinity);if(i>r)break;this.$useLineGroups()&&t.push("
"),this.$renderLine(t,i,!1,i==o?s:!1),this.$useLineGroups()&&t.push("
"),i++}this.element.innerHTML=t.join("")},this.$textToken={text:!0,rparen:!0,lparen:!0},this.$renderToken=function(e,t,n,r){var i=this,o=/\t|&|<|>|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\u3000\uFEFF\uFFF9-\uFFFC])|[\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]/g,u=function(e,n,r,o,u){if(n)return i.showInvisibles?""+s.stringRepeat(i.SPACE_CHAR,e.length)+"":e;if(e=="&")return"&";if(e=="<")return"<";if(e==">")return">";if(e==" "){var a=i.session.getScreenTabSize(t+o);return t+=a-1,i.$tabStrings[a]}if(e=="\u3000"){var f=i.showInvisibles?"ace_cjk ace_invisible ace_invisible_space":"ace_cjk",l=i.showInvisibles?i.SPACE_CHAR:"";return t+=1,""+l+""}return r?""+i.SPACE_CHAR+"":(t+=1,""+e+"")},a=r.replace(o,u);if(!this.$textToken[n.type]){var f="ace_"+n.type.replace(/\./g," ace_"),l="";n.type=="fold"&&(l=" style='width:"+n.value.length*this.config.characterWidth+"px;' "),e.push("",a,"")}else e.push(a);return t+r.length},this.renderIndentGuide=function(e,t,n){var r=t.search(this.$indentGuideRe);return r<=0||r>=n?t:t[0]==" "?(r-=r%this.tabSize,e.push(s.stringRepeat(this.$tabStrings[" "],r/this.tabSize)),t.substr(r)):t[0]==" "?(e.push(s.stringRepeat(this.$tabStrings[" "],r)),t.substr(r)):t},this.$renderWrappedLine=function(e,t,n,r){var i=0,o=0,u=n[0],a=0;for(var f=0;f=u)a=this.$renderToken(e,a,l,c.substring(0,u-i)),c=c.substring(u-i),i=u,r||e.push("