keycloak-memoizeit
Changes
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java 2(+1 -1)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java 2(+1 -1)
authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java 2(+1 -1)
authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyAdminResource.java 18(+17 -1)
core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEnforcementMode.java 40(+40 -0)
core/src/main/java/org/keycloak/representations/idm/authorization/PolicyProviderRepresentation.java 10(+4 -6)
core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java 23(+14 -9)
core/src/main/java/org/keycloak/representations/idm/authorization/ResourceOwnerRepresentation.java 10(+4 -6)
core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java 9(+4 -5)
core/src/main/java/org/keycloak/representations/idm/authorization/ResourceServerRepresentation.java 11(+4 -7)
examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java 2(+1 -1)
examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 35(+31 -4)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthorizationResource.java 61(+61 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientResource.java 2(+2 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java 56(+56 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PolicyResource.java 45(+45 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceResource.java 45(+45 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopeResource.java 46(+46 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopesResource.java 50(+50 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcesResource.java 49(+49 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java 2(+2 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java 1(+1 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedPolicy.java 2(+2 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResourceServer.java 1(+1 -0)
model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceServerAdapter.java 17(+17 -0)
model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceServerEntity.java 2(+1 -1)
server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java 11(+6 -5)
server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java 2(+1 -1)
server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java 2(+1 -1)
services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java 5(+4 -1)
services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java 12(+5 -7)
services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java 20(+18 -2)
services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java 12(+0 -12)
services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java 12(+5 -7)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java 2(+1 -1)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java 7(+4 -3)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java 2(+1 -1)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java 102(+102 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 18(+18 -0)
testsuite/integration-arquillian/test-apps/hello-world-authz-service/hello-world-authz-realm.json 48(+48 -0)
testsuite/integration-arquillian/test-apps/hello-world-authz-service/hello-world-authz-service.json 30(+30 -0)
testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/error.jsp 30(+30 -0)
testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/index.jsp 49(+49 -0)
testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json 9(+7 -2)
testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/WEB-INF/web.xml 45(+45 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.admin/Main.drl 14(+14 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.resource.owner/Main.drl 15(+15 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.user/Main.drl 14(+14 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com/photoz/authz/policy/contextual/Main.drl 15(+15 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/META-INF/kmodule.xml 21(+21 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html 31(+31 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js 168(+168 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js 60(+60 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/keycloak.json 8(+8 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular.min.js 214(+214 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-resource.min.js 13(+13 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-route.min.js 14(+14 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/jwt-decode.min.js 1(+1 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html 19(+19 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html 7(+7 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/detail.html 1(+1 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html 22(+22 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/profile.html 6(+6 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/WEB-INF/web.xml 9(+9 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java 62(+62 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 159(+159 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java 70(+70 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java 79(+79 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java 81(+81 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java 32(+32 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java 12(+12 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml 7(+7 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml 19(+19 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml 26(+26 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json 45(+45 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml 12(+12 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/web.xml 41(+41 -0)
testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json 183(+183 -0)
testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java 92(+92 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java 101(+101 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java 207(+207 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java 93(+93 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AuthorizationTest.java 58(+58 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java 290(+290 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java 185(+185 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ScopeManagementTest.java 75(+75 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java 30(+30 -0)
testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java 30(+30 -0)
Details
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
index 3ae286f..6b1fe19 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
@@ -29,7 +29,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
-import org.keycloak.representations.authorization.Permission;
+import org.keycloak.representations.idm.authorization.Permission;
import java.net.URI;
import java.util.Collections;
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index d413327..5c26124 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -30,7 +30,7 @@ import org.keycloak.authorization.client.resource.ProtectedResource;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
-import org.keycloak.representations.authorization.Permission;
+import org.keycloak.representations.idm.authorization.Permission;
import java.util.ArrayList;
import java.util.HashSet;
diff --git a/adapters/oidc/js/src/main/resources/keycloak-authz.js b/adapters/oidc/js/src/main/resources/keycloak-authz.js
index 7658352..dd18477 100644
--- a/adapters/oidc/js/src/main/resources/keycloak-authz.js
+++ b/adapters/oidc/js/src/main/resources/keycloak-authz.js
@@ -49,7 +49,7 @@
*/
this.authorize = function (wwwAuthenticateHeader) {
this.then = function (onGrant, onDeny, onError) {
- if (wwwAuthenticateHeader.startsWith('UMA')) {
+ if (wwwAuthenticateHeader.indexOf('UMA') != -1) {
var params = wwwAuthenticateHeader.split(',');
for (i = 0; i < params.length; i++) {
@@ -96,7 +96,7 @@
));
}
}
- } else if (wwwAuthenticateHeader.startsWith('KC_ETT')) {
+ } else if (wwwAuthenticateHeader.indexOf('KC_ETT') != -1) {
var params = wwwAuthenticateHeader.substring('KC_ETT'.length).trim().split(',');
var clientId = null;
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
index 7eaccb4..8fcc6f3 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
@@ -19,7 +19,7 @@ package org.keycloak.authorization.client.representation;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.representations.JsonWebToken;
-import org.keycloak.representations.authorization.Permission;
+import org.keycloak.representations.idm.authorization.Permission;
import java.util.List;
diff --git a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyAdminResource.java b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyAdminResource.java
index 1ee1d34..c6e5701 100644
--- a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyAdminResource.java
+++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyAdminResource.java
@@ -1,9 +1,25 @@
+/*
+ * 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.policy.provider.drools;
-import org.keycloak.authorization.admin.representation.PolicyRepresentation;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.kie.api.runtime.KieContainer;
import javax.ws.rs.Consumes;
diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java
index 4aa5503..05bb97d 100644
--- a/core/src/main/java/org/keycloak/AuthorizationContext.java
+++ b/core/src/main/java/org/keycloak/AuthorizationContext.java
@@ -19,7 +19,7 @@ package org.keycloak;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
-import org.keycloak.representations.authorization.Permission;
+import org.keycloak.representations.idm.authorization.Permission;
import java.util.List;
diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java
index 7d7fdea..4ef6831 100755
--- a/core/src/main/java/org/keycloak/representations/AccessToken.java
+++ b/core/src/main/java/org/keycloak/representations/AccessToken.java
@@ -19,7 +19,7 @@ package org.keycloak.representations;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
-import org.keycloak.representations.authorization.Permission;
+import org.keycloak.representations.idm.authorization.Permission;
import java.io.Serializable;
import java.util.HashMap;
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/DecisionStrategy.java b/core/src/main/java/org/keycloak/representations/idm/authorization/DecisionStrategy.java
new file mode 100644
index 0000000..bd66bea
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/DecisionStrategy.java
@@ -0,0 +1,42 @@
+/*
+ * 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.representations.idm.authorization;
+
+/**
+ * The decision strategy dictates how the policies associated with a given policy are evaluated and how a final decision
+ * is obtained.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public 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
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/Logic.java b/core/src/main/java/org/keycloak/representations/idm/authorization/Logic.java
new file mode 100644
index 0000000..70c382e
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/Logic.java
@@ -0,0 +1,36 @@
+/*
+ * 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.representations.idm.authorization;
+
+/**
+ * The decision strategy dictates how the policies associated with a given policy are evaluated and how a final decision
+ * is obtained.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public 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/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEnforcementMode.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEnforcementMode.java
new file mode 100644
index 0000000..4d1eef6
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyEnforcementMode.java
@@ -0,0 +1,40 @@
+/*
+ * 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.representations.idm.authorization;
+
+/**
+ * The policy enforcement mode dictates how authorization requests are handled by the server.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public 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/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java b/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
index 2ab8788..887a461 100644
--- a/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
+++ b/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
@@ -26,7 +26,7 @@ import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.representation.ScopeRepresentation;
import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
import org.keycloak.authorization.client.resource.ProtectedResource;
-import org.keycloak.representations.authorization.Permission;
+import org.keycloak.representations.idm.authorization.Permission;
import java.util.Set;
diff --git a/examples/authz/hello-world-authz-service/hello-world-authz-realm.json b/examples/authz/hello-world-authz-service/hello-world-authz-realm.json
index 3ab917c..022ee6f 100644
--- a/examples/authz/hello-world-authz-service/hello-world-authz-realm.json
+++ b/examples/authz/hello-world-authz-service/hello-world-authz-realm.json
@@ -1,9 +1,8 @@
{
"realm" : "hello-world-authz",
"enabled" : true,
- "privateKey" : "MIIEpQIBAAKCAQEAzMhNM9HXNQWhVf1m64zS67SIyQjj+tV5GR+MqlRTWDXdo8GAWHd+alY1urRhfRoqMy4F499+8wh2REKFykNt0ng6s6wWnEaKDboS3SAUV6lybcOAkwIOCtCZj1ItddKG3m64fzxDDQrcpkbiAvw3S8KJ4UJK+pyh9iX01duSDtM/HhPawsPdY8JSMfuo1IxQ2Vxw+8RKwbbdUeew6cyYGYAeFYwA66mlM3otB0RBHh4bjwg8297+2g53TdwM2rbCHRbrorMQD3031OTyFSp7lXCtoMLWRfAFnOP/2yZWZMXbiJheC0R3sLbU7Ef0/cUbYyk4Ckfq6pcYDR+VZBF7AwIDAQABAoIBAAwa4wVnKBOIS6srmYPfBTDNsTBBCEjxiYEErmn7JhoWxQ1DCPUxyxU6F177/q9Idqoj1FFOCtEO9P6/9+ym470HQmEQkR2Xxd1d3HOZy9oKuCro3ZbTDkVxY0JnlyxZz4MihGFxDH2e4MArfHy0sAgYbdIU+x2pWKGWSMzDd/TMSOExhc/sIQAg6ljbPCLLXCPQFAncoHRyGPrkRZs6UTZi5SJuCglVa2/3G+0drDdPuA83/mwsZfIBqQgbGbFgtq5T5C6CKMkPOQ42Rcclm7kEr6riTkJRo23EO1iOJVpxzI0tbxZsJAsW7zeqv0wWRyUgVfQAje6OdsNexp5aCtECgYEA6nMHCQ9xXvufCyzpIbYGxdAGqH6m1AR5gXerHqRiGNx+8UUt/E9cy/HTOhmZDK/eC4BT9tImeF01l1oSU/+wGKfux0SeAQchBhhq8GD6jmrtgczKAfZHp0Zrht7o9qu9KE7ZNWRmY1foJN9yNYmzY6qqHEy+zNo9amcqT7UZKO8CgYEA35sp9fMpMqkJE+NEJ9Ph/t2081BEkC0DYIuETZRSi+Ek5AliWTyEkg+oisTbWzi6fMQHS7W+M1SQP6djksLQNPP+353DKgup5gtKS+K/y2xNd7fSsNmkjW1bdJJpID7WzwwmwdahHxpcnFFuEXi5FkG3Vqmtd3cD0TYL33JlRy0CgYEA0+a3eybsDy9Zpp4m8IM3R98nxW8DlimdMLlafs2QpGvWiHdAgwWwF90wTxkHzgG+raKFQVbb0npcj7mnSyiUnxRZqt2H+eHZpUq4jR76F3LpzCGui2tvg+8QDMy4vwqmYyIxDCL8r9mqRnl3HpChBPoh2oY7BahTTjKEeZpzbR0CgYEAoNnVjX+mGzNNvGi4Fo5s/BIwoPcU20IGM+Uo/0W7O7Rx/Thi7x6BnzB0ZZ7GzRA51paNSQEsGXCzc5bOIjzR2cXLisDKK+zIAxwMDhrHLWZzM7OgdGeb38DTEUBhLzkE/VwYZUgoD1+/TxOkwhy9yCzt3gGhL1cF//GJCOwZvuECgYEAgsO4rdYScgCpsyePnHsFk+YtqtdORnmttF3JFcL3w2QneXuRwg2uW2Kfz8CVphrR9eOU0tiw38w6QTHIVeyRY8qqlHtiXj6dEYz7frh/k4hI29HwFx43rRpnAnN8kBEJYBYdbjaQ35Wsqkfu1tvHJ+6fxSwvQu/TVdGp0OfilAY=",
- "publicKey" : "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMhNM9HXNQWhVf1m64zS67SIyQjj+tV5GR+MqlRTWDXdo8GAWHd+alY1urRhfRoqMy4F499+8wh2REKFykNt0ng6s6wWnEaKDboS3SAUV6lybcOAkwIOCtCZj1ItddKG3m64fzxDDQrcpkbiAvw3S8KJ4UJK+pyh9iX01duSDtM/HhPawsPdY8JSMfuo1IxQ2Vxw+8RKwbbdUeew6cyYGYAeFYwA66mlM3otB0RBHh4bjwg8297+2g53TdwM2rbCHRbrorMQD3031OTyFSp7lXCtoMLWRfAFnOP/2yZWZMXbiJheC0R3sLbU7Ef0/cUbYyk4Ckfq6pcYDR+VZBF7AwIDAQAB",
- "certificate" : "MIICsTCCAZkCBgFVETX4AzANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFIZWxsbyBXb3JsZCBBdXRoWjAeFw0xNjA2MDIxMzAxMzdaFw0yNjA2MDIxMzAzMTdaMBwxGjAYBgNVBAMMEUhlbGxvIFdvcmxkIEF1dGhaMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMhNM9HXNQWhVf1m64zS67SIyQjj+tV5GR+MqlRTWDXdo8GAWHd+alY1urRhfRoqMy4F499+8wh2REKFykNt0ng6s6wWnEaKDboS3SAUV6lybcOAkwIOCtCZj1ItddKG3m64fzxDDQrcpkbiAvw3S8KJ4UJK+pyh9iX01duSDtM/HhPawsPdY8JSMfuo1IxQ2Vxw+8RKwbbdUeew6cyYGYAeFYwA66mlM3otB0RBHh4bjwg8297+2g53TdwM2rbCHRbrorMQD3031OTyFSp7lXCtoMLWRfAFnOP/2yZWZMXbiJheC0R3sLbU7Ef0/cUbYyk4Ckfq6pcYDR+VZBF7AwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQANm5gIT/c50lwjawM686gNXpppLA928WsCOn9NIIWjSKekP8Bf9S73kf7vWcsEppm5B8rRyRxolXmzwghv74L7uVDg8Injjgj+XbPVQP+cJqWpSaMZHF7UfWe0/4M945Xcbmsl5q+m9PmrPG0AaaZhqXHcp4ehB1H+awyRqiERpJUuwZNycw2+2kjDADpsFf8hZVUd1F6ReYyOkqUyUjbL+jYTC7ZBNa7Ok+w6HCXWgkgVATAgQXJRM3w14IOc5MH/vfMCrCl/eNQLbjGl9y7u8PKwh3MXHDO2OLqtg6hOTSrOGUPJZGmGtUAl+2/R7FzoWkML/BNe2hjsL6UJwg91",
+ "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" :
[
diff --git a/examples/authz/hello-world-authz-service/pom.xml b/examples/authz/hello-world-authz-service/pom.xml
index 5b9b646..679dd72 100755
--- a/examples/authz/hello-world-authz-service/pom.xml
+++ b/examples/authz/hello-world-authz-service/pom.xml
@@ -34,7 +34,6 @@
<name>Keycloak Authz: Hello World Example</name>
<build>
- <finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
diff --git a/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp b/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
index 697f491..75f3d6f 100644
--- a/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
+++ b/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
@@ -19,7 +19,7 @@
<%@ 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" %>
+<%@ page import="org.keycloak.representations.idm.authorization.Permission" %>
<%
KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
@@ -46,5 +46,4 @@
%>
</ul>
</body>
-</html>
-
+</html>
\ No newline at end of file
diff --git a/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json
index f303fe1..a492837 100644
--- a/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json
@@ -1,6 +1,6 @@
{
"realm": "hello-world-authz",
- "realm-public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMhNM9HXNQWhVf1m64zS67SIyQjj+tV5GR+MqlRTWDXdo8GAWHd+alY1urRhfRoqMy4F499+8wh2REKFykNt0ng6s6wWnEaKDboS3SAUV6lybcOAkwIOCtCZj1ItddKG3m64fzxDDQrcpkbiAvw3S8KJ4UJK+pyh9iX01duSDtM/HhPawsPdY8JSMfuo1IxQ2Vxw+8RKwbbdUeew6cyYGYAeFYwA66mlM3otB0RBHh4bjwg8297+2g53TdwM2rbCHRbrorMQD3031OTyFSp7lXCtoMLWRfAFnOP/2yZWZMXbiJheC0R3sLbU7Ef0/cUbYyk4Ckfq6pcYDR+VZBF7AwIDAQAB",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "http://localhost:8080/auth",
"ssl-required": "external",
"resource": "hello-world-authz-service",
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html
index f9375a2..98f0856 100755
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/index.html
@@ -13,7 +13,6 @@
<script src="http://localhost:8080/auth/js/keycloak.js"></script>
<script src="http://localhost:8080/auth/js/keycloak-authz.js"></script>
- <script src="js/security/keycloak-authorization.js" type="text/javascript"></script>
<script src="js/identity.js" type="text/javascript"></script>
<script src="js/app.js" type="text/javascript"></script>
</head>
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json b/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json
index 4b4d193..c1dee24 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json
@@ -1,12 +1,8 @@
{
"realm": "photoz",
- "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"resource" : "photoz-html5-client",
- "public-client" : true,
- "use-resource-role-mappings": "false",
- "scope" : {
- "realm" : [ "user" ]
- }
+ "public-client" : true
}
\ No newline at end of file
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html
index bb381b9..da78224 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html
@@ -10,7 +10,7 @@
<td>
<ul>
<li data-ng-repeat="p in value">
- <a href="#/album/{{p.id}}">{{p.name}}</a> - [<a href="#" ng-click="deleteAlbum(p)">X</a>]
+ <a href="#/album/{{p.id}}">{{p.name}}</a> - [<a href="#" id="delete-{{p.name}}" ng-click="deleteAlbum(p)">X</a>]
</li>
</ul>
</td>
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html
index 556693c..d9ddd25 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html
@@ -1,7 +1,7 @@
<h1>Create an Album</h1>
<form>
- Name: <input type="text" ng-model="album.name"/>
+ Name: <input type="text" id="album.name" ng-model="album.name"/>
- <button ng-click="create()">Save</button>
+ <button ng-click="create()" id="save-album">Save</button>
</form>
diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html
index 5e164b2..bd5853e 100644
--- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html
+++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/partials/home.html
@@ -1,12 +1,12 @@
<h2><span>Welcome To Photoz, {{Identity.claims.name}}</span> [<a href="" ng-click="Identity.logout()">Sign Out</a>]</h2>
-<div data-ng-show="Identity.isAdmin()"><b>Administration: </b> [<a href="#/admin/album">All Albums</a>]</div>
+<div data-ng-show="Identity.isAdmin()"><b>Administration: </b> [<a href="#/admin/album" id="admin-albums">All Albums</a>]</div>
<hr/>
<br/>
<div data-ng-show="!Identity.isAdmin()">
-<a href="#/album/create">Create Album</a> | <a href="#/profile">My Profile</a>
+<a href="#/album/create" id="create-album">Create Album</a> | <a href="#/profile">My Profile</a>
<br/>
<br/>
-<span data-ng-show="albums.length == 0">You don't have any albums, yet.</span>
+<span data-ng-show="albums.length == 0" id="resource-list-empty">You don't have any albums, yet.</span>
<table class="table" data-ng-show="albums.length > 0">
<thead>
<tr>
@@ -15,7 +15,7 @@
</thead>
<tbody>
<tr data-ng-repeat="p in albums">
- <td><a href="#/album/{{p.id}}">{{p.name}}</a> - [<a href="#" ng-click="deleteAlbum(p)">X</a>]</td>
+ <td><a href="#/album/{{p.id}}">{{p.name}}</a> - [<a href="#" id="delete-{{p.name}}" ng-click="deleteAlbum(p)">X</a>]</td>
</tr>
</tbody>
</table>
diff --git a/examples/authz/photoz/photoz-realm.json b/examples/authz/photoz/photoz-realm.json
index 342665e..b3b2b81 100644
--- a/examples/authz/photoz/photoz-realm.json
+++ b/examples/authz/photoz/photoz-realm.json
@@ -92,17 +92,19 @@
"publicClient": true,
"redirectUris": [
"/photoz-html5-client/*"
- ]
+ ],
+ "webOrigins": ["*"]
},
{
"clientId": "photoz-restful-api",
+ "secret": "secret",
"enabled": true,
"baseUrl": "/photoz-restful-api",
"authorizationServicesEnabled" : true,
"redirectUris": [
"/photoz-restful-api/*"
],
- "secret": "secret"
+ "webOrigins" : ["*"]
}
]
}
diff --git a/examples/authz/photoz/photoz-restful-api/pom.xml b/examples/authz/photoz/photoz-restful-api/pom.xml
index eea3c17..4db7bb2 100755
--- a/examples/authz/photoz/photoz-restful-api/pom.xml
+++ b/examples/authz/photoz/photoz-restful-api/pom.xml
@@ -25,6 +25,7 @@
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
+ <scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.persistence</groupId>
diff --git a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
index be2f1eb..388c9e4 100644
--- a/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
+++ b/examples/authz/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -1,11 +1,14 @@
package org.keycloak.example.photoz.album;
import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.representation.ScopeRepresentation;
import org.keycloak.authorization.client.resource.ProtectionResource;
import org.keycloak.example.photoz.ErrorResponse;
import org.keycloak.example.photoz.entity.Album;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.util.JsonSerialization;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
@@ -38,9 +41,18 @@ public class AlbumService {
@PersistenceContext
private EntityManager entityManager;
+ @Context
+ private HttpServletRequest request;
+
+ private AuthzClient authzClient;
+
+ public AlbumService() {
+
+ }
+
@POST
@Consumes("application/json")
- public Response create(@Context HttpServletRequest request, Album newAlbum) {
+ public Response create(Album newAlbum) {
Principal userPrincipal = request.getUserPrincipal();
newAlbum.setUserId(userPrincipal.getName());
@@ -78,7 +90,7 @@ public class AlbumService {
@GET
@Produces("application/json")
- public Response findAll(@Context HttpServletRequest request) {
+ public Response findAll() {
return Response.ok(this.entityManager.createQuery("from Album where userId = '" + request.getUserPrincipal().getName() + "'").getResultList()).build();
}
@@ -107,7 +119,7 @@ public class AlbumService {
albumResource.setOwner(album.getUserId());
- AuthzClient.create().protection().resource().create(albumResource);
+ getAuthzClient().protection().resource().create(albumResource);
} catch (Exception e) {
throw new RuntimeException("Could not register protected resource.", e);
}
@@ -117,7 +129,7 @@ public class AlbumService {
String uri = "/album/" + album.getId();
try {
- ProtectionResource protection = AuthzClient.create().protection();
+ ProtectionResource protection = getAuthzClient().protection();
Set<String> search = protection.resource().findByFilter("uri=" + uri);
if (search.isEmpty()) {
@@ -129,4 +141,19 @@ public class AlbumService {
throw new RuntimeException("Could not search protected resource.", e);
}
}
+
+ private AuthzClient getAuthzClient() {
+ if (this.authzClient == null) {
+ try {
+ AdapterConfig adapterConfig = JsonSerialization.readValue(this.request.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json"), AdapterConfig.class);
+ Configuration configuration = new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), null);
+
+ this.authzClient = AuthzClient.create(configuration);
+ } catch (Exception e) {
+ throw new RuntimeException("Could not create authorization client.", e);
+ }
+ }
+
+ return this.authzClient;
+ }
}
diff --git a/examples/authz/photoz/photoz-restful-api-authz-service.json b/examples/authz/photoz/photoz-restful-api-authz-service.json
index 1d0356c..ff9ee9c 100644
--- a/examples/authz/photoz/photoz-restful-api-authz-service.json
+++ b/examples/authz/photoz/photoz-restful-api-authz-service.json
@@ -152,7 +152,7 @@
}
},
{
- "name": "Delete Album Policy",
+ "name": "Delete Album Permission",
"description": "A policy that only allows the owner to delete his albums.",
"type": "scope",
"logic": "POSITIVE",
diff --git a/examples/authz/servlet-authz/src/main/webapp/index.jsp b/examples/authz/servlet-authz/src/main/webapp/index.jsp
index 78c5444..3fbfca2 100755
--- a/examples/authz/servlet-authz/src/main/webapp/index.jsp
+++ b/examples/authz/servlet-authz/src/main/webapp/index.jsp
@@ -1,6 +1,6 @@
<%@page import="org.keycloak.AuthorizationContext" %>
<%@ page import="org.keycloak.KeycloakSecurityContext" %>
-<%@ page import="org.keycloak.representations.authorization.Permission" %>
+<%@ page import="org.keycloak.representations.idm.authorization.Permission" %>
<%
KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthorizationResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthorizationResource.java
new file mode 100644
index 0000000..07276ec
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthorizationResource.java
@@ -0,0 +1,61 @@
+/*
+ * 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.admin.client.resource;
+
+import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
+
+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.MediaType;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface AuthorizationResource {
+
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ void update(ResourceServerRepresentation server);
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ ResourceServerRepresentation getSettings();
+
+ @Path("/import")
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ void importSettings(ResourceServerRepresentation server);
+
+ @Path("/settings")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ ResourceServerRepresentation exportSettings();
+
+ @Path("/resource")
+ ResourcesResource resources();
+
+ @Path("/scope")
+ ResourceScopesResource scopes();
+
+ @Path("/policy")
+ PoliciesResource policies();
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientResource.java
index ca1745d..fb9640b 100755
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ClientResource.java
@@ -142,4 +142,6 @@ public interface ClientResource {
@Produces(MediaType.APPLICATION_JSON)
GlobalRequestResult testNodesAvailable();
+ @Path("/authz/resource-server")
+ AuthorizationResource authorization();
}
\ No newline at end of file
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
new file mode 100644
index 0000000..fd5d43a
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.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.admin.client.resource;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+
+import javax.ws.rs.Consumes;
+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.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PoliciesResource {
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ Response create(PolicyRepresentation representation);
+
+ @Path("{id}")
+ PolicyResource policy(@PathParam("id") String id);
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @NoCache
+ List<PolicyRepresentation> policies();
+
+ @Path("providers")
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @NoCache
+ List<PolicyProviderRepresentation> policyProviders();
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PolicyResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PolicyResource.java
new file mode 100644
index 0000000..9a45045
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PolicyResource.java
@@ -0,0 +1,45 @@
+/*
+ * 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.admin.client.resource;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PolicyResource {
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @NoCache
+ PolicyRepresentation toRepresentation();
+
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ void update(PolicyRepresentation representation);
+
+ @DELETE
+ void remove();
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceResource.java
new file mode 100644
index 0000000..834cb06
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceResource.java
@@ -0,0 +1,45 @@
+/*
+ * 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.admin.client.resource;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ResourceResource {
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @NoCache
+ ResourceRepresentation toRepresentation();
+
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ void update(ResourceRepresentation resource);
+
+ @DELETE
+ void remove();
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopeResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopeResource.java
new file mode 100644
index 0000000..4a0ad8e
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopeResource.java
@@ -0,0 +1,46 @@
+/*
+ * 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.admin.client.resource;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ResourceScopeResource {
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ @NoCache
+ ScopeRepresentation toRepresentation();
+
+ @PUT
+ @Consumes(MediaType.APPLICATION_JSON)
+ void update(ScopeRepresentation scope);
+
+ @DELETE
+ void remove();
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopesResource.java
new file mode 100644
index 0000000..88f5c74
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopesResource.java
@@ -0,0 +1,50 @@
+/*
+ * 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.admin.client.resource;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
+import javax.ws.rs.Consumes;
+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.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ResourceScopesResource {
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ Response create(ScopeRepresentation scope);
+
+ @Path("{id}")
+ ResourceScopeResource scope(@PathParam("id") String id);
+
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ List<ScopeRepresentation> scopes();
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcesResource.java
new file mode 100644
index 0000000..1aaaa23
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcesResource.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.admin.client.resource;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+
+import javax.ws.rs.Consumes;
+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.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ResourcesResource {
+
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ Response create(ResourceRepresentation resource);
+
+ @Path("{id}")
+ ResourceResource resource(@PathParam("id") String id);
+
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ List<ResourceRepresentation> resources();
+}
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
index f1855d3..5178afc 100644
--- 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
@@ -30,6 +30,8 @@ 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 org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.Logic;
import java.util.ArrayList;
import java.util.HashSet;
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
index 5779ae1..e03f3a7 100644
--- 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
@@ -26,6 +26,7 @@ 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 org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import java.util.ArrayList;
import java.util.List;
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
index 6c6230b..fd2b488 100644
--- 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
@@ -23,6 +23,8 @@ 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 org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.Logic;
import java.util.HashMap;
import java.util.Map;
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
index fe59510..08a425a 100644
--- 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
@@ -19,6 +19,7 @@
package org.keycloak.models.authorization.infinispan.entities;
import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
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
index ddaf637..a5a6b27 100644
--- 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
@@ -22,6 +22,8 @@ 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 org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.Logic;
import javax.persistence.Access;
import javax.persistence.AccessType;
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
index b74b231..a0be18a 100644
--- 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
@@ -19,6 +19,7 @@
package org.keycloak.authorization.jpa.entities;
import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import javax.persistence.Access;
import javax.persistence.AccessType;
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
index 38cb87b..2b28f16 100644
--- 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
@@ -1,3 +1,19 @@
+/*
+ * 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.mongo.adapter;
import org.keycloak.authorization.AuthorizationProvider;
@@ -8,6 +24,8 @@ import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.mongo.entities.PolicyEntity;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.mongo.keycloak.adapters.AbstractMongoAdapter;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.Logic;
import java.util.Map;
import java.util.Set;
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
index 72feedb..1bfbf3f 100644
--- 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
@@ -1,9 +1,26 @@
+/*
+ * 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.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;
+import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
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
index 9230b88..c489542 100644
--- 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
@@ -18,12 +18,12 @@
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 org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.Logic;
import java.util.HashMap;
import java.util.HashSet;
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
index 7013e1b..8167c42 100644
--- 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
@@ -18,11 +18,11 @@
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;
+import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
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
index 1960d6a..03596d9 100644
--- a/server-spi/src/main/java/org/keycloak/authorization/model/Policy.java
+++ b/server-spi/src/main/java/org/keycloak/authorization/model/Policy.java
@@ -18,6 +18,9 @@
package org.keycloak.authorization.model;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.Logic;
+
import java.util.Map;
import java.util.Set;
@@ -152,42 +155,4 @@ public interface Policy {
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/ResourceServer.java b/server-spi/src/main/java/org/keycloak/authorization/model/ResourceServer.java
index 2424c8d..d5b9ac4 100644
--- a/server-spi/src/main/java/org/keycloak/authorization/model/ResourceServer.java
+++ b/server-spi/src/main/java/org/keycloak/authorization/model/ResourceServer.java
@@ -18,6 +18,8 @@
package org.keycloak.authorization.model;
+import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
+
/**
* 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.
@@ -68,24 +70,4 @@ public interface ResourceServer {
* @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/policy/evaluation/DecisionResultCollector.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
index f06eb3f..abd3f93 100644
--- 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
@@ -21,6 +21,7 @@ package org.keycloak.authorization.policy.evaluation;
import org.keycloak.authorization.Decision;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
import java.util.HashMap;
import java.util.List;
@@ -81,17 +82,17 @@ public abstract class DecisionResultCollector implements Decision<DefaultEvaluat
}
Policy policy = policyResult.getPolicy();
- Policy.DecisionStrategy decisionStrategy = policy.getDecisionStrategy();
+ DecisionStrategy decisionStrategy = policy.getDecisionStrategy();
if (decisionStrategy == null) {
- decisionStrategy = Policy.DecisionStrategy.UNANIMOUS;
+ decisionStrategy = DecisionStrategy.UNANIMOUS;
}
- if (Policy.DecisionStrategy.AFFIRMATIVE.equals(decisionStrategy) && grantCount > 0) {
+ if (DecisionStrategy.AFFIRMATIVE.equals(decisionStrategy) && grantCount > 0) {
return true;
- } else if (Policy.DecisionStrategy.UNANIMOUS.equals(decisionStrategy) && denyCount == 0) {
+ } else if (DecisionStrategy.UNANIMOUS.equals(decisionStrategy) && denyCount == 0) {
return true;
- } else if (Policy.DecisionStrategy.CONSENSUS.equals(decisionStrategy)) {
+ } else if (DecisionStrategy.CONSENSUS.equals(decisionStrategy)) {
if (grantCount > denyCount) {
return true;
}
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
index df379af..0bd5b6c 100644
--- 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
@@ -21,8 +21,8 @@ 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;
+import org.keycloak.representations.idm.authorization.Logic;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
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
index 8b12558..e2ef2f9 100644
--- 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
@@ -23,13 +23,13 @@ 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 org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import java.util.HashMap;
import java.util.List;
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 0bec462..2516105 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
@@ -78,13 +78,13 @@ import org.keycloak.representations.idm.UserConsentRepresentation;
import org.keycloak.representations.idm.UserFederationMapperRepresentation;
import org.keycloak.representations.idm.UserFederationProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyEnforcementMode;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -1002,7 +1002,7 @@ public class RepresentationToModel {
ResourceServer resourceServer = resourceServerStore.create(client.getId());
resourceServer.setAllowRemoteResourceManagement(true);
- resourceServer.setPolicyEnforcementMode(ResourceServer.PolicyEnforcementMode.ENFORCING);
+ resourceServer.setPolicyEnforcementMode(PolicyEnforcementMode.ENFORCING);
}
return client;
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
index c34893f..1b54d56 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
@@ -18,10 +18,9 @@
package org.keycloak.authorization.admin;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.jboss.resteasy.annotations.cache.NoCache;
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;
@@ -31,6 +30,8 @@ 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 org.keycloak.representations.idm.authorization.PolicyProviderRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.services.resources.admin.RealmAuth;
import javax.ws.rs.Consumes;
@@ -67,6 +68,7 @@ public class PolicyService {
@POST
@Consumes("application/json")
@Produces("application/json")
+ @NoCache
public Response create(PolicyRepresentation representation) {
this.auth.requireManage();
Policy policy = Models.toModel(representation, this.resourceServer, authorization);
@@ -94,6 +96,7 @@ public class PolicyService {
@PUT
@Consumes("application/json")
@Produces("application/json")
+ @NoCache
public Response update(@PathParam("id") String id, PolicyRepresentation representation) {
this.auth.requireManage();
representation.setId(id);
@@ -161,6 +164,7 @@ public class PolicyService {
@Path("{id}")
@GET
@Produces("application/json")
+ @NoCache
public Response findById(@PathParam("id") String id) {
this.auth.requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
@@ -175,6 +179,7 @@ public class PolicyService {
@GET
@Produces("application/json")
+ @NoCache
public Response findAll() {
this.auth.requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
@@ -188,6 +193,7 @@ public class PolicyService {
@Path("providers")
@GET
@Produces("application/json")
+ @NoCache
public Response findPolicyProviders() {
this.auth.requireView();
return Response.ok(
@@ -292,7 +298,7 @@ public class PolicyService {
boolean hasPolicy = false;
for (Policy policyModel : new HashSet<Policy>(policy.getAssociatedPolicies())) {
- if (policyModel.getId().equals(policyId)) {
+ if (policyModel.getId().equals(policyId) || policyModel.getName().equals(policyId)) {
hasPolicy = true;
}
}
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
index 57b3e4e..ce1fe84 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java
@@ -28,7 +28,10 @@ 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 org.keycloak.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import java.util.ArrayList;
import java.util.List;
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
index 84e5295..5feb31c 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
@@ -21,11 +21,6 @@ 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;
@@ -42,6 +37,13 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserFederationManager;
import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.resources.admin.RealmAuth;
import org.keycloak.util.JsonSerialization;
@@ -191,212 +193,207 @@ public class ResourceServerService {
return Response.ok(settings).build();
}
+ @Path("/import")
@POST
- @Consumes(MediaType.MULTIPART_FORM_DATA)
- public Response importSettings(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response importSettings(@Context final UriInfo uriInfo, ResourceServerRepresentation rep) throws IOException {
this.auth.requireManage();
- Map<String, List<InputPart>> uploadForm = input.getFormDataMap();
- List<InputPart> inputParts = uploadForm.get("file");
- for (InputPart inputPart : inputParts) {
- ResourceServerRepresentation rep = JsonSerialization.readValue(inputPart.getBodyAsString(), ResourceServerRepresentation.class);
+ resourceServer.setPolicyEnforcementMode(rep.getPolicyEnforcementMode());
+ resourceServer.setAllowRemoteResourceManagement(rep.isAllowRemoteResourceManagement());
- 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);
+ 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);
+ ResteasyProviderFactory.getInstance().injectProperties(scopeResource);
- rep.getScopes().forEach(scope -> {
- Scope existing = scopeStore.findByName(scope.getName(), resourceServer.getId());
+ rep.getScopes().forEach(scope -> {
+ Scope existing = scopeStore.findByName(scope.getName(), resourceServer.getId());
- if (existing != null) {
- scopeResource.update(existing.getId(), scope);
- } else {
- scopeResource.create(scope);
- }
- });
+ if (existing != null) {
+ scopeResource.update(existing.getId(), scope);
+ } else {
+ scopeResource.create(scope);
+ }
+ });
- ResourceSetService resourceSetResource = new ResourceSetService(resourceServer, this.authorization, this.auth);
+ ResourceSetService resourceSetResource = new ResourceSetService(resourceServer, this.authorization, this.auth);
- rep.getResources().forEach(resourceRepresentation -> {
- ResourceOwnerRepresentation owner = resourceRepresentation.getOwner();
+ rep.getResources().forEach(resourceRepresentation -> {
+ ResourceOwnerRepresentation owner = resourceRepresentation.getOwner();
- if (owner == null) {
- owner = new ResourceOwnerRepresentation();
- }
+ if (owner == null) {
+ owner = new ResourceOwnerRepresentation();
+ }
- owner.setId(resourceServer.getClientId());
+ owner.setId(resourceServer.getClientId());
- if (owner.getName() != null) {
- UserModel user = this.session.users().getUserByUsername(owner.getName(), this.realm);
+ if (owner.getName() != null) {
+ UserModel user = this.session.users().getUserByUsername(owner.getName(), this.realm);
- if (user != null) {
- owner.setId(user.getId());
- }
+ 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, this.auth);
+ Resource existing = resourceStore.findByName(resourceRepresentation.getName(), this.resourceServer.getId());
- ResteasyProviderFactory.getInstance().injectProperties(policyResource);
+ if (existing != null) {
+ resourceSetResource.update(existing.getId(), resourceRepresentation);
+ } else {
+ resourceSetResource.create(resourceRepresentation);
+ }
+ });
- rep.getPolicies().forEach(policyRepresentation -> {
- Map<String, String> config = policyRepresentation.getConfig();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ PolicyService policyResource = new PolicyService(resourceServer, this.authorization, this.auth);
- String roles = config.get("roles");
+ ResteasyProviderFactory.getInstance().injectProperties(policyResource);
- if (roles != null && !roles.isEmpty()) {
- roles = roles.replace("[", "");
- roles = roles.replace("]", "");
+ rep.getPolicies().forEach(policyRepresentation -> {
+ Map<String, String> config = policyRepresentation.getConfig();
- if (!roles.isEmpty()) {
- String roleNames = "";
+ String roles = config.get("roles");
- for (String role : roles.split(",")) {
- if (!roleNames.isEmpty()) {
- roleNames = roleNames + ",";
- }
+ if (roles != null && !roles.isEmpty()) {
+ roles = roles.replace("[", "");
+ roles = roles.replace("]", "");
- role = role.replace("\"", "");
+ if (!roles.isEmpty()) {
+ String roleNames = "";
- roleNames = roleNames + "\"" + this.realm.getRole(role).getId() + "\"";
+ for (String role : roles.split(",")) {
+ if (!roleNames.isEmpty()) {
+ roleNames = roleNames + ",";
}
- config.put("roles", "[" + roleNames + "]");
- }
- }
+ role = role.replace("\"", "");
- String users = config.get("users");
+ roleNames = roleNames + "\"" + this.realm.getRole(role).getId() + "\"";
+ }
- if (users != null) {
- users = users.replace("[", "");
- users = users.replace("]", "");
+ config.put("roles", "[" + roleNames + "]");
+ }
+ }
- if (!users.isEmpty()) {
- String userNames = "";
+ String users = config.get("users");
- for (String user : users.split(",")) {
- if (!userNames.isEmpty()) {
- userNames = userNames + ",";
- }
+ if (users != null) {
+ users = users.replace("[", "");
+ users = users.replace("]", "");
- user = user.replace("\"", "");
+ if (!users.isEmpty()) {
+ String userNames = "";
- userNames = userNames + "\"" + this.session.users().getUserByUsername(user, this.realm).getId() + "\"";
+ for (String user : users.split(",")) {
+ if (!userNames.isEmpty()) {
+ userNames = userNames + ",";
}
- config.put("users", "[" + userNames + "]");
+ user = user.replace("\"", "");
+
+ userNames = userNames + "\"" + this.session.users().getUserByUsername(user, this.realm).getId() + "\"";
}
- }
- String scopes = config.get("scopes");
+ config.put("users", "[" + userNames + "]");
+ }
+ }
- if (scopes != null && !scopes.isEmpty()) {
- scopes = scopes.replace("[", "");
- scopes = scopes.replace("]", "");
+ String scopes = config.get("scopes");
- if (!scopes.isEmpty()) {
- String scopeNames = "";
+ if (scopes != null && !scopes.isEmpty()) {
+ scopes = scopes.replace("[", "");
+ scopes = scopes.replace("]", "");
- for (String scope : scopes.split(",")) {
- if (!scopeNames.isEmpty()) {
- scopeNames = scopeNames + ",";
- }
+ if (!scopes.isEmpty()) {
+ String scopeNames = "";
- scope = scope.replace("\"", "");
+ for (String scope : scopes.split(",")) {
+ if (!scopeNames.isEmpty()) {
+ scopeNames = scopeNames + ",";
+ }
- Scope newScope = scopeStore.findByName(scope, resourceServer.getId());
+ scope = scope.replace("\"", "");
- if (newScope == null) {
- throw new RuntimeException("Scope with name [" + scope + "] not defined.");
- }
+ Scope newScope = scopeStore.findByName(scope, resourceServer.getId());
- scopeNames = scopeNames + "\"" + newScope.getId() + "\"";
+ if (newScope == null) {
+ throw new RuntimeException("Scope with name [" + scope + "] not defined.");
}
- config.put("scopes", "[" + scopeNames + "]");
+ scopeNames = scopeNames + "\"" + newScope.getId() + "\"";
}
- }
- String policyResources = config.get("resources");
+ config.put("scopes", "[" + scopeNames + "]");
+ }
+ }
- if (policyResources != null && !policyResources.isEmpty()) {
- policyResources = policyResources.replace("[", "");
- policyResources = policyResources.replace("]", "");
+ String policyResources = config.get("resources");
- if (!policyResources.isEmpty()) {
- String resourceNames = "";
+ if (policyResources != null && !policyResources.isEmpty()) {
+ policyResources = policyResources.replace("[", "");
+ policyResources = policyResources.replace("]", "");
- for (String resource : policyResources.split(",")) {
- if (!resourceNames.isEmpty()) {
- resourceNames = resourceNames + ",";
- }
+ if (!policyResources.isEmpty()) {
+ String resourceNames = "";
- resource = resource.replace("\"", "");
+ for (String resource : policyResources.split(",")) {
+ if (!resourceNames.isEmpty()) {
+ resourceNames = resourceNames + ",";
+ }
- if ("".equals(resource)) {
- continue;
- }
+ resource = resource.replace("\"", "");
- resourceNames = resourceNames + "\"" + storeFactory.getResourceStore().findByName(resource, resourceServer.getId()).getId() + "\"";
+ if ("".equals(resource)) {
+ continue;
}
- config.put("resources", "[" + resourceNames + "]");
+ resourceNames = resourceNames + "\"" + storeFactory.getResourceStore().findByName(resource, resourceServer.getId()).getId() + "\"";
}
- }
- String applyPolicies = config.get("applyPolicies");
+ config.put("resources", "[" + resourceNames + "]");
+ }
+ }
- if (applyPolicies != null && !applyPolicies.isEmpty()) {
- applyPolicies = applyPolicies.replace("[", "");
- applyPolicies = applyPolicies.replace("]", "");
+ String applyPolicies = config.get("applyPolicies");
- if (!applyPolicies.isEmpty()) {
- String policyNames = "";
+ if (applyPolicies != null && !applyPolicies.isEmpty()) {
+ applyPolicies = applyPolicies.replace("[", "");
+ applyPolicies = applyPolicies.replace("]", "");
- for (String pId : applyPolicies.split(",")) {
- if (!policyNames.isEmpty()) {
- policyNames = policyNames + ",";
- }
+ if (!applyPolicies.isEmpty()) {
+ String policyNames = "";
- pId = pId.replace("\"", "").trim();
+ for (String pId : applyPolicies.split(",")) {
+ if (!policyNames.isEmpty()) {
+ policyNames = policyNames + ",";
+ }
- Policy policy = policyStore.findByName(pId, resourceServer.getId());
+ pId = pId.replace("\"", "").trim();
- if (policy == null) {
- throw new RuntimeException("Policy with name [" + pId + "] not defined.");
- }
+ Policy policy = policyStore.findByName(pId, resourceServer.getId());
- policyNames = policyNames + "\"" + policy.getId() + "\"";
+ if (policy == null) {
+ throw new RuntimeException("Policy with name [" + pId + "] not defined.");
}
- config.put("applyPolicies", "[" + policyNames + "]");
+ policyNames = policyNames + "\"" + policy.getId() + "\"";
}
+
+ config.put("applyPolicies", "[" + policyNames + "]");
}
+ }
- Policy existing = policyStore.findByName(policyRepresentation.getName(), this.resourceServer.getId());
+ Policy existing = policyStore.findByName(policyRepresentation.getName(), this.resourceServer.getId());
- if (existing != null) {
- policyResource.update(existing.getId(), policyRepresentation);
- } else {
- policyResource.create(policyRepresentation);
- }
- });
- }
+ if (existing != null) {
+ policyResource.update(existing.getId(), policyRepresentation);
+ } else {
+ policyResource.create(policyRepresentation);
+ }
+ });
return Response.noContent().build();
}
@@ -434,8 +431,8 @@ public class ResourceServerService {
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);
+ defaultPermission.setDecisionStrategy(DecisionStrategy.UNANIMOUS);
+ defaultPermission.setLogic(Logic.POSITIVE);
HashMap<String, String> defaultPermissionConfig = new HashMap<>();
@@ -454,8 +451,8 @@ public class ResourceServerService {
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);
+ defaultPolicy.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+ defaultPolicy.setLogic(Logic.POSITIVE);
HashMap<String, String> defaultPolicyConfig = new HashMap<>();
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index c9b30b2..9078408 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -17,9 +17,8 @@
*/
package org.keycloak.authorization.admin;
+import org.jboss.resteasy.annotations.cache.NoCache;
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;
@@ -27,6 +26,8 @@ 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.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.RealmAuth;
@@ -136,6 +137,7 @@ public class ResourceSetService {
@Path("{id}")
@GET
+ @NoCache
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
requireView();
@@ -150,6 +152,7 @@ public class ResourceSetService {
}
@GET
+ @NoCache
@Produces("application/json")
public Response findAll() {
requireView();
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
index 56291c8..08bbed9 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
@@ -18,13 +18,13 @@
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.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.admin.RealmAuth;
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
index abdd980..60dc51e 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/util/Models.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/util/Models.java
@@ -20,11 +20,6 @@ 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;
@@ -36,6 +31,11 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.util.JsonSerialization;
@@ -162,13 +162,22 @@ public final class Models {
return representation1;
}).collect(Collectors.toList()));
- List<String> obj = model.getAssociatedPolicies().stream().map(new Function<Policy, String>() {
- @Override
- public String apply(Policy policy) {
- return policy.getId();
- }
+ List<PolicyRepresentation> associatedPolicies = new ArrayList<>();
+
+ List<String> obj = model.getAssociatedPolicies().stream().map(policy -> {
+ PolicyRepresentation representation1 = new PolicyRepresentation();
+
+ representation1.setId(policy.getId());
+ representation1.setName(policy.getName());
+ representation1.setType(policy.getType());
+
+ associatedPolicies.add(representation1);
+
+ return policy.getId();
}).collect(Collectors.toList());
+ representation.setAssociatedPolicies(associatedPolicies);
+
try {
representation.getConfig().put("applyPolicies", JsonSerialization.writeValueAsString(obj));
} catch (IOException e) {
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
index ad154a6..405675a 100644
--- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
+++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
@@ -1,13 +1,12 @@
/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2016 Red Hat, Inc., and individual contributors
- * as indicated by the @author tags.
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,13 +14,11 @@
* 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;
@@ -39,7 +36,8 @@ 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.representations.idm.authorization.Permission;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.Cors;
diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
index df6f54d..ccc457d 100644
--- a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
+++ b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
@@ -39,7 +39,7 @@ 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.representations.idm.authorization.Permission;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.Cors;
@@ -182,9 +182,8 @@ public class EntitlementService {
AccessToken.Authorization authorization = new AccessToken.Authorization();
authorization.setPermissions(permissions);
-
accessToken.setAuthorization(authorization);
- ;
+
return new TokenManager().encodeToken(realm, accessToken);
}
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
index cf2f9e0..910cee5 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
@@ -1,8 +1,22 @@
+/*
+ * 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.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;
@@ -11,6 +25,8 @@ import org.keycloak.authorization.protection.permission.representation.Permissio
import org.keycloak.authorization.protection.permission.representation.PermissionResponse;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
import javax.ws.rs.core.Response;
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
index 9d54730..4f2181f 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java
@@ -18,27 +18,15 @@
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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
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
index 9ee6368..8726ce6 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java
@@ -18,9 +18,9 @@
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 org.keycloak.representations.idm.authorization.ResourceRepresentation;
import java.util.ArrayList;
import java.util.List;
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
index f4aaac5..e45b976 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
@@ -19,15 +19,15 @@ 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.representations.idm.authorization.ResourceOwnerRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
import javax.ws.rs.Consumes;
diff --git a/services/src/main/java/org/keycloak/authorization/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
index 43204b8..4d84b03 100644
--- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java
+++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
@@ -28,7 +28,7 @@ import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.StoreFactory;
-import org.keycloak.representations.authorization.Permission;
+import org.keycloak.representations.idm.authorization.Permission;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java
index 0786eab..31b221b 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java
@@ -1,13 +1,12 @@
/*
- * JBoss, Home of Professional Open Source.
- * Copyright 2016 Red Hat, Inc., and individual contributors
- * as indicated by the @author tags.
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -15,7 +14,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package org.keycloak.testsuite.authorization;
import org.apache.commons.collections.map.HashedMap;
@@ -23,8 +21,6 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.junit.Before;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.Decision;
-import org.keycloak.authorization.admin.representation.ResourceRepresentation;
-import org.keycloak.authorization.admin.representation.ScopeRepresentation;
import org.keycloak.authorization.common.KeycloakEvaluationContext;
import org.keycloak.authorization.common.KeycloakIdentity;
import org.keycloak.authorization.model.Policy;
@@ -42,6 +38,8 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.client.Invocation;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java
index f323265..4a6f9b6 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java
@@ -19,8 +19,8 @@
package org.keycloak.testsuite.authorization;
import org.junit.Test;
-import org.keycloak.authorization.admin.representation.ResourceRepresentation;
import org.keycloak.authorization.model.Resource;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation.Builder;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
index a4cc551..50ab943 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
@@ -21,12 +21,13 @@ package org.keycloak.testsuite.authorization;
import org.apache.commons.collections.map.HashedMap;
import org.junit.Test;
import org.keycloak.authorization.Decision.Effect;
-import org.keycloak.authorization.admin.representation.PolicyRepresentation;
-import org.keycloak.authorization.admin.representation.ResourceRepresentation;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.client.Entity;
@@ -329,7 +330,7 @@ public class ResourcePermissionManagementTest extends AbstractPhotozAdminTest {
newPermission.setName("Album Resource Policy");
newPermission.setType("resource");
- newPermission.setDecisionStrategy(Policy.DecisionStrategy.AFFIRMATIVE);
+ newPermission.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
HashedMap config = new HashedMap();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
index 839a813..4566fe6 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
@@ -19,8 +19,8 @@
package org.keycloak.testsuite.authorization;
import org.junit.Test;
-import org.keycloak.authorization.admin.representation.ScopeRepresentation;
import org.keycloak.authorization.model.Scope;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation.Builder;
diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml
index ed794f1..90f6ecf 100644
--- a/testsuite/integration-arquillian/pom.xml
+++ b/testsuite/integration-arquillian/pom.xml
@@ -33,7 +33,7 @@
<packaging>pom</packaging>
<name>Keycloak Arquillian Integration TestSuite</name>
-
+
<properties>
<containers.home>${project.build.directory}/containers</containers.home>
@@ -48,6 +48,9 @@
<arquillian-wildfly-container.version>2.0.0.Final</arquillian-wildfly-container.version>
<version.shrinkwrap.resolvers>2.2.2</version.shrinkwrap.resolvers>
<undertow-embedded.version>1.0.0.Alpha2</undertow-embedded.version>
+
+ <maven.compiler.target>1.8</maven.compiler.target>
+ <maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencyManagement>
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java
new file mode 100644
index 0000000..9382f20
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/authorization/TestPolicyProviderFactory.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.authorization;
+
+import org.keycloak.Config;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+import static org.bouncycastle.asn1.x500.style.RFC4519Style.l;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class TestPolicyProviderFactory implements PolicyProviderFactory {
+
+ @Override
+ public String getName() {
+ return "Test";
+ }
+
+ @Override
+ public String getGroup() {
+ return "Test Suite";
+ }
+
+ @Override
+ public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
+ return new TestPolicyProvider(policy, authorization);
+ }
+
+ @Override
+ public PolicyProviderAdminService getAdminResource(ResourceServer resourceServer) {
+ return null;
+ }
+
+ @Override
+ public PolicyProvider create(KeycloakSession session) {
+ return null;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "test";
+ }
+
+ private class TestPolicyProvider implements PolicyProvider {
+
+ private final Policy policy;
+ private final AuthorizationProvider authorization;
+
+ public TestPolicyProvider(Policy policy, AuthorizationProvider authorization) {
+ this.policy = policy;
+ this.authorization = authorization;
+ }
+
+ @Override
+ public void evaluate(Evaluation evaluation) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
new file mode 100644
index 0000000..ae41cc1
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -0,0 +1,18 @@
+#
+# 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.testsuite.authorization.TestPolicyProviderFactory
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/hello-world-authz-realm.json b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/hello-world-authz-realm.json
new file mode 100644
index 0000000..022ee6f
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/hello-world-authz-realm.json
@@ -0,0 +1,48 @@
+{
+ "realm" : "hello-world-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" : ["uma_authorization"]
+ },
+ {
+ "username" : "jdoe",
+ "enabled" : true,
+ "credentials" : [ {
+ "type" : "password",
+ "value" : "jdoe"
+ } ],
+ "realmRoles" : ["uma_authorization"]
+ },
+ {
+ "username" : "service-account-hello-world-authz-service",
+ "enabled" : true,
+ "serviceAccountClientId" : "hello-world-authz-service",
+ "clientRoles": {
+ "hello-world-authz-service" : ["uma_protection"]
+ }
+ }
+ ],
+ "clients" : [
+ {
+ "clientId" : "hello-world-authz-service",
+ "secret" : "secret",
+ "authorizationServicesEnabled" : true,
+ "enabled" : true,
+ "redirectUris" : [ "http://localhost:8080/hello-world-authz-service/*" ],
+ "baseUrl": "http://localhost:8080/hello-world-authz-service",
+ "adminUrl": "http://localhost:8080/hello-world-authz-service",
+ "directAccessGrantsEnabled" : true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/hello-world-authz-service.json b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/hello-world-authz-service.json
new file mode 100644
index 0000000..ea56e62
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/hello-world-authz-service.json
@@ -0,0 +1,30 @@
+{
+ "resources": [
+ {
+ "name": "Default Resource",
+ "uri": "/*",
+ "type": "urn:hello-world-authz-service:resources:default"
+ }
+ ],
+ "policies": [
+ {
+ "name": "Only From Realm Policy",
+ "description": "A policy that grants access only for users within this realm",
+ "type": "js",
+ "config": {
+ "applyPolicies": "[]",
+ "code": "var context = $evaluation.getContext();\n\n// using attributes from the evaluation context to obtain the realm\nvar contextAttributes = context.getAttributes();\nvar realmName = contextAttributes.getValue('kc.realm.name').asString(0);\n\n// using attributes from the identity to obtain the issuer\nvar identity = context.getIdentity();\nvar identityAttributes = identity.getAttributes();\nvar issuer = identityAttributes.getValue('iss').asString(0);\n\n// only users from the realm have access granted \nif (issuer.endsWith(realmName)) {\n $evaluation.grant();\n}"
+ }
+ },
+ {
+ "name": "Default Permission",
+ "description": "A permission that applies to the default resource type",
+ "type": "resource",
+ "config": {
+ "defaultResourceType": "urn:hello-world-authz-service:resources:default",
+ "default": "true",
+ "applyPolicies": "[\"Only From Realm Policy\"]"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml
new file mode 100755
index 0000000..c0a1882
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-test-apps</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>hello-world-authz-service</artifactId>
+ <packaging>war</packaging>
+
+ <name>Keycloak Authz Tests: Hello World Example</name>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.jboss.as.plugins</groupId>
+ <artifactId>jboss-as-maven-plugin</artifactId>
+ <configuration>
+ <skip>false</skip>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.wildfly.plugins</groupId>
+ <artifactId>wildfly-maven-plugin</artifactId>
+ <configuration>
+ <skip>false</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/error.jsp b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/error.jsp
new file mode 100644
index 0000000..00d25b3
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/error.jsp
@@ -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.
+ ~
+ --%>
+
+<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
+<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
+
+<html>
+<body>
+<h2><a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+ .queryParam("redirect_uri", "http://localhost:8080/hello-world-authz-service").build("hello-world-authz").toString()%>">Logout</a></h2>
+
+<h3>Access Denied !</h3>
+</body>
+</html>
+
diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/index.jsp b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/index.jsp
new file mode 100644
index 0000000..75f3d6f
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/index.jsp
@@ -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.
+ ~
+ --%>
+<%@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.idm.authorization.Permission" %>
+
+<%
+ KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
+ AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext();
+%>
+<html>
+<body>
+<h2>Welcome !</h2>
+<h2><a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+ .queryParam("redirect_uri", "http://localhost:8080/hello-world-authz-service").build("hello-world-authz").toString()%>">Logout</a></h2>
+
+<h3>Your permissions are:</h3>
+
+<ul>
+ <%
+ for (Permission permission : authzContext.getPermissions()) {
+ %>
+ <li>
+ <p>Resource: <%= permission.getResourceSetName() %></p>
+ <p>ID: <%= permission.getResourceSetId() %></p>
+ </li>
+ <%
+ }
+ %>
+</ul>
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..1ca06be
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/hello-world-authz-service/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ version="3.0">
+
+ <module-name>hello-world-authz-service</module-name>
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>All Resources</web-resource-name>
+ <url-pattern>/*</url-pattern>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>uma_authorization</role-name>
+ </auth-constraint>
+ </security-constraint>
+
+ <login-config>
+ <auth-method>KEYCLOAK</auth-method>
+ <realm-name>hello-world-authz</realm-name>
+ </login-config>
+
+ <security-role>
+ <role-name>uma_authorization</role-name>
+ </security-role>
+</web-app>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml
new file mode 100755
index 0000000..6985f3e
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/pom.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-test-apps-photoz-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>photoz-authz-policy</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Keycloak Authz Tests: Photoz Authz Rule-based Policy</name>
+
+ <description>
+ Photoz Authz Rule-based Policies using JBoss Drools
+ </description>
+
+</project>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.admin/Main.drl b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.admin/Main.drl
new file mode 100644
index 0000000..deb1c84
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.admin/Main.drl
@@ -0,0 +1,14 @@
+package com.photoz.authz.policy.admin
+
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+
+rule "Authorize Admin Resources"
+ dialect "mvel"
+ when
+ $evaluation : Evaluation(
+ $identity : context.identity,
+ $identity.hasRole("admin")
+ )
+ then
+ $evaluation.grant();
+end
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.resource.owner/Main.drl b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.resource.owner/Main.drl
new file mode 100644
index 0000000..9378b94
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.resource.owner/Main.drl
@@ -0,0 +1,15 @@
+package com.photoz.authz.policy.admin
+
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+
+rule "Authorize Resource Owner"
+ dialect "mvel"
+ when
+ $evaluation : Evaluation(
+ $identity: context.identity,
+ $permission: permission,
+ $permission.resource != null && $permission.resource.owner.equals($identity.id)
+ )
+ then
+ $evaluation.grant();
+end
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.user/Main.drl b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.user/Main.drl
new file mode 100644
index 0000000..9b1677e
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com.photoz.authz.policy.user/Main.drl
@@ -0,0 +1,14 @@
+package com.photoz.authz.policy.admin
+
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+
+rule "Authorize View User Album"
+ dialect "mvel"
+ when
+ $evaluation : Evaluation(
+ $identity : context.identity,
+ $identity.hasRole("user")
+ )
+ then
+ $evaluation.grant();
+end
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com/photoz/authz/policy/contextual/Main.drl b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com/photoz/authz/policy/contextual/Main.drl
new file mode 100644
index 0000000..8a6a772
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/com/photoz/authz/policy/contextual/Main.drl
@@ -0,0 +1,15 @@
+package com.photoz.authz.policy.admin
+
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+
+rule "Authorize Using Context Information"
+ dialect "mvel"
+ when
+ $evaluation : Evaluation(
+ $attributes: context.attributes,
+ $attributes.containsValue("kc.identity.authc.method", "otp"),
+ $attributes.containsValue("someAttribute", "you_can_access")
+ )
+ then
+ $evaluation.grant();
+end
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/META-INF/kmodule.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/META-INF/kmodule.xml
new file mode 100644
index 0000000..84bacd5
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-authz-policy/src/main/resources/META-INF/kmodule.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<kmodule xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://jboss.org/kie/6.0.0/kmodule">
+
+ <kbase name="PhotozAuthzAdminPolicy" packages="com.photoz.authz.policy.admin">
+ <ksession name="MainAdminSession" default="true"/>
+ </kbase>
+
+ <kbase name="PhotozAuthzUserPolicy" packages="com.photoz.authz.policy.user">
+ <ksession name="MainUserSession" default="true"/>
+ </kbase>
+
+ <kbase name="PhotozAuthzOwnerPolicy" packages="com.photoz.authz.policy.resource.owner">
+ <ksession name="MainOwnerSession" default="true"/>
+ </kbase>
+
+ <kbase name="PhotozAuthzContextualPolicy" packages="com.photoz.authz.policy.contextual">
+ <ksession name="MainContextualSession" default="true"/>
+ </kbase>
+
+</kmodule>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml
new file mode 100755
index 0000000..d57557a
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/pom.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-test-apps-photoz-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>photoz-html5-client</artifactId>
+ <packaging>war</packaging>
+
+ <name>Keycloak Authz Tests: Photoz HTML5 Client</name>
+ <description>Photoz Test HTML5 Client</description>
+
+ <build>
+ <finalName>${project.artifactId}</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.jboss.as.plugins</groupId>
+ <artifactId>jboss-as-maven-plugin</artifactId>
+ <configuration>
+ <skip>false</skip>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.wildfly.plugins</groupId>
+ <artifactId>wildfly-maven-plugin</artifactId>
+ <configuration>
+ <skip>false</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html
new file mode 100755
index 0000000..5ac24ea
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/index.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<html lang="en">
+
+<head>
+ <meta charset="utf-8">
+ <title>Photoz HTML5 Client</title>
+
+ <!-- Load AngularJS -->
+ <script src="lib/angular/angular.min.js"></script>
+ <script src="lib/angular/angular-resource.min.js"></script>
+ <script src="lib/angular/angular-route.min.js"></script>
+ <script src="lib/jwt-decode.min.js"></script>
+
+ <script src="http://localhost:8180/auth/js/keycloak.js"></script>
+ <script src="http://localhost:8180/auth/js/keycloak-authz.js"></script>
+ <script src="js/identity.js" type="text/javascript"></script>
+ <script src="js/app.js" type="text/javascript"></script>
+</head>
+
+<body data-ng-controller="TokenCtrl">
+
+<a href data-ng-click="showRpt()">Show Requesting Party Token </a> | <a href data-ng-click="showAccessToken()">Show Access Token </a> | <a href data-ng-click="requestEntitlements()">Request Entitlements</a>
+
+<div id="content-area" class="col-md-9" role="main">
+ <div id="content" ng-view/>
+</div>
+
+<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="output"></pre>
+
+</body>
+</html>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
new file mode 100755
index 0000000..2990675
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/app.js
@@ -0,0 +1,168 @@
+var module = angular.module('photoz', ['ngRoute', 'ngResource']);
+
+var resourceServerId = 'photoz-restful-api';
+var apiUrl = window.location.origin + '/' + resourceServerId;
+
+angular.element(document).ready(function ($http) {
+ var keycloak = new Keycloak('keycloak.json');
+ keycloak.init({onLoad: 'login-required'}).success(function () {
+ console.log('User is now authenticated.');
+
+ module.factory('Identity', function () {
+ return new Identity(keycloak);
+ });
+
+ angular.bootstrap(document, ["photoz"]);
+ }).error(function () {
+ window.location.reload();
+ });
+});
+
+module.config(function ($httpProvider, $routeProvider) {
+ $httpProvider.interceptors.push('authInterceptor');
+ $routeProvider.when('/', {
+ templateUrl: 'partials/home.html',
+ controller: 'GlobalCtrl'
+ }).when('/album/create', {
+ templateUrl: 'partials/album/create.html',
+ controller: 'AlbumCtrl',
+ }).when('/album/:id', {
+ templateUrl: 'partials/album/detail.html',
+ controller: 'AlbumCtrl',
+ }).when('/admin/album', {
+ templateUrl: 'partials/admin/albums.html',
+ controller: 'AdminAlbumCtrl',
+ }).when('/profile', {
+ templateUrl: 'partials/profile.html',
+ controller: 'ProfileCtrl',
+ });
+});
+
+module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Album, Identity) {
+ Album.query(function (albums) {
+ $scope.albums = albums;
+ });
+
+ $scope.Identity = Identity;
+
+ $scope.deleteAlbum = function (album) {
+ new Album(album).$delete({id: album.id}, function () {
+ $route.reload();
+ });
+ }
+});
+
+module.controller('TokenCtrl', function ($scope, Identity) {
+ $scope.showRpt = function () {
+ document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.authorization.rpt), null, ' ');
+ }
+
+ $scope.showAccessToken = function () {
+ document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.authc.token), null, ' ');
+ }
+
+ $scope.requestEntitlements = function () {
+ Identity.authorization.entitlement('photoz-restful-api').then(function (rpt) {});
+ }
+});
+
+module.controller('AlbumCtrl', function ($scope, $http, $routeParams, $location, Album) {
+ $scope.album = {};
+ if ($routeParams.id) {
+ $scope.album = Album.get({id: $routeParams.id});
+ }
+ $scope.create = function () {
+ var newAlbum = new Album($scope.album);
+ newAlbum.$save({}, function (data) {
+ $location.path('/');
+ });
+ };
+});
+
+module.controller('ProfileCtrl', function ($scope, $http, $routeParams, $location, Profile) {
+ $scope.profile = Profile.get();
+});
+
+module.controller('AdminAlbumCtrl', function ($scope, $http, $route, AdminAlbum, Album) {
+ $scope.albums = {};
+ $http.get(apiUrl + '/admin/album').success(function (data) {
+ $scope.albums = data;
+ });
+ $scope.deleteAlbum = function (album) {
+ var newAlbum = new Album(album);
+ newAlbum.$delete({id: album.id}, function () {
+ $route.reload();
+ });
+ }
+});
+
+module.factory('Album', ['$resource', function ($resource) {
+ return $resource(apiUrl + '/album/:id');
+}]);
+
+module.factory('Profile', ['$resource', function ($resource) {
+ return $resource(apiUrl + '/profile');
+}]);
+
+module.factory('AdminAlbum', ['$resource', function ($resource) {
+ return $resource(apiUrl + '/admin/album/:id');
+}]);
+
+module.factory('authInterceptor', function ($q, $injector, $timeout, Identity) {
+ return {
+ request: function (request) {
+ document.getElementById("output").innerHTML = '';
+ if (Identity.authorization && Identity.authorization.rpt && request.url.indexOf('/authorize') == -1) {
+ retries = 0;
+ request.headers.Authorization = 'Bearer ' + Identity.authorization.rpt;
+ } else {
+ request.headers.Authorization = 'Bearer ' + Identity.authc.token;
+ }
+ return request;
+ },
+ responseError: function (rejection) {
+ var status = rejection.status;
+
+ if (status == 403 || status == 401) {
+ var retry = (!rejection.config.retry || rejection.config.retry < 1);
+
+ if (!retry) {
+ document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
+ return $q.reject(rejection);
+ }
+
+ if (rejection.config.url.indexOf('/authorize') == -1 && retry) {
+ var deferred = $q.defer();
+
+ // here is the authorization logic, which tries to obtain an authorization token from the server in case the resource server
+ // returns a 403 or 401.
+ Identity.authorization.authorize(rejection.headers('WWW-Authenticate')).then(function (rpt) {
+ deferred.resolve(rejection);
+ }, function () {
+ document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
+ }, function () {
+ document.getElementById("output").innerHTML = 'Unexpected error from server.';
+ });
+
+ var promise = deferred.promise;
+
+ return promise.then(function (res) {
+ if (!res.config.retry) {
+ res.config.retry = 1;
+ } else {
+ res.config.retry++;
+ }
+
+ var $http = $injector.get("$http");
+
+ return $http(res.config).then(function (response) {
+ return response;
+ });
+ });
+ }
+ }
+
+ return $q.reject(rejection);
+ }
+ };
+});
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js
new file mode 100644
index 0000000..9a018e4
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/js/identity.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+/**
+ * Creates an Identity object holding the information obtained from the access token issued by Keycloak, after a successful authentication,
+ * and a few utility methods to manage it.
+ */
+(function (window, undefined) {
+ var Identity = function (keycloak) {
+ this.loggedIn = true;
+
+ this.claims = {};
+ this.claims.name = keycloak.idTokenParsed.name;
+
+ this.authc = {};
+ this.authc.token = keycloak.token;
+
+ this.logout = function () {
+ keycloak.logout();
+ };
+
+ this.hasRole = function (name) {
+ if (keycloak && keycloak.hasRealmRole(name)) {
+ return true;
+ }
+ return false;
+ };
+
+ this.isAdmin = function () {
+ return this.hasRole("admin");
+ };
+
+ this.authorization = new KeycloakAuthorization(keycloak);
+ }
+
+ if ( typeof module === "object" && module && typeof module.exports === "object" ) {
+ module.exports = Identity;
+ } else {
+ window.Identity = Identity;
+
+ if ( typeof define === "function" && define.amd ) {
+ define( "identity", [], function () { return Identity; } );
+ }
+ }
+})( window );
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/keycloak.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/keycloak.json
new file mode 100644
index 0000000..c1dee24
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/keycloak.json
@@ -0,0 +1,8 @@
+{
+ "realm": "photoz",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8080/auth",
+ "ssl-required" : "external",
+ "resource" : "photoz-html5-client",
+ "public-client" : true
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular.min.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular.min.js
new file mode 100644
index 0000000..569a9a2
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular.min.js
@@ -0,0 +1,214 @@
+/*
+ AngularJS v1.3.0-beta.5
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(O,U,s){'use strict';function v(b){return function(){var a=arguments[0],c,a="["+(b?b+":":"")+a+"] http://errors.angularjs.org/1.3.0-beta.5/"+(b?b+"/":"")+a;for(c=1;c<arguments.length;c++)a=a+(1==c?"?":"&")+"p"+(c-1)+"="+encodeURIComponent("function"==typeof arguments[c]?arguments[c].toString().replace(/ \{[\s\S]*$/,""):"undefined"==typeof arguments[c]?"undefined":"string"!=typeof arguments[c]?JSON.stringify(arguments[c]):arguments[c]);return Error(a)}}function db(b){if(null==b||Da(b))return!1;
+var a=b.length;return 1===b.nodeType&&a?!0:t(b)||M(b)||0===a||"number"===typeof a&&0<a&&a-1 in b}function q(b,a,c){var d;if(b)if(P(b))for(d in b)"prototype"==d||("length"==d||"name"==d||b.hasOwnProperty&&!b.hasOwnProperty(d))||a.call(c,b[d],d);else if(b.forEach&&b.forEach!==q)b.forEach(a,c);else if(db(b))for(d=0;d<b.length;d++)a.call(c,b[d],d);else for(d in b)b.hasOwnProperty(d)&&a.call(c,b[d],d);return b}function Tb(b){var a=[],c;for(c in b)b.hasOwnProperty(c)&&a.push(c);return a.sort()}function ad(b,
+a,c){for(var d=Tb(b),e=0;e<d.length;e++)a.call(c,b[d[e]],d[e]);return d}function Ub(b){return function(a,c){b(c,a)}}function eb(){for(var b=ka.length,a;b;){b--;a=ka[b].charCodeAt(0);if(57==a)return ka[b]="A",ka.join("");if(90==a)ka[b]="0";else return ka[b]=String.fromCharCode(a+1),ka.join("")}ka.unshift("0");return ka.join("")}function Vb(b,a){a?b.$$hashKey=a:delete b.$$hashKey}function A(b){var a=b.$$hashKey;q(arguments,function(a){a!==b&&q(a,function(a,c){b[c]=a})});Vb(b,a);return b}function Y(b){return parseInt(b,
+10)}function Wb(b,a){return A(new (A(function(){},{prototype:b})),a)}function C(){}function Ea(b){return b}function aa(b){return function(){return b}}function D(b){return"undefined"===typeof b}function B(b){return"undefined"!==typeof b}function X(b){return null!=b&&"object"===typeof b}function t(b){return"string"===typeof b}function Ab(b){return"number"===typeof b}function ra(b){return"[object Date]"===ya.call(b)}function M(b){return"[object Array]"===ya.call(b)}function P(b){return"function"===typeof b}
+function fb(b){return"[object RegExp]"===ya.call(b)}function Da(b){return b&&b.document&&b.location&&b.alert&&b.setInterval}function bd(b){return!(!b||!(b.nodeName||b.prop&&b.attr&&b.find))}function cd(b,a,c){var d=[];q(b,function(b,g,f){d.push(a.call(c,b,g,f))});return d}function gb(b,a){if(b.indexOf)return b.indexOf(a);for(var c=0;c<b.length;c++)if(a===b[c])return c;return-1}function Fa(b,a){var c=gb(b,a);0<=c&&b.splice(c,1);return a}function ba(b,a){if(Da(b)||b&&b.$evalAsync&&b.$watch)throw Oa("cpws");
+if(a){if(b===a)throw Oa("cpi");if(M(b))for(var c=a.length=0;c<b.length;c++)a.push(ba(b[c]));else{c=a.$$hashKey;q(a,function(b,c){delete a[c]});for(var d in b)a[d]=ba(b[d]);Vb(a,c)}}else(a=b)&&(M(b)?a=ba(b,[]):ra(b)?a=new Date(b.getTime()):fb(b)?a=RegExp(b.source):X(b)&&(a=ba(b,{})));return a}function Xb(b,a){a=a||{};for(var c in b)!b.hasOwnProperty(c)||"$"===c.charAt(0)&&"$"===c.charAt(1)||(a[c]=b[c]);return a}function za(b,a){if(b===a)return!0;if(null===b||null===a)return!1;if(b!==b&&a!==a)return!0;
+var c=typeof b,d;if(c==typeof a&&"object"==c)if(M(b)){if(!M(a))return!1;if((c=b.length)==a.length){for(d=0;d<c;d++)if(!za(b[d],a[d]))return!1;return!0}}else{if(ra(b))return ra(a)&&b.getTime()==a.getTime();if(fb(b)&&fb(a))return b.toString()==a.toString();if(b&&b.$evalAsync&&b.$watch||a&&a.$evalAsync&&a.$watch||Da(b)||Da(a)||M(a))return!1;c={};for(d in b)if("$"!==d.charAt(0)&&!P(b[d])){if(!za(b[d],a[d]))return!1;c[d]=!0}for(d in a)if(!c.hasOwnProperty(d)&&"$"!==d.charAt(0)&&a[d]!==s&&!P(a[d]))return!1;
+return!0}return!1}function Yb(){return U.securityPolicy&&U.securityPolicy.isActive||U.querySelector&&!(!U.querySelector("[ng-csp]")&&!U.querySelector("[data-ng-csp]"))}function hb(b,a){var c=2<arguments.length?sa.call(arguments,2):[];return!P(a)||a instanceof RegExp?a:c.length?function(){return arguments.length?a.apply(b,c.concat(sa.call(arguments,0))):a.apply(b,c)}:function(){return arguments.length?a.apply(b,arguments):a.call(b)}}function dd(b,a){var c=a;"string"===typeof b&&"$"===b.charAt(0)?c=
+s:Da(a)?c="$WINDOW":a&&U===a?c="$DOCUMENT":a&&(a.$evalAsync&&a.$watch)&&(c="$SCOPE");return c}function ta(b,a){return"undefined"===typeof b?s:JSON.stringify(b,dd,a?" ":null)}function Zb(b){return t(b)?JSON.parse(b):b}function Pa(b){"function"===typeof b?b=!0:b&&0!==b.length?(b=I(""+b),b=!("f"==b||"0"==b||"false"==b||"no"==b||"n"==b||"[]"==b)):b=!1;return b}function ha(b){b=y(b).clone();try{b.empty()}catch(a){}var c=y("<div>").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<g;f++)d=a[f],b&&(b=(e=b)[d]);return!c&&P(b)?hb(e,b):b}function Db(b){var a=b[0];b=b[b.length-1];if(a===b)return y(a);var c=[a];do{a=a.nextSibling;if(!a)break;c.push(a)}while(a!==b);return y(c)}function gd(b){var a=v("$injector"),c=v("ng");b=b.angular||(b.angular={});b.$$minErr=b.$$minErr||v;return b.module||
+(b.module=function(){var b={};return function(e,g,f){if("hasOwnProperty"===e)throw c("badname","module");g&&b.hasOwnProperty(e)&&(b[e]=null);return b[e]||(b[e]=function(){function b(a,d,e){return function(){c[e||"push"]([a,d,arguments]);return n}}if(!g)throw a("nomod",e);var c=[],d=[],l=b("$injector","invoke"),n={_invokeQueue:c,_runBlocks:d,requires:g,name:e,provider:b("$provide","provider"),factory:b("$provide","factory"),service:b("$provide","service"),value:b("$provide","value"),constant:b("$provide",
+"constant","unshift"),animation:b("$animateProvider","register"),filter:b("$filterProvider","register"),controller:b("$controllerProvider","register"),directive:b("$compileProvider","directive"),config:l,run:function(a){d.push(a);return this}};f&&l(f);return n}())}}())}function hd(b){A(b,{bootstrap:cc,copy:ba,extend:A,equals:za,element:y,forEach:q,injector:dc,noop:C,bind:hb,toJson:ta,fromJson:Zb,identity:Ea,isUndefined:D,isDefined:B,isString:t,isFunction:P,isObject:X,isNumber:Ab,isElement:bd,isArray:M,
+version:id,isDate:ra,lowercase:I,uppercase:Ga,callbacks:{counter:0},$$minErr:v,$$csp:Yb});Sa=gd(O);try{Sa("ngLocale")}catch(a){Sa("ngLocale",[]).provider("$locale",jd)}Sa("ng",["ngLocale"],["$provide",function(a){a.provider({$$sanitizeUri:kd});a.provider("$compile",fc).directive({a:ld,input:gc,textarea:gc,form:md,script:nd,select:od,style:pd,option:qd,ngBind:rd,ngBindHtml:sd,ngBindTemplate:td,ngClass:ud,ngClassEven:vd,ngClassOdd:wd,ngCloak:xd,ngController:yd,ngForm:zd,ngHide:Ad,ngIf:Bd,ngInclude:Cd,
+ngInit:Dd,ngNonBindable:Ed,ngPluralize:Fd,ngRepeat:Gd,ngShow:Hd,ngStyle:Id,ngSwitch:Jd,ngSwitchWhen:Kd,ngSwitchDefault:Ld,ngOptions:Md,ngTransclude:Nd,ngModel:Od,ngList:Pd,ngChange:Qd,required:hc,ngRequired:hc,ngValue:Rd}).directive({ngInclude:Sd}).directive(Eb).directive(ic);a.provider({$anchorScroll:Td,$animate:Ud,$browser:Vd,$cacheFactory:Wd,$controller:Xd,$document:Yd,$exceptionHandler:Zd,$filter:jc,$interpolate:$d,$interval:ae,$http:be,$httpBackend:ce,$location:de,$log:ee,$parse:fe,$rootScope:ge,
+$q:he,$sce:ie,$sceDelegate:je,$sniffer:ke,$templateCache:le,$timeout:me,$window:ne,$$rAF:oe,$$asyncCallback:pe})}])}function Ta(b){return b.replace(qe,function(a,b,d,e){return e?d.toUpperCase():d}).replace(re,"Moz$1")}function Fb(b,a,c,d){function e(b){var e=c&&b?[this.filter(b)]:[this],m=a,k,l,n,p,r,u;if(!d||null!=b)for(;e.length;)for(k=e.shift(),l=0,n=k.length;l<n;l++)for(p=y(k[l]),m?p.triggerHandler("$destroy"):m=!m,r=0,p=(u=p.children()).length;r<p;r++)e.push(Ha(u[r]));return g.apply(this,arguments)}
+var g=Ha.fn[b],g=g.$original||g;e.$original=g;Ha.fn[b]=e}function se(b,a){var c,d,e=a.createDocumentFragment(),g=[];if(Gb.test(b)){c=c||e.appendChild(a.createElement("div"));d=(te.exec(b)||["",""])[1].toLowerCase();d=ea[d]||ea._default;c.innerHTML=d[1]+b.replace(ue,"<$1></$2>")+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<b.length;a++)Ia(b[a])}function mc(b,a,c,d){if(B(d))throw Hb("offargs");var e=la(b,"events");la(b,"handle")&&(D(a)?q(e,function(a,c){Ua(b,c,a);delete e[c]}):q(a.split(" "),function(a){D(c)?(Ua(b,
+a,e[a]),delete e[a]):Fa(e[a]||[],c)}))}function lc(b,a){var c=b[jb],d=Va[c];d&&(a?delete Va[c].data[a]:(d.handle&&(d.events.$destroy&&d.handle({},"$destroy"),mc(b)),delete Va[c],b[jb]=s))}function la(b,a,c){var d=b[jb],d=Va[d||-1];if(B(c))d||(b[jb]=d=++we,d=Va[d]={}),d[a]=c;else return d&&d[a]}function nc(b,a,c){var d=la(b,"data"),e=B(c),g=!e&&B(a),f=g&&!X(a);d||f||la(b,"data",d={});if(e)d[a]=c;else if(g){if(f)return d&&d[a];A(d,a)}else return d}function Jb(b,a){return b.getAttribute?-1<(" "+(b.getAttribute("class")||
+"")+" ").replace(/[\n\t]/g," ").indexOf(" "+a+" "):!1}function kb(b,a){a&&b.setAttribute&&q(a.split(" "),function(a){b.setAttribute("class",ca((" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ").replace(" "+ca(a)+" "," ")))})}function lb(b,a){if(a&&b.setAttribute){var c=(" "+(b.getAttribute("class")||"")+" ").replace(/[\n\t]/g," ");q(a.split(" "),function(a){a=ca(a);-1===c.indexOf(" "+a+" ")&&(c+=a+" ")});b.setAttribute("class",ca(c))}}function kc(b,a){if(a){a=a.nodeName||!B(a.length)||
+Da(a)?[a]:a;for(var c=0;c<a.length;c++)b.push(a[c])}}function oc(b,a){return mb(b,"$"+(a||"ngController")+"Controller")}function mb(b,a,c){b=y(b);9==b[0].nodeType&&(b=b.find("html"));for(a=M(a)?a:[a];b.length;){for(var d=b[0],e=0,g=a.length;e<g;e++)if((c=b.data(a[e]))!==s)return c;b=y(d.parentNode||11===d.nodeType&&d.host)}}function pc(b){for(var a=0,c=b.childNodes;a<c.length;a++)Ia(c[a]);for(;b.firstChild;)b.removeChild(b.firstChild)}function qc(b,a){var c=nb[a.toLowerCase()];return c&&rc[b.nodeName]&&
+c}function xe(b,a){var c=function(c,e){c.preventDefault||(c.preventDefault=function(){c.returnValue=!1});c.stopPropagation||(c.stopPropagation=function(){c.cancelBubble=!0});c.target||(c.target=c.srcElement||U);if(D(c.defaultPrevented)){var g=c.preventDefault;c.preventDefault=function(){c.defaultPrevented=!0;g.call(c)};c.defaultPrevented=!1}c.isDefaultPrevented=function(){return c.defaultPrevented||!1===c.returnValue};var f=Xb(a[e||c.type]||[]);q(f,function(a){a.call(b,c)});8>=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<h;g++){var f=d[g],m=n.get(f[0]);m[f[1]].apply(m,f[2])}else P(a)?b.push(n.invoke(a)):M(a)?b.push(n.invoke(a)):Ra(a,"module")}catch(l){throw M(a)&&(a=a[a.length-1]),l.message&&(l.stack&&-1==l.stack.indexOf(l.message))&&(l=l.message+"\n"+l.stack),Xa("modulerr",a,l.stack||l.message||l);}}});return b}function g(a,b){function c(d){if(a.hasOwnProperty(d)){if(a[d]===f)throw Xa("cdep",m.join(" <- "));return a[d]}try{return m.unshift(d),a[d]=f,a[d]=b(d)}catch(e){throw a[d]===f&&delete a[d],
+e;}finally{m.shift()}}function d(a,b,e){var g=[],h=sc(a),f,m,k;m=0;for(f=h.length;m<f;m++){k=h[m];if("string"!==typeof k)throw Xa("itkn",k);g.push(e&&e.hasOwnProperty(k)?e[k]:c(k))}a.$inject||(a=a[f]);return a.apply(b,g)}return{invoke:d,instantiate:function(a,b){var c=function(){},e;c.prototype=(M(a)?a[a.length-1]:a).prototype;c=new c;e=d(a,c,b);return X(e)||P(e)?e:c},get:c,annotate:sc,has:function(b){return l.hasOwnProperty(b+h)||a.hasOwnProperty(b)}}}var f={},h="Provider",m=[],k=new Wa,l={$provide:{provider:a(c),
+factory:a(d),service:a(function(a,b){return d(a,["$injector",function(a){return a.instantiate(b)}])}),value:a(function(a,b){return d(a,aa(b))}),constant:a(function(a,b){Ba(a,"constant");l[a]=b;p[a]=b}),decorator:function(a,b){var c=n.get(a+h),d=c.$get;c.$get=function(){var a=r.invoke(d,c);return r.invoke(b,null,{$delegate:a})}}}},n=l.$injector=g(l,function(){throw Xa("unpr",m.join(" <- "));}),p={},r=p.$injector=g(p,function(a){a=n.get(a+h);return r.invoke(a.$get,a)});q(e(b),function(a){r.invoke(a||
+C)});return r}function Td(){var b=!0;this.disableAutoScrolling=function(){b=!1};this.$get=["$window","$location","$rootScope",function(a,c,d){function e(a){var b=null;q(a,function(a){b||"a"!==I(a.nodeName)||(b=a)});return b}function g(){var b=c.hash(),d;b?(d=f.getElementById(b))?d.scrollIntoView():(d=e(f.getElementsByName(b)))?d.scrollIntoView():"top"===b&&a.scrollTo(0,0):a.scrollTo(0,0)}var f=a.document;b&&d.$watch(function(){return c.hash()},function(){d.$evalAsync(g)});return g}]}function pe(){this.$get=
+["$$rAF","$timeout",function(b,a){return b.supported?function(a){return b(a)}:function(b){return a(b,0,!1)}}]}function Ce(b,a,c,d){function e(a){try{a.apply(null,sa.call(arguments,1))}finally{if(u--,0===u)for(;z.length;)try{z.pop()()}catch(b){c.error(b)}}}function g(a,b){(function S(){q(K,function(a){a()});w=b(S,a)})()}function f(){x=null;H!=h.url()&&(H=h.url(),q(ma,function(a){a(h.url())}))}var h=this,m=a[0],k=b.location,l=b.history,n=b.setTimeout,p=b.clearTimeout,r={};h.isMock=!1;var u=0,z=[];h.$$completeOutstandingRequest=
+e;h.$$incOutstandingRequestCount=function(){u++};h.notifyWhenNoOutstandingRequests=function(a){q(K,function(a){a()});0===u?a():z.push(a)};var K=[],w;h.addPollFn=function(a){D(w)&&g(100,n);K.push(a);return a};var H=k.href,G=a.find("base"),x=null;h.url=function(a,c){k!==b.location&&(k=b.location);l!==b.history&&(l=b.history);if(a){if(H!=a)return H=a,d.history?c?l.replaceState(null,"",a):(l.pushState(null,"",a),G.attr("href",G.attr("href"))):(x=a,c?k.replace(a):k.href=a),h}else return x||k.href.replace(/%27/g,
+"'")};var ma=[],L=!1;h.onUrlChange=function(a){if(!L){if(d.history)y(b).on("popstate",f);if(d.hashchange)y(b).on("hashchange",f);else h.addPollFn(f);L=!0}ma.push(a);return a};h.baseHref=function(){var a=G.attr("href");return a?a.replace(/^(https?\:)?\/\/[^\/]*/,""):""};var Q={},da="",E=h.baseHref();h.cookies=function(a,b){var d,e,g,h;if(a)b===s?m.cookie=escape(a)+"=;path="+E+";expires=Thu, 01 Jan 1970 00:00:00 GMT":t(b)&&(d=(m.cookie=escape(a)+"="+escape(b)+";path="+E).length+1,4096<d&&c.warn("Cookie '"+
+a+"' possibly not set or overflowed because it was too large ("+d+" > 4096 bytes)!"));else{if(m.cookie!==da)for(da=m.cookie,d=da.split("; "),Q={},g=0;g<d.length;g++)e=d[g],h=e.indexOf("="),0<h&&(a=unescape(e.substring(0,h)),Q[a]===s&&(Q[a]=unescape(e.substring(h+1))));return Q}};h.defer=function(a,b){var c;u++;c=n(function(){delete r[c];e(a)},b||0);r[c]=!0;return c};h.defer.cancel=function(a){return r[a]?(delete r[a],p(a),e(C),!0):!1}}function Vd(){this.$get=["$window","$log","$sniffer","$document",
+function(b,a,c,d){return new Ce(b,d,a,c)}]}function Wd(){this.$get=function(){function b(b,d){function e(a){a!=n&&(p?p==a&&(p=a.n):p=a,g(a.n,a.p),g(a,n),n=a,n.n=null)}function g(a,b){a!=b&&(a&&(a.p=b),b&&(b.n=a))}if(b in a)throw v("$cacheFactory")("iid",b);var f=0,h=A({},d,{id:b}),m={},k=d&&d.capacity||Number.MAX_VALUE,l={},n=null,p=null;return a[b]={put:function(a,b){if(k<Number.MAX_VALUE){var c=l[a]||(l[a]={key:a});e(c)}if(!D(b))return a in m||f++,m[a]=b,f>k&&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<Number.MAX_VALUE){var b=l[a];if(!b)return;b==n&&(n=b.p);b==p&&(p=b.n);g(b.n,b.p);delete l[a]}delete m[a];f--},removeAll:function(){m={};f=0;l={};n=p=null},destroy:function(){l=h=m=null;delete a[b]},info:function(){return A({},h,{size:f})}}}var a={};b.info=function(){var b={};q(a,function(a,e){b[e]=a.info()});return b};b.get=function(b){return a[b]};return b}}function le(){this.$get=["$cacheFactory",function(b){return b("templates")}]}
+function fc(b,a){var c={},d="Directive",e=/^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/,g=/(([\d\w\-_]+)(?:\:([^;]+))?;?)/,f=/^(on[a-z]+|formaction)$/;this.directive=function m(a,e){Ba(a,"directive");t(a)?(Cb(e,"directiveFactory"),c.hasOwnProperty(a)||(c[a]=[],b.factory(a+d,["$injector","$exceptionHandler",function(b,d){var e=[];q(c[a],function(c,g){try{var f=b.invoke(c);P(f)?f={compile:aa(f)}:!f.compile&&f.link&&(f.compile=aa(f.link));f.priority=f.priority||0;f.index=g;f.name=f.name||a;f.require=f.require||
+f.controller&&f.name;f.restrict=f.restrict||"A";e.push(f)}catch(m){d(m)}});return e}])),c[a].push(e)):q(a,Ub(m));return this};this.aHrefSanitizationWhitelist=function(b){return B(b)?(a.aHrefSanitizationWhitelist(b),this):a.aHrefSanitizationWhitelist()};this.imgSrcSanitizationWhitelist=function(b){return B(b)?(a.imgSrcSanitizationWhitelist(b),this):a.imgSrcSanitizationWhitelist()};this.$get=["$injector","$interpolate","$exceptionHandler","$http","$templateCache","$parse","$controller","$rootScope",
+"$document","$sce","$animate","$$sanitizeUri",function(a,b,l,n,p,r,u,z,K,w,H,G){function x(a,b,c,d,e){a instanceof y||(a=y(a));q(a,function(b,c){3==b.nodeType&&b.nodeValue.match(/\S+/)&&(a[c]=y(b).wrap("<span></span>").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;d<f;d++){var m=e[d].nodeType;1!==m&&9!==m||e.eq(d).data("$scope",b)}c&&c(e,b);g&&g(b,e,e);return e}}
+function ma(a,b){try{a.addClass(b)}catch(c){}}function L(a,b,c,d,e,g){function f(a,c,d,e){var g,k,l,n,r,p,u;g=c.length;var J=Array(g);for(r=0;r<g;r++)J[r]=c[r];u=r=0;for(p=m.length;r<p;u++)k=J[u],c=m[r++],g=m[r++],l=y(k),c?(c.scope?(n=a.$new(),l.data("$scope",n)):n=a,(l=c.transclude)||!e&&b?c(g,n,k,d,Q(a,l||b)):c(g,n,k,d,e)):g&&g(a,k.childNodes,s,e)}for(var m=[],k,l,n,r,p=0;p<a.length;p++)k=new Kb,l=da(a[p],[],k,0===p?d:s,e),(g=l.length?ia(l,a[p],k,b,c,null,[],[],g):null)&&g.scope&&ma(y(a[p]),"ng-scope"),
+k=g&&g.terminal||!(n=a[p].childNodes)||!n.length?null:L(n,g?g.transclude:b),m.push(g,k),r=r||g||k,g=null;return r?f:null}function Q(a,b){return function(c,d,e){var g=!1;c||(c=a.$new(),g=c.$$transcluded=!0);d=b(c,d,e);if(g)d.on("$destroy",hb(c,c.$destroy));return d}}function da(a,b,c,d,f){var m=c.$attr,k;switch(a.nodeType){case 1:S(b,na(La(a).toLowerCase()),"E",d,f);var l,n,r;k=a.attributes;for(var p=0,u=k&&k.length;p<u;p++){var z=!1,K=!1;l=k[p];if(!T||8<=T||l.specified){n=l.name;r=na(n);W.test(r)&&
+(n=ib(r.substr(6),"-"));var H=r.replace(/(Start|End)$/,"");r===H+"Start"&&(z=n,K=n.substr(0,n.length-5)+"end",n=n.substr(0,n.length-6));r=na(n.toLowerCase());m[r]=n;c[r]=l=ca(l.value);qc(a,r)&&(c[r]=!0);N(a,b,l,r);S(b,r,"A",d,f,z,K)}}a=a.className;if(t(a)&&""!==a)for(;k=g.exec(a);)r=na(k[2]),S(b,r,"C",d,f)&&(c[r]=ca(k[3])),a=a.substr(k.index+k[0].length);break;case 3:v(b,a.nodeValue);break;case 8:try{if(k=e.exec(a.nodeValue))r=na(k[1]),S(b,r,"M",d,f)&&(c[r]=ca(k[2]))}catch(x){}}b.sort(D);return b}
+function E(a,b,c){var d=[],e=0;if(b&&a.hasAttribute&&a.hasAttribute(b)){do{if(!a)throw ja("uterdir",b,c);1==a.nodeType&&(a.hasAttribute(b)&&e++,a.hasAttribute(c)&&e--);d.push(a);a=a.nextSibling}while(0<e)}else d.push(a);return y(d)}function R(a,b,c){return function(d,e,g,f,k){e=E(e[0],b,c);return a(d,e,g,f,k)}}function ia(a,c,d,e,g,f,m,n,p){function z(a,b,c,d){if(a){c&&(a=R(a,c,d));a.require=F.require;if(Q===F||F.$$isolateScope)a=uc(a,{isolateScope:!0});m.push(a)}if(b){c&&(b=R(b,c,d));b.require=F.require;
+if(Q===F||F.$$isolateScope)b=uc(b,{isolateScope:!0});n.push(b)}}function K(a,b,c){var d,e="data",g=!1;if(t(a)){for(;"^"==(d=a.charAt(0))||"?"==d;)a=a.substr(1),"^"==d&&(e="inheritedData"),g=g||"?"==d;d=null;c&&"data"===e&&(d=c[a]);d=d||b[e]("$"+a+"Controller");if(!d&&!g)throw ja("ctreq",a,v);}else M(a)&&(d=[],q(a,function(a){d.push(K(a,b,c))}));return d}function H(a,e,g,f,p){function z(a,b){var c;2>arguments.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;f<w;f++)try{G=m[f],G(G.isolateScope?E:e,x,J,G.require&&K(G.require,x,da),ob)}catch(F){l(F,ha(x))}f=e;Q&&(Q.template||null===Q.templateUrl)&&(f=E);a&&a(f,g.childNodes,s,p);for(f=n.length-1;0<=f;f--)try{G=n[f],G(G.isolateScope?E:e,x,J,G.require&&K(G.require,x,da),ob)}catch(B){l(B,ha(x))}}p=p||{};for(var w=-Number.MAX_VALUE,G,L=p.controllerDirectives,Q=p.newIsolateScopeDirective,ia=p.templateDirective,S=p.nonTlbTranscludeDirective,D=!1,A=p.hasElementTranscludeDirective,
+Z=d.$$element=y(c),F,v,V,Ya=e,O,N=0,oa=a.length;N<oa;N++){F=a[N];var T=F.$$start,W=F.$$end;T&&(Z=E(c,T,W));V=s;if(w>F.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;b<c;b++)a[b]=Wb(a[b],{$$isolateScope:!0})}function S(b,e,g,f,k,n,r){if(e===k)return null;
+k=null;if(c.hasOwnProperty(e)){var p;e=a.get(e+d);for(var u=0,z=e.length;u<z;u++)try{p=e[u],(f===s||f>p.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.name<b.name?-1:1:a.index-b.index}function I(a,b,c,d){if(b)throw ja("multidir",b.name,c.name,a,ha(d));}function v(a,c){var d=b(c,!0);d&&a.push({priority:0,compile:aa(function(a,b){var c=b.parent(),e=c.data("$binding")||[];e.push(d);ma(c.data("$binding",e),"ng-binding");a.$watch(d,function(a){b[0].nodeValue=a})})})}function O(a,b){if("srcdoc"==b)return w.HTML;var c=La(a);if("xlinkHref"==b||
+"FORM"==c&&"action"==b||"IMG"!=c&&("src"==b||"ngSrc"==b))return w.RESOURCE_URL}function N(a,c,d,e){var g=b(d,!0);if(g){if("multiple"===e&&"SELECT"===La(a))throw ja("selmulti",ha(a));c.push({priority:100,compile:function(){return{pre:function(c,d,m){d=m.$$observers||(m.$$observers={});if(f.test(e))throw ja("nodomevents");if(g=b(m[e],!0,O(a,e)))m[e]=g(c),(d[e]||(d[e]=[])).$$inter=!0,(m.$$observers&&m.$$observers[e].$$scope||c).$watch(g,function(a,b){"class"===e&&a!=b?m.$updateClass(a,b):m.$set(e,a)})}}}})}}
+function pb(a,b,c){var d=b[0],e=b.length,g=d.parentNode,f,m;if(a)for(f=0,m=a.length;f<m;f++)if(a[f]==d){a[f++]=c;m=f+e-1;for(var k=a.length;f<k;f++,m++)m<k?a[f]=a[m]:delete a[f];a.length-=e-1;break}g&&g.replaceChild(c,d);a=U.createDocumentFragment();a.appendChild(d);c[y.expando]=d[y.expando];d=1;for(e=b.length;d<e;d++)g=b[d],y(g).remove(),a.appendChild(g),delete b[d];b[0]=c;b.length=1}function uc(a,b){return A(function(){return a.apply(null,arguments)},a,b)}var Kb=function(a,b){this.$$element=a;this.$attr=
+b||{}};Kb.prototype={$normalize:na,$addClass:function(a){a&&0<a.length&&H.addClass(this.$$element,a)},$removeClass:function(a){a&&0<a.length&&H.removeClass(this.$$element,a)},$updateClass:function(a,b){var c=vc(a,b),d=vc(b,a);0===c.length?H.removeClass(this.$$element,d):0===d.length?H.addClass(this.$$element,c):H.setClass(this.$$element,c,d)},$set:function(a,b,c,d){var e=qc(this.$$element[0],a);e&&(this.$$element.prop(a,b),d=e);this[a]=b;d?this.$attr[a]=d:(d=this.$attr[a])||(this.$attr[a]=d=ib(a,
+"-"));e=La(this.$$element);if("A"===e&&"href"===a||"IMG"===e&&"src"===a)this[a]=b=G(b,"src"===a);!1!==c&&(null===b||b===s?this.$$element.removeAttr(d):this.$$element.attr(d,b));(c=this.$$observers)&&q(c[a],function(a){try{a(b)}catch(c){l(c)}})},$observe:function(a,b){var c=this,d=c.$$observers||(c.$$observers={}),e=d[a]||(d[a]=[]);e.push(b);z.$evalAsync(function(){e.$$inter||b(c[a])});return function(){Fa(e,b)}}};var Z=b.startSymbol(),oa=b.endSymbol(),Y="{{"==Z||"}}"==oa?Ea:function(a){return a.replace(/\{\{/g,
+Z).replace(/}}/g,oa)},W=/^ngAttr[A-Z]/;return x}]}function na(b){return Ta(b.replace(De,""))}function vc(b,a){var c="",d=b.split(/\s+/),e=a.split(/\s+/),g=0;a:for(;g<d.length;g++){for(var f=d[g],h=0;h<e.length;h++)if(f==e[h])continue a;c+=(0<c.length?" ":"")+f}return c}function Xd(){var b={},a=/^(\S+)(\s+as\s+(\w+))?$/;this.register=function(a,d){Ba(a,"controller");X(a)?A(b,a):b[a]=d};this.$get=["$injector","$window",function(c,d){return function(e,g){var f,h,m;t(e)&&(f=e.match(a),h=f[1],m=f[3],e=
+b.hasOwnProperty(h)?b[h]:ec(g.$scope,h,!0)||ec(d,h,!0),Ra(e,h,!0));f=c.instantiate(e,g);if(m){if(!g||"object"!=typeof g.$scope)throw v("$controller")("noscp",h||e.name,m);g.$scope[m]=f}return f}}]}function Yd(){this.$get=["$window",function(b){return y(b.document)}]}function Zd(){this.$get=["$log",function(b){return function(a,c){b.error.apply(b,arguments)}}]}function wc(b){var a={},c,d,e;if(!b)return a;q(b.split("\n"),function(b){e=b.indexOf(":");c=I(ca(b.substr(0,e)));d=ca(b.substr(e+1));c&&(a[c]=
+a[c]?a[c]+(", "+d):d)});return a}function xc(b){var a=X(b)?b:s;return function(c){a||(a=wc(b));return c?a[I(c)]||null:a}}function yc(b,a,c){if(P(c))return c(b,a);q(c,function(c){b=c(b,a)});return b}function be(){var b=/^\s*(\[|\{[^\{])/,a=/[\}\]]\s*$/,c=/^\)\]\}',?\n/,d={"Content-Type":"application/json;charset=utf-8"},e=this.defaults={transformResponse:[function(d){t(d)&&(d=d.replace(c,""),b.test(d)&&a.test(d)&&(d=Zb(d)));return d}],transformRequest:[function(a){return X(a)&&"[object File]"!==ya.call(a)&&
+"[object Blob]"!==ya.call(a)?ta(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ba(d),put:ba(d),patch:ba(d)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN"},g=this.interceptors=[],f=this.responseInterceptors=[];this.$get=["$httpBackend","$browser","$cacheFactory","$rootScope","$q","$injector",function(a,b,c,d,n,p){function r(a){function c(a){var b=A({},a,{data:yc(a.data,a.headers,d.transformResponse)});return 200<=a.status&&300>a.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<c.length&&(a+=(-1==a.indexOf("?")?"?":"&")+
+c.join("&"));return a}var K=c("$http"),w=[];q(g,function(a){w.unshift(t(a)?p.get(a):p.invoke(a))});q(f,function(a,b){var c=t(a)?p.get(a):p.invoke(a);w.splice(b,0,{response:function(a){return c(n.when(a))},responseError:function(a){return c(n.reject(a))}})});r.pendingRequests=[];(function(a){q(arguments,function(a){r[a]=function(b,c){return r(A(c||{},{method:a,url:b}))}})})("get","delete","head","jsonp");(function(a){q(arguments,function(a){r[a]=function(b,c,d){return r(A(d||{},{method:a,url:b,data:c}))}})})("post",
+"put");r.defaults=e;return r}]}function Ee(b){if(8>=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<p)var L=c(z,p);else p&&p.then&&p.then(z)}}function $d(){var b="{{",a="}}";this.startSymbol=function(a){return a?(b=a,this):b};this.endSymbol=function(b){return b?(a=b,this):a};this.$get=["$parse","$exceptionHandler","$sce",function(c,d,e){function g(g,k,l){for(var n,p,r=0,u=[],z=g.length,K=!1,w=[];r<z;)-1!=(n=g.indexOf(b,r))&&-1!=(p=g.indexOf(a,n+f))?(r!=n&&u.push(g.substring(r,n)),u.push(r=c(K=g.substring(n+f,p))),
+r.exp=K,r=p+h,K=!0):(r!=z&&u.push(g.substring(r)),r=z);(z=u.length)||(u.push(""),z=1);if(l&&1<u.length)throw zc("noconcat",g);if(!k||K)return w.length=z,r=function(a){try{for(var b=0,c=z,f;b<c;b++)"function"==typeof(f=u[b])&&(f=f(a),f=l?e.getTrusted(l,f):e.valueOf(f),null===f||D(f)?f="":"string"!=typeof f&&(f=ta(f))),w[b]=f;return w.join("")}catch(h){a=zc("interr",g,h.toString()),d(a)}},r.exp=g,r.parts=u,r}var f=b.length,h=a.length;g.startSymbol=function(){return b};g.endSymbol=function(){return a};
+return g}]}function ae(){this.$get=["$rootScope","$window","$q",function(b,a,c){function d(d,f,h,m){var k=a.setInterval,l=a.clearInterval,n=c.defer(),p=n.promise,r=0,u=B(m)&&!m;h=B(h)?h:0;p.then(null,null,d);p.$$intervalId=k(function(){n.notify(r++);0<h&&r>=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;1<a.length;f++){g=fa(a.shift(),d);var h=b[g];h||(h={},b[g]=h);b=h;b.then&&e.unwrapPromises&&(va(d),"$$v"in b||function(a){a.then(function(b){a.$$v=b})}(b),b.$$v===s&&(b.$$v={}),b=b.$$v)}g=fa(a.shift(),d);return b[g]=c}function Gc(b,
+a,c,d,e,g,f){fa(b,g);fa(a,g);fa(c,g);fa(d,g);fa(e,g);return f.unwrapPromises?function(f,m){var k=m&&m.hasOwnProperty(b)?m:f,l;if(null==k)return k;(k=k[b])&&k.then&&(va(g),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);if(!a)return k;if(null==k)return s;(k=k[a])&&k.then&&(va(g),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);if(!c)return k;if(null==k)return s;(k=k[c])&&k.then&&(va(g),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);if(!d)return k;if(null==
+k)return s;(k=k[d])&&k.then&&(va(g),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);if(!e)return k;if(null==k)return s;(k=k[e])&&k.then&&(va(g),"$$v"in k||(l=k,l.$$v=s,l.then(function(a){l.$$v=a})),k=k.$$v);return k}:function(g,f){var k=f&&f.hasOwnProperty(b)?f:g;if(null==k)return k;k=k[b];if(!a)return k;if(null==k)return s;k=k[a];if(!c)return k;if(null==k)return s;k=k[c];if(!d)return k;if(null==k)return s;k=k[d];return e?null==k?s:k=k[e]:k}}function He(b,a){fa(b,a);return function(a,
+d){return null==a?s:(d&&d.hasOwnProperty(b)?d:a)[b]}}function Ie(b,a,c){fa(b,c);fa(a,c);return function(c,e){if(null==c)return s;c=(e&&e.hasOwnProperty(b)?e:c)[b];return null==c?s:c[a]}}function Hc(b,a,c){if(Pb.hasOwnProperty(b))return Pb[b];var d=b.split("."),e=d.length,g;if(a.unwrapPromises||1!==e)if(a.unwrapPromises||2!==e)if(a.csp)g=6>e?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(f<e);return h};else{var f="var p;\n";
+q(d,function(b,d){fa(b,c);f+="if(s == null) return undefined;\ns="+(d?"s":'((k&&k.hasOwnProperty("'+b+'"))?k:s)')+'["'+b+'"];\n'+(a.unwrapPromises?'if (s && s.then) {\n pw("'+c.replace(/(["\r\n])/g,"\\$1")+'");\n if (!("$$v" in s)) {\n p=s;\n p.$$v = undefined;\n p.then(function(v) {p.$$v=v;});\n}\n s=s.$$v\n}\n':"")});var f=f+"return s;",h=new Function("s","k","pw",f);h.toString=aa(f);g=a.unwrapPromises?function(a,b){return h(a,b,va)}:h}else g=Ie(d[0],d[1],c);else g=He(d[0],c);"hasOwnProperty"!==
+b&&(Pb[b]=g);return g}function fe(){var b={},a={csp:!1,unwrapPromises:!1,logPromiseWarnings:!0};this.unwrapPromises=function(b){return B(b)?(a.unwrapPromises=!!b,this):a.unwrapPromises};this.logPromiseWarnings=function(b){return B(b)?(a.logPromiseWarnings=b,this):a.logPromiseWarnings};this.$get=["$filter","$sniffer","$log",function(c,d,e){a.csp=d.csp;va=function(b){a.logPromiseWarnings&&!Ic.hasOwnProperty(b)&&(Ic[b]=!0,e.warn("[$parse] Promise found in the expression `"+b+"`. Automatic unwrapping of promises in Angular expressions is deprecated."))};
+return function(d){var e;switch(typeof d){case "string":if(b.hasOwnProperty(d))return b[d];e=new Qb(a);e=(new ab(e,c,a)).parse(d,!1);"hasOwnProperty"!==d&&(b[d]=e);return e;case "function":return d;default:return C}}}]}function he(){this.$get=["$rootScope","$exceptionHandler",function(b,a){return Je(function(a){b.$evalAsync(a)},a)}]}function Je(b,a){function c(a){return a}function d(a){return f(a)}var e=function(){var f=[],k,l;return l={resolve:function(a){if(f){var c=f;f=s;k=g(a);c.length&&b(function(){for(var a,
+b=0,d=c.length;b<d;b++)a=c[b],k.then(a[0],a[1],a[2])})}},reject:function(a){l.resolve(h(a))},notify:function(a){if(f){var c=f;f.length&&b(function(){for(var b,d=0,e=c.length;d<e;d++)b=c[d],b[2](a)})}},promise:{then:function(b,g,h){var l=e(),z=function(d){try{l.resolve((P(b)?b:c)(d))}catch(e){l.reject(e),a(e)}},K=function(b){try{l.resolve((P(g)?g:d)(b))}catch(c){l.reject(c),a(c)}},w=function(b){try{l.notify((P(h)?h:c)(b))}catch(d){a(d)}};f?f.push([z,K,w]):k.then(z,K,w);return l.promise},"catch":function(a){return this.then(null,
+a)},"finally":function(a){function b(a,c){var d=e();c?d.resolve(a):d.reject(a);return d.promise}function d(e,g){var f=null;try{f=(a||c)()}catch(h){return b(h,!1)}return f&&P(f.then)?f.then(function(){return b(e,g)},function(a){return b(a,!1)}):b(e,g)}return this.then(function(a){return d(a,!0)},function(a){return d(a,!1)})}}}},g=function(a){return a&&P(a.then)?a:{then:function(c){var d=e();b(function(){d.resolve(c(a))});return d.promise}}},f=function(a){var b=e();b.reject(a);return b.promise},h=function(c){return{then:function(g,
+f){var h=e();b(function(){try{h.resolve((P(f)?f:d)(c))}catch(b){h.reject(b),a(b)}});return h.promise}}};return{defer:e,reject:f,when:function(h,k,l,n){var p=e(),r,u=function(b){try{return(P(k)?k:c)(b)}catch(d){return a(d),f(d)}},z=function(b){try{return(P(l)?l:d)(b)}catch(c){return a(c),f(c)}},K=function(b){try{return(P(n)?n:c)(b)}catch(d){a(d)}};b(function(){g(h).then(function(a){r||(r=!0,p.resolve(g(a).then(u,z,K)))},function(a){r||(r=!0,p.resolve(z(a)))},function(a){r||p.notify(K(a))})});return p.promise},
+all:function(a){var b=e(),c=0,d=M(a)?[]:{};q(a,function(a,e){c++;g(a).then(function(a){d.hasOwnProperty(e)||(d[e]=a,--c||b.resolve(d))},function(a){d.hasOwnProperty(e)||b.reject(a)})});0===c&&b.resolve(d);return b.promise}}}function oe(){this.$get=["$window","$timeout",function(b,a){var c=b.requestAnimationFrame||b.webkitRequestAnimationFrame||b.mozRequestAnimationFrame,d=b.cancelAnimationFrame||b.webkitCancelAnimationFrame||b.mozCancelAnimationFrame||b.webkitCancelRequestAnimationFrame,e=!!c,g=e?
+function(a){var b=c(a);return function(){d(b)}}:function(b){var c=a(b,16.66,!1);return function(){a.cancel(c)}};g.supported=e;return g}]}function ge(){var b=10,a=v("$rootScope"),c=null;this.digestTtl=function(a){arguments.length&&(b=a);return b};this.$get=["$injector","$exceptionHandler","$parse","$browser",function(d,e,g,f){function h(){this.$id=eb();this.$$phase=this.$parent=this.$$watchers=this.$$nextSibling=this.$$prevSibling=this.$$childHead=this.$$childTail=null;this["this"]=this.$root=this;
+this.$$destroyed=!1;this.$$asyncQueue=[];this.$$postDigestQueue=[];this.$$listeners={};this.$$listenerCount={};this.$$isolateBindings={}}function m(b){if(p.$$phase)throw a("inprog",p.$$phase);p.$$phase=b}function k(a,b){var c=g(a);Ra(c,b);return c}function l(a,b,c){do a.$$listenerCount[c]-=b,0===a.$$listenerCount[c]&&delete a.$$listenerCount[c];while(a=a.$parent)}function n(){}h.prototype={constructor:h,$new:function(a){a?(a=new h,a.$root=this.$root,a.$$asyncQueue=this.$$asyncQueue,a.$$postDigestQueue=
+this.$$postDigestQueue):(a=function(){},a.prototype=this,a=new a,a.$id=eb());a["this"]=a;a.$$listeners={};a.$$listenerCount={};a.$parent=this;a.$$watchers=a.$$nextSibling=a.$$childHead=a.$$childTail=null;a.$$prevSibling=this.$$childTail;this.$$childHead?this.$$childTail=this.$$childTail.$$nextSibling=a:this.$$childHead=this.$$childTail=a;return a},$watch:function(a,b,d){var e=k(a,"watch"),g=this.$$watchers,f={fn:b,last:n,get:e,exp:a,eq:!!d};c=null;if(!P(b)){var h=k(b||C,"listener");f.fn=function(a,
+b,c){h(c)}}if("string"==typeof a&&e.constant){var m=f.fn;f.fn=function(a,b,c){m.call(this,a,b,c);Fa(g,f)}}g||(g=this.$$watchers=[]);g.unshift(f);return function(){Fa(g,f);c=null}},$watchCollection:function(a,b){var c=this,d,e,f,h=1<b.length,k=0,m=g(a),l=[],n={},p=!0,q=0;return this.$watch(function(){d=m(c);var a,b;if(X(d))if(db(d))for(e!==l&&(e=l,q=e.length=0,k++),a=d.length,q!==a&&(k++,e.length=q=a),b=0;b<a;b++)e[b]!==e[b]&&d[b]!==d[b]||e[b]===d[b]||(k++,e[b]=d[b]);else{e!==n&&(e=n={},q=0,k++);a=
+0;for(b in d)d.hasOwnProperty(b)&&(a++,e.hasOwnProperty(b)?e[b]!==d[b]&&(k++,e[b]=d[b]):(q++,e[b]=d[b],k++));if(q>a)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;a<d.length;a++)f[a]=d[a]}else for(a in f={},d)Jc.call(d,a)&&(f[a]=d[a]);else f=d})},$digest:function(){var d,g,f,h,k=this.$$asyncQueue,l=this.$$postDigestQueue,q,x,s=b,L,Q=[],y,E,R;m("$digest");
+c=null;do{x=!1;for(L=this;k.length;){try{R=k.shift(),R.scope.$eval(R.expression)}catch(B){p.$$phase=null,e(B)}c=null}a:do{if(h=L.$$watchers)for(q=h.length;q--;)try{if(d=h[q])if((g=d.get(L))!==(f=d.last)&&!(d.eq?za(g,f):"number"==typeof g&&"number"==typeof f&&isNaN(g)&&isNaN(f)))x=!0,c=d,d.last=d.eq?ba(g):g,d.fn(g,f===n?g:f,L),5>s&&(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;m<l;m++)if(d[m])try{d[m].apply(null,k)}catch(n){e(n)}else d.splice(m,1),m--,l--;if(f)break;g=g.$parent}while(g);return h},$broadcast:function(a,b){for(var c=this,d=this,g={name:a,
+targetScope:this,preventDefault:function(){g.defaultPrevented=!0},defaultPrevented:!1},f=[g].concat(sa.call(arguments,1)),h,k;c=d;){g.currentScope=c;d=c.$$listeners[a]||[];h=0;for(k=d.length;h<k;h++)if(d[h])try{d[h].apply(null,f)}catch(m){e(m)}else d.splice(h,1),h--,k--;if(!(d=c.$$listenerCount[a]&&c.$$childHead||c!==this&&c.$$nextSibling))for(;c!==this&&!(d=c.$$nextSibling);)c=c.$parent}return g}};var p=new h;return p}]}function kd(){var b=/^\s*(https?|ftp|mailto|tel|file):/,a=/^\s*(https?|ftp|file|blob):|data:image\//;
+this.aHrefSanitizationWhitelist=function(a){return B(a)?(b=a,this):b};this.imgSrcSanitizationWhitelist=function(b){return B(b)?(a=b,this):a};this.$get=function(){return function(c,d){var e=d?a:b,g;if(!T||8<=T)if(g=ua(c).href,""!==g&&!g.match(e))return"unsafe:"+g;return c}}}function Ke(b){if("self"===b)return b;if(t(b)){if(-1<b.indexOf("***"))throw wa("iwcard",b);b=b.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08").replace("\\*\\*",".*").replace("\\*","[^:/.?&;]*");return RegExp("^"+
+b+"$")}if(fb(b))return RegExp("^"+b.source+"$");throw wa("imatcher");}function Kc(b){var a=[];B(b)&&q(b,function(b){a.push(Ke(b))});return a}function je(){this.SCE_CONTEXTS=ga;var b=["self"],a=[];this.resourceUrlWhitelist=function(a){arguments.length&&(b=Kc(a));return b};this.resourceUrlBlacklist=function(b){arguments.length&&(a=Kc(b));return a};this.$get=["$injector",function(c){function d(a){var b=function(a){this.$$unwrapTrustedValue=function(){return a}};a&&(b.prototype=new a);b.prototype.valueOf=
+function(){return this.$$unwrapTrustedValue()};b.prototype.toString=function(){return this.$$unwrapTrustedValue().toString()};return b}var e=function(a){throw wa("unsafe");};c.has("$sanitize")&&(e=c.get("$sanitize"));var g=d(),f={};f[ga.HTML]=d(g);f[ga.CSS]=d(g);f[ga.URL]=d(g);f[ga.JS]=d(g);f[ga.RESOURCE_URL]=d(f[ga.URL]);return{trustAs:function(a,b){var c=f.hasOwnProperty(a)?f[a]:null;if(!c)throw wa("icontext",a,b);if(null===b||b===s||""===b)return b;if("string"!==typeof b)throw wa("itype",a);return new c(b)},
+getTrusted:function(c,d){if(null===d||d===s||""===d)return d;var g=f.hasOwnProperty(c)?f[c]:null;if(g&&d instanceof g)return d.$$unwrapTrustedValue();if(c===ga.RESOURCE_URL){var g=ua(d.toString()),l,n,p=!1;l=0;for(n=b.length;l<n;l++)if("self"===b[l]?Lb(g):b[l].exec(g.href)){p=!0;break}if(p)for(l=0,n=a.length;l<n;l++)if("self"===a[l]?Lb(g):a[l].exec(g.href)){p=!1;break}if(p)return d;throw wa("insecurl",d.toString());}if(c===ga.HTML)return e(d);throw wa("unsafe");},valueOf:function(a){return a instanceof
+g?a.$$unwrapTrustedValue():a}}}]}function ie(){var b=!0;this.enabled=function(a){arguments.length&&(b=!!a);return b};this.$get=["$parse","$sniffer","$sceDelegate",function(a,c,d){if(b&&c.msie&&8>c.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||7<f),hasEvent:function(a){if("input"==a&&9==T)return!1;if(D(c[a])){var b=g.createElement("div");c[a]="on"+
+a in b}return c[a]},csp:Yb(),vendorPrefix:h,transitions:l,animations:n,android:d,msie:T,msieDocumentMode:f}}]}function me(){this.$get=["$rootScope","$browser","$q","$exceptionHandler",function(b,a,c,d){function e(e,h,m){var k=c.defer(),l=k.promise,n=B(m)&&!m;h=a.defer(function(){try{k.resolve(e())}catch(a){k.reject(a),d(a)}finally{delete g[l.$$timeoutId]}n||b.$apply()},h);l.$$timeoutId=h;g[h]=k;return l}var g={};e.cancel=function(b){return b&&b.$$timeoutId in g?(g[b.$$timeoutId].reject("canceled"),
+delete g[b.$$timeoutId],a.defer.cancel(b.$$timeoutId)):!1};return e}]}function ua(b,a){var c=b;T&&(W.setAttribute("href",c),c=W.href);W.setAttribute("href",c);return{href:W.href,protocol:W.protocol?W.protocol.replace(/:$/,""):"",host:W.host,search:W.search?W.search.replace(/^\?/,""):"",hash:W.hash?W.hash.replace(/^#/,""):"",hostname:W.hostname,port:W.port,pathname:"/"===W.pathname.charAt(0)?W.pathname:"/"+W.pathname}}function Lb(b){b=t(b)?ua(b):b;return b.protocol===Lc.protocol&&b.host===Lc.host}
+function ne(){this.$get=aa(O)}function jc(b){function a(d,e){if(X(d)){var g={};q(d,function(b,c){g[c]=a(c,b)});return g}return b.factory(d+c,e)}var c="Filter";this.register=a;this.$get=["$injector",function(a){return function(b){return a.get(b+c)}}];a("currency",Mc);a("date",Nc);a("filter",Le);a("json",Me);a("limitTo",Ne);a("lowercase",Oe);a("number",Oc);a("orderBy",Pc);a("uppercase",Pe)}function Le(){return function(b,a,c){if(!M(b))return b;var d=typeof c,e=[];e.check=function(a){for(var b=0;b<e.length;b++)if(!e[b](a))return!1;
+return!0};"function"!==d&&(c="boolean"===d&&c?function(a,b){return Qa.equals(a,b)}:function(a,b){if(a&&b&&"object"===typeof a&&"object"===typeof b){for(var d in a)if("$"!==d.charAt(0)&&Jc.call(a,d)&&c(a[d],b[d]))return!0;return!1}b=(""+b).toLowerCase();return-1<(""+a).toLowerCase().indexOf(b)});var g=function(a,b){if("string"==typeof b&&"!"===b.charAt(0))return!g(a,b.substr(1));switch(typeof a){case "boolean":case "number":case "string":return c(a,b);case "object":switch(typeof b){case "object":return c(a,
+b);default:for(var d in a)if("$"!==d.charAt(0)&&g(a[d],b))return!0}return!1;case "array":for(d=0;d<a.length;d++)if(g(a[d],b))return!0;return!1;default:return!1}};switch(typeof a){case "boolean":case "number":case "string":a={$:a};case "object":for(var f in a)(function(b){"undefined"!=typeof a[b]&&e.push(function(c){return g("$"==b?c:c&&c[b],a[b])})})(f);break;case "function":e.push(a);break;default:return b}d=[];for(f=0;f<b.length;f++){var h=b[f];e.check(h)&&d.push(h)}return d}}function Mc(b){var a=
+b.NUMBER_FORMATS;return function(b,d){D(d)&&(d=a.CURRENCY_SYM);return Qc(b,a.PATTERNS[1],a.GROUP_SEP,a.DECIMAL_SEP,2).replace(/\u00A4/g,d)}}function Oc(b){var a=b.NUMBER_FORMATS;return function(b,d){return Qc(b,a.PATTERNS[0],a.GROUP_SEP,a.DECIMAL_SEP,d)}}function Qc(b,a,c,d,e){if(null==b||!isFinite(b)||X(b))return"";var g=0>b;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)0<e&&(-1<b&&1>b)&&(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;k<l;k++)0===(l-k)%p&&0!==k&&(h+=c),h+=f.charAt(k);for(k=l;k<f.length;k++)0===(f.length-k)%n&&0!==k&&(h+=c),h+=f.charAt(k);for(;b.length<e;)b+="0";e&&"0"!==e&&(h+=d+b.substr(0,e))}m.push(g?a.negPre:a.posPre);m.push(h);m.push(g?a.negSuf:a.posSuf);return m.join("")}function tb(b,
+a,c){var d="";0>b&&(d="-",b=-b);for(b=""+b;b.length<a;)b="0"+b;c&&(b=b.substr(b.length-a));return d+b}function $(b,a,c,d){c=c||0;return function(e){e=e["get"+b]();if(0<c||e>-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);0<a?(d=0,e=a):(d=b.length+a,e=b.length);for(;d<e;d++)c.push(b[d]);return c}}function Pc(b){return function(a,c,d){function e(a,b){return Pa(b)?function(b,c){return a(c,b)}:a}function g(a,b){var c=typeof a,d=typeof b;return c==d?("string"==c&&(a=a.toLowerCase(),b=b.toLowerCase()),a===b?0:a<b?-1:1):c<d?-1:1}if(!M(a)||!c)return a;c=M(c)?c:[c];c=cd(c,function(a){var c=!1,d=a||Ea;if(t(a)){if("+"==a.charAt(0)||"-"==a.charAt(0))c=
+"-"==a.charAt(0),a=a.substring(1);d=b(a);if(d.constant){var f=d();return e(function(a,b){return g(a[f],b[f])},c)}}return e(function(a,b){return g(d(a),d(b))},c)});for(var f=[],h=0;h<a.length;h++)f.push(a[h]);return f.sort(e(function(a,b){for(var d=0;d<c.length;d++){var e=c[d](a,b);if(0!==e)return e}return 0},d))}}function xa(b){P(b)&&(b={link:b});b.restrict=b.restrict||"AC";return aa(b)}function Uc(b,a,c,d){function e(a,c){c=c?"-"+ib(c,"-"):"";d.removeClass(b,(a?vb:wb)+c);d.addClass(b,(a?wb:vb)+c)}
+var g=this,f=b.parent().controller("form")||xb,h=0,m=g.$error={},k=[];g.$name=a.name||a.ngForm;g.$dirty=!1;g.$pristine=!0;g.$valid=!0;g.$invalid=!1;f.$addControl(g);b.addClass(Ma);e(!0);g.$addControl=function(a){Ba(a.$name,"input");k.push(a);a.$name&&(g[a.$name]=a)};g.$removeControl=function(a){a.$name&&g[a.$name]===a&&delete g[a.$name];q(m,function(b,c){g.$setValidity(c,!0,a)});Fa(k,a)};g.$setValidity=function(a,b,c){var d=m[a];if(b)d&&(Fa(d,c),d.length||(h--,h||(e(b),g.$valid=!0,g.$invalid=!1),
+m[a]=!1,e(!0,a),f.$setValidity(a,!0,g)));else{h||e(b);if(d){if(-1!=gb(d,c))return}else m[a]=d=[],h++,e(!1,a),f.$setValidity(a,!1,g);d.push(c);g.$valid=!1;g.$invalid=!0}};g.$setDirty=function(){d.removeClass(b,Ma);d.addClass(b,yb);g.$dirty=!0;g.$pristine=!1;f.$setDirty()};g.$setPristine=function(){d.removeClass(b,yb);d.addClass(b,Ma);g.$dirty=!1;g.$pristine=!0;q(k,function(a){a.$setPristine()})}}function qa(b,a,c,d){b.$setValidity(a,c);return c?d:s}function Te(b,a,c){var d=c.prop("validity");X(d)&&
+b.$parsers.push(function(c){if(b.$error[a]||!(d.badInput||d.customError||d.typeMismatch)||d.valueMissing)return c;b.$setValidity(a,!1)})}function bb(b,a,c,d,e,g){var f=a.prop("validity");if(!e.android){var h=!1;a.on("compositionstart",function(a){h=!0});a.on("compositionend",function(){h=!1;m()})}var m=function(){if(!h){var e=a.val();Pa(c.ngTrim||"T")&&(e=ca(e));if(d.$viewValue!==e||f&&""===e&&!f.valueMissing)b.$$phase?d.$setViewValue(e):b.$apply(function(){d.$setViewValue(e)})}};if(e.hasEvent("input"))a.on("input",
+m);else{var k,l=function(){k||(k=g.defer(function(){m();k=null}))};a.on("keydown",function(a){a=a.keyCode;91===a||(15<a&&19>a||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<a.length&&(d[a[c]]=+b)}),new Date(d.yyyy,d.MM-1,d.dd,d.HH,d.mm)):NaN}}function cb(b,a,c,d){return function(e,g,f,h,m,k,l){bb(e,g,f,h,m,k);h.$parsers.push(function(d){if(h.$isEmpty(d))return h.$setValidity(b,!0),null;if(a.test(d))return h.$setValidity(b,!0),c(d);h.$setValidity(b,!1);return s});h.$formatters.push(function(a){return ra(a)?l("date")(a,d):""});f.min&&(e=function(a){var b=h.$isEmpty(a)||c(a)>=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(;d<a.length;d++){for(var e=a[d],l=0;l<b.length;l++)if(e==b[l])continue a;c.push(e)}return c}function e(a){if(!M(a)){if(t(a))return a.split(" ");if(X(a)){var b=[];q(a,function(a,c){a&&b.push(c)});return b}}return a}return{restrict:"AC",
+link:function(g,f,h){function m(a,b){var c=f.data("$classCounts")||{},d=[];q(a,function(a){if(0<b||c[a])c[a]=(c[a]||0)+b,c[a]===+(0<b)&&d.push(a)});f.data("$classCounts",c);return d.join(" ")}function k(b){if(!0===a||g.$index%2===a){var k=e(b||[]);if(!l){var r=m(k,1);h.$addClass(r)}else if(!za(b,l)){var q=e(l),r=d(k,q),k=d(q,k),k=m(k,-1),r=m(r,1);0===r.length?c.removeClass(f,k):0===k.length?c.addClass(f,r):c.setClass(f,r,k)}}l=ba(b)}var l;g.$watch(h[b],k,!0);h.$observe("class",function(a){k(g.$eval(h[b]))});
+"ngClass"!==b&&g.$watch("$index",function(c,d){var f=c&1;if(f!==d&1){var k=e(g.$eval(h[b]));f===a?(f=m(k,1),h.$addClass(f)):(f=m(k,-1),h.$removeClass(f))}})}}}]}var I=function(b){return t(b)?b.toLowerCase():b},Jc=Object.prototype.hasOwnProperty,Ga=function(b){return t(b)?b.toUpperCase():b},T,y,Ha,sa=[].slice,Ue=[].push,ya=Object.prototype.toString,Oa=v("ng"),Qa=O.angular||(O.angular={}),Sa,La,ka=["0","0","0"];T=Y((/msie (\d+)/.exec(I(navigator.userAgent))||[])[1]);isNaN(T)&&(T=Y((/trident\/.*; rv:(\d+)/.exec(I(navigator.userAgent))||
+[])[1]));C.$inject=[];Ea.$inject=[];var ca=function(){return String.prototype.trim?function(b){return t(b)?b.trim():b}:function(b){return t(b)?b.replace(/^\s\s*/,"").replace(/\s\s*$/,""):b}}();La=9>T?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,'<select multiple="multiple">',"</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_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<d.length;c++)Ia(d[c]);b.innerHTML=a},empty:pc},function(b,a){N.prototype[a]=function(a,d){var e,g;if(b!==pc&&(2==b.length&&b!==Jb&&b!==oc?a:d)===s){if(X(a)){for(e=0;e<this.length;e++)if(b===nc)b(this[e],a);else for(g in a)b(this[e],g,a[g]);return this}e=b.$dv;g=e===s?Math.min(this.length,1):this.length;for(var f=0;f<g;f++){var h=b(this[f],a,d);e=
+e?e+h:h}return e}for(e=0;e<this.length;e++)b(this[e],a,d);return this}});q({removeData:lc,dealoc:Ia,on:function a(c,d,e,g){if(B(g))throw Hb("onargs");var f=la(c,"events"),h=la(c,"handle");f||la(c,"events",f={});h||la(c,"handle",h=xe(c,f));q(d.split(" "),function(d){var g=f[d];if(!g){if("mouseenter"==d||"mouseleave"==d){var l=U.body.contains||U.body.compareDocumentPosition?function(a,c){var d=9===a.nodeType?a.documentElement:a,e=c&&c.parentNode;return a===e||!!(e&&1===e.nodeType&&(d.contains?d.contains(e):
+a.compareDocumentPosition&&a.compareDocumentPosition(e)&16))}:function(a,c){if(c)for(;c=c.parentNode;)if(c===a)return!0;return!1};f[d]=[];a(c,{mouseleave:"mouseout",mouseenter:"mouseover"}[d],function(a){var c=a.relatedTarget;c&&(c===this||l(this,c))||h(a,d)})}else qb(c,d,h),f[d]=[];g=f[d]}g.push(e)})},off:mc,one:function(a,c,d){a=y(a);a.on(c,function g(){a.off(c,d);a.off(c,g)});a.on(c,d)},replaceWith:function(a,c){var d,e=a.parentNode;Ia(a);q(new N(c),function(c){d?e.insertBefore(c,d.nextSibling):
+e.replaceChild(c,a);d=c})},children:function(a){var c=[];q(a.childNodes,function(a){1===a.nodeType&&c.push(a)});return c},contents:function(a){return a.contentDocument||a.childNodes||[]},append:function(a,c){q(new N(c),function(c){1!==a.nodeType&&11!==a.nodeType||a.appendChild(c)})},prepend:function(a,c){if(1===a.nodeType){var d=a.firstChild;q(new N(c),function(c){a.insertBefore(c,d)})}},wrap:function(a,c){c=y(c)[0];var d=a.parentNode;d&&d.replaceChild(c,a);c.appendChild(a)},remove:function(a){Ia(a);
+var c=a.parentNode;c&&c.removeChild(a)},after:function(a,c){var d=a,e=a.parentNode;q(new N(c),function(a){e.insertBefore(a,d.nextSibling);d=a})},addClass:lb,removeClass:kb,toggleClass:function(a,c,d){c&&q(c.split(" "),function(c){var g=d;D(g)&&(g=!Jb(a,c));(g?lb:kb)(a,c)})},parent:function(a){return(a=a.parentNode)&&11!==a.nodeType?a:null},next:function(a){if(a.nextElementSibling)return a.nextElementSibling;for(a=a.nextSibling;null!=a&&1!==a.nodeType;)a=a.nextSibling;return a},find:function(a,c){return a.getElementsByTagName?
+a.getElementsByTagName(c):[]},clone:Ib,triggerHandler:function(a,c,d){c=(la(a,"events")||{})[c];d=d||[];var e=[{preventDefault:C,stopPropagation:C}];q(c,function(c){c.apply(a,e.concat(d))})}},function(a,c){N.prototype[c]=function(c,e,g){for(var f,h=0;h<this.length;h++)D(f)?(f=a(this[h],c,e,g),B(f)&&(f=y(f))):kc(f,a(this[h],c,e,g));return B(f)?f:this};N.prototype.bind=N.prototype.on;N.prototype.unbind=N.prototype.off});Wa.prototype={put:function(a,c){this[Ja(a)]=c},get:function(a){return this[Ja(a)]},
+remove:function(a){var c=this[a=Ja(a)];delete this[a];return c}};var ze=/^function\s*[^\(]*\(\s*([^\)]*)\)/m,Ae=/,/,Be=/^\s*(_?)(\S+?)\1\s*$/,ye=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Xa=v("$injector"),Ve=v("$animate"),Ud=["$provide",function(a){this.$$selectors={};this.register=function(c,d){var e=c+"-animation";if(c&&"."!=c.charAt(0))throw Ve("notcsel",c);this.$$selectors[c.substr(1)]=e;a.factory(e,d)};this.classNameFilter=function(a){1===arguments.length&&(this.$$classNameFilter=a instanceof RegExp?
+a:null);return this.$$classNameFilter};this.$get=["$timeout","$$asyncCallback",function(a,d){return{enter:function(a,c,f,h){f?f.after(a):c.prepend(a);h&&d(h)},leave:function(a,c){a.remove();c&&d(c)},move:function(a,c,d,h){this.enter(a,c,d,h)},addClass:function(a,c,f){c=t(c)?c:M(c)?c.join(" "):"";q(a,function(a){lb(a,c)});f&&d(f)},removeClass:function(a,c,f){c=t(c)?c:M(c)?c.join(" "):"";q(a,function(a){kb(a,c)});f&&d(f)},setClass:function(a,c,f,h){q(a,function(a){lb(a,c);kb(a,f)});h&&d(h)},enabled:C}}]}],
+ja=v("$compile");fc.$inject=["$provide","$$sanitizeUriProvider"];var De=/^(x[\:\-_]|data[\:\-_])/i,zc=v("$interpolate"),We=/^([^\?#]*)(\?([^#]*))?(#(.*))?$/,Ge={http:80,https:443,ftp:21},Nb=v("$location");Ec.prototype=Ob.prototype=Dc.prototype={$$html5:!1,$$replace:!1,absUrl:rb("$$absUrl"),url:function(a,c){if(D(a))return this.$$url;var d=We.exec(a);d[1]&&this.path(decodeURIComponent(d[1]));(d[2]||d[1])&&this.search(d[3]||"");this.hash(d[5]||"",c);return this},protocol:rb("$$protocol"),host:rb("$$host"),
+port:rb("$$port"),path:Fc("$$path",function(a){return"/"==a.charAt(0)?a:"/"+a}),search:function(a,c){switch(arguments.length){case 0:return this.$$search;case 1:if(t(a))this.$$search=ac(a);else if(X(a))this.$$search=a;else throw Nb("isrcharg");break;default:D(c)||null===c?delete this.$$search[a]:this.$$search[a]=c}this.$$compose();return this},hash:Fc("$$hash",Ea),replace:function(){this.$$replace=!0;return this}};var Ca=v("$parse"),Ic={},va,Na={"null":function(){return null},"true":function(){return!0},
+"false":function(){return!1},undefined:C,"+":function(a,c,d,e){d=d(a,c);e=e(a,c);return B(d)?B(e)?d+e:d:B(e)?e:s},"-":function(a,c,d,e){d=d(a,c);e=e(a,c);return(B(d)?d:0)-(B(e)?e:0)},"*":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)},"=":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 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<this.text.length;){this.ch=this.text.charAt(this.index);if(this.is("\"'"))this.readString(this.ch);else if(this.isNumber(this.ch)||this.is(".")&&this.isNumber(this.peek()))this.readNumber();else if(this.isIdent(this.ch))this.readIdent(),this.was("{,")&&("{"===a[0]&&(c=this.tokens[this.tokens.length-1]))&&(c.json=-1===c.text.indexOf("."));
+else if(this.is("(){}[].,;:?"))this.tokens.push({index:this.index,text:this.ch,json:this.was(":[,")&&this.is("{[")||this.is("}]:,")}),this.is("{[")&&a.unshift(this.ch),this.is("}]")&&a.shift(),this.index++;else if(this.isWhitespace(this.ch)){this.index++;continue}else{var d=this.ch+this.peek(),e=d+this.peek(2),g=Na[this.ch],f=Na[d],h=Na[e];h?(this.tokens.push({index:this.index,text:e,fn:h}),this.index+=3):f?(this.tokens.push({index:this.index,text:d,fn:f}),this.index+=2):g?(this.tokens.push({index:this.index,
+text:this.ch,fn:g,json:this.was("[,:")&&this.is("+-")}),this.index+=1):this.throwError("Unexpected next character ",this.index,this.index+1)}this.lastCh=this.ch}return this.tokens},is:function(a){return-1!==a.indexOf(this.ch)},was:function(a){return-1!==a.indexOf(this.lastCh)},peek:function(a){a=a||1;return this.index+a<this.text.length?this.text.charAt(this.index+a):!1},isNumber:function(a){return"0"<=a&&"9">=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<this.text.length;){var d=I(this.text.charAt(this.index));if("."==d||this.isNumber(d))a+=d;else{var e=this.peek();if("e"==d&&this.isExpOperator(e))a+=
+d;else if(this.isExpOperator(d)&&e&&this.isNumber(e)&&"e"==a.charAt(a.length-1))a+=d;else if(!this.isExpOperator(d)||e&&this.isNumber(e)||"e"!=a.charAt(a.length-1))break;else this.throwError("Invalid exponent")}this.index++}a*=1;this.tokens.push({index:c,text:a,json:!0,fn:function(){return a}})},readIdent:function(){for(var a=this,c="",d=this.index,e,g,f,h;this.index<this.text.length;){h=this.text.charAt(this.index);if("."===h||this.isIdent(h)||this.isNumber(h))"."===h&&(e=this.index),c+=h;else break;
+this.index++}if(e)for(g=this.index;g<this.text.length;){h=this.text.charAt(g);if("("===h){f=c.substr(e-d+1);c=c.substr(0,e-d);this.index=g;break}if(this.isWhitespace(h))g++;else break}d={index:d,text:c};if(Na.hasOwnProperty(c))d.fn=Na[c],d.json=Na[c];else{var m=Hc(c,this.options,this.text);d.fn=A(function(a,c){return m(a,c)},{assign:function(d,e){return sb(d,c,e,a.text,a.options)}})}this.tokens.push(d);f&&(this.tokens.push({index:e,text:".",json:!1}),this.tokens.push({index:e+1,text:f,json:!1}))},
+readString:function(a){var c=this.index;this.index++;for(var d="",e=a,g=!1;this.index<this.text.length;){var f=this.text.charAt(this.index),e=e+f;if(g)"u"===f?(f=this.text.substring(this.index+1,this.index+5),f.match(/[\da-f]{4}/i)||this.throwError("Invalid unicode escape [\\u"+f+"]"),this.index+=4,d+=String.fromCharCode(parseInt(f,16))):d=(g=Xe[f])?d+g:d+f,g=!1;else if("\\"===f)g=!0;else{if(f===a){this.index++;this.tokens.push({index:c,text:e,string:d,json:!0,fn:function(){return d}});return}d+=
+f}this.index++}this.throwError("Unterminated quote",c)}};var ab=function(a,c,d){this.lexer=a;this.$filter=c;this.options=d};ab.ZERO=A(function(){return 0},{constant:!0});ab.prototype={constructor:ab,parse:function(a,c){this.text=a;this.json=c;this.tokens=this.lexer.lex(a);c&&(this.assignment=this.logicalOR,this.functionCall=this.fieldAccess=this.objectIndex=this.filterChain=function(){this.throwError("is not valid json",{text:a,index:0})});var d=c?this.primary():this.statements();0!==this.tokens.length&&
+this.throwError("is an unexpected token",this.tokens[0]);d.literal=!!d.literal;d.constant=!!d.constant;return d},primary:function(){var a;if(this.expect("("))a=this.filterChain(),this.consume(")");else if(this.expect("["))a=this.arrayDeclaration();else if(this.expect("{"))a=this.object();else{var c=this.expect();(a=c.fn)||this.throwError("not a primary expression",c);c.json&&(a.constant=!0,a.literal=!0)}for(var d;c=this.expect("(","[",".");)"("===c.text?(a=this.functionCall(a,d),d=null):"["===c.text?
+(d=a,a=this.objectIndex(a)):"."===c.text?(d=a,a=this.fieldAccess(a)):this.throwError("IMPOSSIBLE");return a},throwError:function(a,c){throw Ca("syntax",c.text,a,c.index+1,this.text,this.text.substring(c.index));},peekToken:function(){if(0===this.tokens.length)throw Ca("ueoe",this.text);return this.tokens[0]},peek:function(a,c,d,e){if(0<this.tokens.length){var g=this.tokens[0],f=g.text;if(f===a||f===c||f===d||f===e||!(a||c||d||e))return g}return!1},expect:function(a,c,d,e){return(a=this.peek(a,c,d,
+e))?(this.json&&!a.json&&this.throwError("is not valid json",a),this.tokens.shift(),a):!1},consume:function(a){this.expect(a)||this.throwError("is unexpected, expecting ["+a+"]",this.peek())},unaryFn:function(a,c){return A(function(d,e){return a(d,e,c)},{constant:c.constant})},ternaryFn:function(a,c,d){return A(function(e,g){return a(e,g)?c(e,g):d(e,g)},{constant:a.constant&&c.constant&&d.constant})},binaryFn:function(a,c,d){return A(function(e,g){return c(e,g,a,d)},{constant:a.constant&&d.constant})},
+statements:function(){for(var a=[];;)if(0<this.tokens.length&&!this.peek("}",")",";","]")&&a.push(this.filterChain()),!this.expect(";"))return 1===a.length?a[0]:function(c,d){for(var e,g=0;g<a.length;g++){var f=a[g];f&&(e=f(c,d))}return e}},filterChain:function(){for(var a=this.expression(),c;;)if(c=this.expect("|"))a=this.binaryFn(a,c.fn,this.filter());else return a},filter:function(){for(var a=this.expect(),c=this.$filter(a.text),d=[];;)if(a=this.expect(":"))d.push(this.expression());else{var e=
+function(a,e,h){h=[h];for(var m=0;m<d.length;m++)h.push(d[m](a,e));return c.apply(a,h)};return function(){return e}}},expression:function(){return this.assignment()},assignment:function(){var a=this.ternary(),c,d;return(d=this.expect("="))?(a.assign||this.throwError("implies assignment but ["+this.text.substring(0,d.index)+"] can not be assigned to",d),c=this.ternary(),function(d,g){return a.assign(d,c(d,g),g)}):a},ternary:function(){var a=this.logicalOR(),c,d;if(this.expect("?")){c=this.ternary();
+if(d=this.expect(":"))return this.ternaryFn(a,c,this.ternary());this.throwError("expected :",d)}else return a},logicalOR:function(){for(var a=this.logicalAND(),c;;)if(c=this.expect("||"))a=this.binaryFn(a,c.fn,this.logicalAND());else return a},logicalAND:function(){var a=this.equality(),c;if(c=this.expect("&&"))a=this.binaryFn(a,c.fn,this.logicalAND());return a},equality:function(){var a=this.relational(),c;if(c=this.expect("==","!=","===","!=="))a=this.binaryFn(a,c.fn,this.equality());return a},
+relational:function(){var a=this.additive(),c;if(c=this.expect("<",">","<=",">="))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;k<d.length;k++)h.push(d[k](g,f));k=a(g,f,m)||C;$a(m,e.text);$a(k,e.text);h=k.apply?k.apply(m,h):k(h[0],h[1],h[2],h[3],h[4]);return $a(h,e.text)}},arrayDeclaration:function(){var a=[],c=!0;if("]"!==this.peekToken().text){do{if(this.peek("]"))break;
+var d=this.expression();a.push(d);d.constant||(c=!1)}while(this.expect(","))}this.consume("]");return A(function(c,d){for(var f=[],h=0;h<a.length;h++)f.push(a[h](c,d));return f},{literal:!0,constant:c})},object:function(){var a=[],c=!0;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;var d=this.expect(),d=d.string||d.text;this.consume(":");var e=this.expression();a.push({key:d,value:e});e.constant||(c=!1)}while(this.expect(","))}this.consume("}");return A(function(c,d){for(var e={},m=0;m<
+a.length;m++){var k=a[m];e[k.key]=k.value(c,d)}return e},{literal:!0,constant:c})}};var Pb={},wa=v("$sce"),ga={HTML:"html",CSS:"css",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},W=U.createElement("a"),Lc=ua(O.location.href,!0);jc.$inject=["$provide"];Mc.$inject=["$locale"];Oc.$inject=["$locale"];var Rc=".",Se={yyyy:$("FullYear",4),yy:$("FullYear",2,0,!0),y:$("FullYear",1),MMMM:ub("Month"),MMM:ub("Month",!0),MM:$("Month",2,1),M:$("Month",1,1),dd:$("Date",2),d:$("Date",1),HH:$("Hours",2),H:$("Hours",
+1),hh:$("Hours",2,-12),h:$("Hours",1,-12),mm:$("Minutes",2),m:$("Minutes",1),ss:$("Seconds",2),s:$("Seconds",1),sss:$("Milliseconds",3),EEEE:ub("Day"),EEE:ub("Day",!0),a:function(a,c){return 12>a.getHours()?c.AMPMS[0]:c.AMPMS[1]},Z:function(a){a=-1*a.getTimezoneOffset();return a=(0<=a?"+":"")+(tb(Math[0<a?"floor":"ceil"](a/60),2)+tb(Math.abs(a%60),2))},ww:Tc(2),w:Tc(1)},Re=/((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/,Qe=/^\-?\d+$/;Nc.$inject=["$locale"];var Oe=
+aa(I),Pe=aa(Ga);Pc.$inject=["$parse"];var ld=aa({restrict:"E",compile:function(a,c){8>=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;f<h;f++)if(t=a===S?f:S[f],C=a[t],C=n(t,C,f),Ba(C,"`track by` id"),H.hasOwnProperty(C))v=H[C],delete H[C],w[C]=v,D[f]=v;else{if(w.hasOwnProperty(C))throw q(D,function(a){a&&a.scope&&(H[a.id]=a)}),d("dupes",k,C);D[f]={id:C};w[C]=!1}for(t in H)H.hasOwnProperty(t)&&(v=H[t],
+f=Db(v.clone),c.leave(f),q(f,function(a){a.$$NG_REMOVED=!0}),v.scope.$destroy());f=0;for(h=S.length;f<h;f++){t=a===S?f:S[f];C=a[t];v=D[f];D[f-1]&&(l=D[f-1].clone[D[f-1].clone.length-1]);if(v.scope){R=v.scope;n=l;do n=n.nextSibling;while(n&&n.$$NG_REMOVED);v.clone[0]!=n&&c.move(Db(v.clone),null,y(l));l=v.clone[v.clone.length-1]}else R=e.$new();R[z]=C;B&&(R[B]=t);R.$index=f;R.$first=0===f;R.$last=f===E-1;R.$middle=!(R.$first||R.$last);R.$odd=!(R.$even=0===(f&1));v.scope||m(R,function(a){a[a.length++]=
+U.createComment(" end ngRepeat: "+k+" ");c.enter(a,null,y(l));l=a;v.scope=R;v.clone=a;w[v.id]=v})}H=w})}}}],Hd=["$animate",function(a){return function(c,d,e){c.$watch(e.ngShow,function(c){a[Pa(c)?"removeClass":"addClass"](d,"ng-hide")})}}],Ad=["$animate",function(a){return function(c,d,e){c.$watch(e.ngHide,function(c){a[Pa(c)?"addClass":"removeClass"](d,"ng-hide")})}}],Id=xa(function(a,c,d){a.$watch(d.ngStyle,function(a,d){d&&a!==d&&q(d,function(a,d){c.css(d,"")});a&&c.css(a)},!0)}),Jd=["$animate",
+function(a){return{restrict:"EA",require:"ngSwitch",controller:["$scope",function(){this.cases={}}],link:function(c,d,e,g){var f,h,m,k=[];c.$watch(e.ngSwitch||e.on,function(d){var n,p=k.length;if(0<p){if(m){for(n=0;n<p;n++)m[n].remove();m=null}m=[];for(n=0;n<p;n++){var r=h[n];k[n].$destroy();m[n]=r;a.leave(r,function(){m.splice(n,1);0===m.length&&(m=null)})}}h=[];k=[];if(f=g.cases["!"+d]||g.cases["?"])c.$eval(e.change),q(f,function(d){var e=c.$new();k.push(e);d.transclude(e,function(c){var e=d.element;
+h.push(c);a.enter(c,e.parent(),e)})})})}}}],Kd=xa({transclude:"element",priority:800,require:"^ngSwitch",link:function(a,c,d,e,g){e.cases["!"+d.ngSwitchWhen]=e.cases["!"+d.ngSwitchWhen]||[];e.cases["!"+d.ngSwitchWhen].push({transclude:g,element:c})}}),Ld=xa({transclude:"element",priority:800,require:"^ngSwitch",link:function(a,c,d,e,g){e.cases["?"]=e.cases["?"]||[];e.cases["?"].push({transclude:g,element:c})}}),Nd=xa({link:function(a,c,d,e,g){if(!g)throw v("ngTransclude")("orphan",ha(c));g(function(a){c.empty();
+c.append(a)})}}),nd=["$templateCache",function(a){return{restrict:"E",terminal:!0,compile:function(c,d){"text/ng-template"==d.type&&a.put(d.id,c[0].text)}}}],cf=v("ngOptions"),Md=aa({terminal:!0}),od=["$compile","$parse",function(a,c){var d=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,e={$setViewValue:C};return{restrict:"E",require:["select","?ngModel"],
+controller:["$element","$scope","$attrs",function(a,c,d){var m=this,k={},l=e,n;m.databound=d.ngModel;m.init=function(a,c,d){l=a;n=d};m.addOption=function(c){Ba(c,'"option value"');k[c]=!0;l.$viewValue==c&&(a.val(c),n.parent()&&n.remove())};m.removeOption=function(a){this.hasOption(a)&&(delete k[a],l.$viewValue==a&&this.renderUnknownOption(a))};m.renderUnknownOption=function(c){c="? "+Ja(c)+" ?";n.val(c);a.prepend(n);a.val(c);n.prop("selected",!0)};m.hasOption=function(a){return k.hasOwnProperty(a)};
+c.$on("$destroy",function(){m.renderUnknownOption=C})}],link:function(e,f,h,m){function k(a,c,d,e){d.$render=function(){var a=d.$viewValue;e.hasOption(a)?(G.parent()&&G.remove(),c.val(a),""===a&&v.prop("selected",!0)):D(a)&&v?c.val(""):e.renderUnknownOption(a)};c.on("change",function(){a.$apply(function(){G.parent()&&G.remove();d.$setViewValue(c.val())})})}function l(a,c,d){var e;d.$render=function(){var a=new Wa(d.$viewValue);q(c.find("option"),function(c){c.selected=B(a.get(c.value))})};a.$watch(function(){za(e,
+d.$viewValue)||(e=ba(d.$viewValue),d.$render())});c.on("change",function(){a.$apply(function(){var a=[];q(c.find("option"),function(c){c.selected&&a.push(c.value)});d.$setViewValue(a)})})}function n(e,f,g){function h(){var a={"":[]},c=[""],d,k,s,t,u;t=g.$modelValue;u=y(e)||[];var D=n?Tb(u):u,G,J,A;J={};s=!1;var E,I;if(r)if(v&&M(t))for(s=new Wa([]),A=0;A<t.length;A++)J[l]=t[A],s.put(v(e,J),t[A]);else s=new Wa(t);for(A=0;G=D.length,A<G;A++){k=A;if(n){k=D[A];if("$"===k.charAt(0))continue;J[n]=k}J[l]=
+u[k];d=p(e,J)||"";(k=a[d])||(k=a[d]=[],c.push(d));r?d=B(s.remove(v?v(e,J):q(e,J))):(v?(d={},d[l]=t,d=v(e,d)===v(e,J)):d=t===q(e,J),s=s||d);E=m(e,J);E=B(E)?E:"";k.push({id:v?v(e,J):n?D[A]:A,label:E,selected:d})}r||(z||null===t?a[""].unshift({id:"",label:"",selected:!s}):s||a[""].unshift({id:"?",label:"",selected:!0}));J=0;for(D=c.length;J<D;J++){d=c[J];k=a[d];x.length<=J?(t={element:C.clone().attr("label",d),label:k.label},u=[t],x.push(u),f.append(t.element)):(u=x[J],t=u[0],t.label!=d&&t.element.attr("label",
+t.label=d));E=null;A=0;for(G=k.length;A<G;A++)s=k[A],(d=u[A+1])?(E=d.element,d.label!==s.label&&E.text(d.label=s.label),d.id!==s.id&&E.val(d.id=s.id),d.selected!==s.selected&&E.prop("selected",d.selected=s.selected)):(""===s.id&&z?I=z:(I=w.clone()).val(s.id).attr("selected",s.selected).text(s.label),u.push({element:I,label:s.label,id:s.id,selected:s.selected}),E?E.after(I):t.element.append(I),E=I);for(A++;u.length>A;)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<w;p++)for(a=x[p],m=1,t=a.length;m<t;m++){if((h=a[m].element)[0].selected){h=h.val();n&&(d[n]=h);if(v)for(u=0;u<c.length&&(d[l]=c[u],v(e,d)!=h);u++);else d[l]=c[h];k.push(q(e,
+d))}}else{h=f.val();if("?"==h)k=s;else if(""===h)k=null;else if(v)for(u=0;u<c.length;u++){if(d[l]=c[u],v(e,d)==h){k=q(e,d);break}}else d[l]=c[h],n&&(d[n]=h),k=q(e,d);1<x[0].length&&x[0][1].id!==h&&(x[0][1].selected=!1)}g.$setViewValue(k)})});g.$render=h;e.$watch(h)}if(m[1]){var p=m[0];m=m[1];var r=h.multiple,t=h.ngOptions,z=!1,v,w=y(U.createElement("option")),C=y(U.createElement("optgroup")),G=w.clone();h=0;for(var x=f.children(),A=x.length;h<A;h++)if(""===x[h].value){v=z=x.eq(h);break}p.init(m,z,
+G);r&&(m.$isEmpty=function(a){return!a||0===a.length});t?n(e,f,m):r?l(e,f,m):k(e,f,m,p)}}}}],qd=["$interpolate",function(a){var c={addOption:C,removeOption:C};return{restrict:"E",priority:100,compile:function(d,e){if(D(e.value)){var g=a(d.text(),!0);g||e.$set("value",d.text())}return function(a,d,e){var k=d.parent(),l=k.data("$selectController")||k.parent().data("$selectController");l&&l.databound?d.prop("selected",!1):l=c;g?a.$watch(g,function(a,c){e.$set("value",a);a!==c&&l.removeOption(c);l.addOption(a)}):
+l.addOption(e.value);d.on("$destroy",function(){l.removeOption(e.value)})}}}}],pd=aa({restrict:"E",terminal:!1});O.angular.bootstrap?console.log("WARNING: Tried to load angular more than once."):((Ha=O.jQuery)?(y=Ha,A(Ha.fn,{scope:Ka.scope,isolateScope:Ka.isolateScope,controller:Ka.controller,injector:Ka.injector,inheritedData:Ka.inheritedData}),Fb("remove",!0,!0,!1),Fb("empty",!1,!1,!1),Fb("html",!1,!1,!0)):y=N,Qa.element=y,hd(Qa),y(U).ready(function(){ed(U,cc)}))})(window,document);
+!angular.$$csp()&&angular.element(document).find("head").prepend('<style type="text/css">@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;}</style>');
+//# sourceMappingURL=angular.min.js.map
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-resource.min.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-resource.min.js
new file mode 100644
index 0000000..3f196c3
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-resource.min.js
@@ -0,0 +1,13 @@
+/*
+ AngularJS v1.3.0-beta.5
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(H,a,A){'use strict';function D(p,g){g=g||{};a.forEach(g,function(a,c){delete g[c]});for(var c in p)!p.hasOwnProperty(c)||"$"===c.charAt(0)&&"$"===c.charAt(1)||(g[c]=p[c]);return g}var v=a.$$minErr("$resource"),C=/^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;a.module("ngResource",["ng"]).factory("$resource",["$http","$q",function(p,g){function c(a,c){this.template=a;this.defaults=c||{};this.urlParams={}}function t(n,w,l){function r(h,d){var e={};d=x({},w,d);s(d,function(b,d){u(b)&&(b=b());var k;if(b&&
+b.charAt&&"@"==b.charAt(0)){k=h;var a=b.substr(1);if(null==a||""===a||"hasOwnProperty"===a||!C.test("."+a))throw v("badmember",a);for(var a=a.split("."),f=0,c=a.length;f<c&&k!==A;f++){var g=a[f];k=null!==k?k[g]:A}}else k=b;e[d]=k});return e}function e(a){return a.resource}function f(a){D(a||{},this)}var F=new c(n);l=x({},B,l);s(l,function(h,d){var c=/^(POST|PUT|PATCH)$/i.test(h.method);f[d]=function(b,d,k,w){var q={},n,l,y;switch(arguments.length){case 4:y=w,l=k;case 3:case 2:if(u(d)){if(u(b)){l=
+b;y=d;break}l=d;y=k}else{q=b;n=d;l=k;break}case 1:u(b)?l=b:c?n=b:q=b;break;case 0:break;default:throw v("badargs",arguments.length);}var t=this instanceof f,m=t?n:h.isArray?[]:new f(n),z={},B=h.interceptor&&h.interceptor.response||e,C=h.interceptor&&h.interceptor.responseError||A;s(h,function(a,b){"params"!=b&&("isArray"!=b&&"interceptor"!=b)&&(z[b]=G(a))});c&&(z.data=n);F.setUrlParams(z,x({},r(n,h.params||{}),q),h.url);q=p(z).then(function(b){var d=b.data,k=m.$promise;if(d){if(a.isArray(d)!==!!h.isArray)throw v("badcfg",
+h.isArray?"array":"object",a.isArray(d)?"array":"object");h.isArray?(m.length=0,s(d,function(b){m.push(new f(b))})):(D(d,m),m.$promise=k)}m.$resolved=!0;b.resource=m;return b},function(b){m.$resolved=!0;(y||E)(b);return g.reject(b)});q=q.then(function(b){var a=B(b);(l||E)(a,b.headers);return a},C);return t?q:(m.$promise=q,m.$resolved=!1,m)};f.prototype["$"+d]=function(b,a,k){u(b)&&(k=a,a=b,b={});b=f[d].call(this,b,this,a,k);return b.$promise||b}});f.bind=function(a){return t(n,x({},w,a),l)};return f}
+var B={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},E=a.noop,s=a.forEach,x=a.extend,G=a.copy,u=a.isFunction;c.prototype={setUrlParams:function(c,g,l){var r=this,e=l||r.template,f,p,h=r.urlParams={};s(e.split(/\W/),function(a){if("hasOwnProperty"===a)throw v("badname");!/^\d+$/.test(a)&&(a&&RegExp("(^|[^\\\\]):"+a+"(\\W|$)").test(e))&&(h[a]=!0)});e=e.replace(/\\:/g,":");g=g||{};s(r.urlParams,function(d,c){f=g.hasOwnProperty(c)?
+g[c]:r.defaults[c];a.isDefined(f)&&null!==f?(p=encodeURIComponent(f).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"%20").replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+"),e=e.replace(RegExp(":"+c+"(\\W|$)","g"),function(a,c){return p+c})):e=e.replace(RegExp("(/?):"+c+"(\\W|$)","g"),function(a,c,d){return"/"==d.charAt(0)?d:c+d})});e=e.replace(/\/+$/,"")||"/";e=e.replace(/\/\.(?=\w+($|\?))/,".");c.url=e.replace(/\/\\\./,"/.");s(g,function(a,
+e){r.urlParams[e]||(c.params=c.params||{},c.params[e]=a)})}};return t}])})(window,window.angular);
+//# sourceMappingURL=angular-resource.min.js.map
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-route.min.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-route.min.js
new file mode 100644
index 0000000..9e161e2
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/angular/angular-route.min.js
@@ -0,0 +1,14 @@
+/*
+ AngularJS v1.3.0-beta.5
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(n,e,A){'use strict';function x(s,g,k){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,c,b,f,w){function y(){p&&(p.remove(),p=null);h&&(h.$destroy(),h=null);l&&(k.leave(l,function(){p=null}),p=l,l=null)}function v(){var b=s.current&&s.current.locals;if(e.isDefined(b&&b.$template)){var b=a.$new(),d=s.current;l=w(b,function(d){k.enter(d,null,l||c,function(){!e.isDefined(t)||t&&!a.$eval(t)||g()});y()});h=d.scope=b;h.$emit("$viewContentLoaded");h.$eval(u)}else y()}
+var h,l,p,t=b.autoscroll,u=b.onload||"";a.$on("$routeChangeSuccess",v);v()}}}function z(e,g,k){return{restrict:"ECA",priority:-400,link:function(a,c){var b=k.current,f=b.locals;c.html(f.$template);var w=e(c.contents());b.controller&&(f.$scope=a,f=g(b.controller,f),b.controllerAs&&(a[b.controllerAs]=f),c.data("$ngControllerController",f),c.children().data("$ngControllerController",f));w(a)}}}n=e.module("ngRoute",["ng"]).provider("$route",function(){function s(a,c){return e.extend(new (e.extend(function(){},
+{prototype:a})),c)}function g(a,e){var b=e.caseInsensitiveMatch,f={originalPath:a,regexp:a},k=f.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,e,b,c){a="?"===c?c:null;c="*"===c?c:null;k.push({name:b,optional:!!a});e=e||"";return""+(a?"":e)+"(?:"+(a?e:"")+(c&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");f.regexp=RegExp("^"+a+"$",b?"i":"");return f}var k={};this.when=function(a,c){k[a]=e.extend({reloadOnSearch:!0},c,a&&g(a,c));if(a){var b=
+"/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";k[b]=e.extend({redirectTo:a},g(b,c))}return this};this.otherwise=function(a){this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$http","$templateCache","$sce",function(a,c,b,f,g,n,v,h){function l(){var d=p(),m=r.current;if(d&&m&&d.$$route===m.$$route&&e.equals(d.pathParams,m.pathParams)&&!d.reloadOnSearch&&!u)m.params=d.params,e.copy(m.params,b),a.$broadcast("$routeUpdate",m);else if(d||m)u=!1,a.$broadcast("$routeChangeStart",
+d,m),(r.current=d)&&d.redirectTo&&(e.isString(d.redirectTo)?c.path(t(d.redirectTo,d.params)).search(d.params).replace():c.url(d.redirectTo(d.pathParams,c.path(),c.search())).replace()),f.when(d).then(function(){if(d){var a=e.extend({},d.resolve),c,b;e.forEach(a,function(d,c){a[c]=e.isString(d)?g.get(d):g.invoke(d)});e.isDefined(c=d.template)?e.isFunction(c)&&(c=c(d.params)):e.isDefined(b=d.templateUrl)&&(e.isFunction(b)&&(b=b(d.params)),b=h.getTrustedResourceUrl(b),e.isDefined(b)&&(d.loadedTemplateUrl=
+b,c=n.get(b,{cache:v}).then(function(a){return a.data})));e.isDefined(c)&&(a.$template=c);return f.all(a)}}).then(function(c){d==r.current&&(d&&(d.locals=c,e.copy(d.params,b)),a.$broadcast("$routeChangeSuccess",d,m))},function(c){d==r.current&&a.$broadcast("$routeChangeError",d,m,c)})}function p(){var a,b;e.forEach(k,function(f,k){var q;if(q=!b){var g=c.path();q=f.keys;var l={};if(f.regexp)if(g=f.regexp.exec(g)){for(var h=1,p=g.length;h<p;++h){var n=q[h-1],r="string"==typeof g[h]?decodeURIComponent(g[h]):
+g[h];n&&r&&(l[n.name]=r)}q=l}else q=null;else q=null;q=a=q}q&&(b=s(f,{params:e.extend({},c.search(),a),pathParams:a}),b.$$route=f)});return b||k[null]&&s(k[null],{params:{},pathParams:{}})}function t(a,c){var b=[];e.forEach((a||"").split(":"),function(a,d){if(0===d)b.push(a);else{var e=a.match(/(\w+)(.*)/),f=e[1];b.push(c[f]);b.push(e[2]||"");delete c[f]}});return b.join("")}var u=!1,r={routes:k,reload:function(){u=!0;a.$evalAsync(l)}};a.$on("$locationChangeSuccess",l);return r}]});n.provider("$routeParams",
+function(){this.$get=function(){return{}}});n.directive("ngView",x);n.directive("ngView",z);x.$inject=["$route","$anchorScroll","$animate"];z.$inject=["$compile","$controller","$route"]})(window,window.angular);
+//# sourceMappingURL=angular-route.min.js.map
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/jwt-decode.min.js b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/jwt-decode.min.js
new file mode 100644
index 0000000..f56f967
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/lib/jwt-decode.min.js
@@ -0,0 +1 @@
+!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b){function c(a){return decodeURIComponent(atob(a).replace(/(.)/g,function(a,b){var c=b.charCodeAt(0).toString(16).toUpperCase();return c.length<2&&(c="0"+c),"%"+c}))}var d=a("Base64");b.exports=function(a){var b=a.replace(/-/g,"+").replace(/_/g,"/");switch(b.length%4){case 0:break;case 2:b+="==";break;case 3:b+="=";break;default:throw"Illegal base64url string!"}try{return c(b)}catch(e){return d.atob(b)}}},{Base64:4}],2:[function(a,b){"use strict";var c=a("./base64_url_decode"),d=a("./json_parse");b.exports=function(a){if(!a)throw new Error("Invalid token specified");return d(c(a.split(".")[1]))}},{"./base64_url_decode":1,"./json_parse":3}],3:[function(require,module,exports){module.exports=function(str){var parsed;return parsed="object"==typeof JSON?JSON.parse(str):eval("("+str+")")}},{}],4:[function(a,b,c){!function(){var a="undefined"!=typeof c?c:this,b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",d=function(){try{document.createElement("$")}catch(a){return a}}();a.btoa||(a.btoa=function(a){for(var c,e,f=0,g=b,h="";a.charAt(0|f)||(g="=",f%1);h+=g.charAt(63&c>>8-f%1*8)){if(e=a.charCodeAt(f+=.75),e>255)throw d;c=c<<8|e}return h}),a.atob||(a.atob=function(a){if(a=a.replace(/=+$/,""),a.length%4==1)throw d;for(var c,e,f=0,g=0,h="";e=a.charAt(g++);~e&&(c=f%4?64*c+e:e,f++%4)?h+=String.fromCharCode(255&c>>(-2*f&6)):0)e=b.indexOf(e);return h})}()},{}],5:[function(a){var b="undefined"!=typeof self?self:"undefined"!=typeof window?window:{},c=a("./lib/index");"function"==typeof b.window.define&&b.window.define.amd?b.window.define("jwt_decode",function(){return c}):b.window&&(b.window.jwt_decode=c)},{"./lib/index":2}]},{},[5]);
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html
new file mode 100644
index 0000000..da78224
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/admin/albums.html
@@ -0,0 +1,19 @@
+<h1>All Albums</h1>
+<table class="table" data-ng-repeat="(key, value) in albums">
+ <thead>
+ <tr>
+ <th>{{key}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>
+ <ul>
+ <li data-ng-repeat="p in value">
+ <a href="#/album/{{p.id}}">{{p.name}}</a> - [<a href="#" id="delete-{{p.name}}" ng-click="deleteAlbum(p)">X</a>]
+ </li>
+ </ul>
+ </td>
+ </tr>
+ </tbody>
+</table>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html
new file mode 100644
index 0000000..d9ddd25
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/create.html
@@ -0,0 +1,7 @@
+<h1>Create an Album</h1>
+
+<form>
+ Name: <input type="text" id="album.name" ng-model="album.name"/>
+
+ <button ng-click="create()" id="save-album">Save</button>
+</form>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/detail.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/detail.html
new file mode 100644
index 0000000..cf32df1
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/album/detail.html
@@ -0,0 +1 @@
+<h1>{{album.name}}</h1>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html
new file mode 100644
index 0000000..bd5853e
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/home.html
@@ -0,0 +1,22 @@
+<h2><span>Welcome To Photoz, {{Identity.claims.name}}</span> [<a href="" ng-click="Identity.logout()">Sign Out</a>]</h2>
+<div data-ng-show="Identity.isAdmin()"><b>Administration: </b> [<a href="#/admin/album" id="admin-albums">All Albums</a>]</div>
+<hr/>
+<br/>
+<div data-ng-show="!Identity.isAdmin()">
+<a href="#/album/create" id="create-album">Create Album</a> | <a href="#/profile">My Profile</a>
+<br/>
+<br/>
+<span data-ng-show="albums.length == 0" id="resource-list-empty">You don't have any albums, yet.</span>
+<table class="table" data-ng-show="albums.length > 0">
+ <thead>
+ <tr>
+ <th>Your Albums</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr data-ng-repeat="p in albums">
+ <td><a href="#/album/{{p.id}}">{{p.name}}</a> - [<a href="#" id="delete-{{p.name}}" ng-click="deleteAlbum(p)">X</a>]</td>
+ </tr>
+ </tbody>
+</table>
+</div>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/profile.html b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/profile.html
new file mode 100644
index 0000000..c6f6750
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/partials/profile.html
@@ -0,0 +1,6 @@
+<h1>My Profile</h1>
+
+<form>
+ <p>Name: {{profile.userName}}</p>
+ <p>Total of albums: {{profile.totalAlbums}}</p>
+</form>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..a370557
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-html5-client/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ version="3.0">
+
+ <module-name>photoz-html5-client</module-name>
+
+</web-app>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json
new file mode 100644
index 0000000..b3b2b81
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-realm.json
@@ -0,0 +1,110 @@
+{
+ "realm": "photoz",
+ "enabled": true,
+ "sslRequired": "external",
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [
+ "password"
+ ],
+ "users": [
+ {
+ "username": "alice",
+ "enabled": true,
+ "email": "alice@keycloak.org",
+ "firstName": "Alice",
+ "lastName": "In Chains",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "alice"
+ }
+ ],
+ "realmRoles": [
+ "user", "uma_authorization"
+ ]
+ },
+ {
+ "username": "jdoe",
+ "enabled": true,
+ "email": "jdoe@keycloak.org",
+ "firstName": "John",
+ "lastName": "Doe",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "jdoe"
+ }
+ ],
+ "realmRoles": [
+ "user", "uma_authorization"
+ ]
+ },
+ {
+ "username": "admin",
+ "enabled": true,
+ "email": "admin@admin.com",
+ "firstName": "Admin",
+ "lastName": "Istrator",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "admin"
+ }
+ ],
+ "realmRoles": [
+ "user", "admin", "uma_authorization"
+ ],
+ "clientRoles": {
+ "realm-management": [
+ "realm-admin"
+ ]
+ }
+ },
+ {
+ "username": "service-account-photoz-restful-api",
+ "enabled": true,
+ "email": "service-account-photoz-restful-api@placeholder.org",
+ "serviceAccountClientId": "photoz-restful-api",
+ "clientRoles": {
+ "photoz-restful-api" : ["uma_protection"]
+ }
+ }
+ ],
+ "roles": {
+ "realm": [
+ {
+ "name": "user",
+ "description": "User privileges"
+ },
+ {
+ "name": "admin",
+ "description": "Administrator privileges"
+ }
+ ]
+ },
+ "clients": [
+ {
+ "clientId": "photoz-html5-client",
+ "enabled": true,
+ "adminUrl": "/photoz-html5-client",
+ "baseUrl": "/photoz-html5-client",
+ "publicClient": true,
+ "redirectUris": [
+ "/photoz-html5-client/*"
+ ],
+ "webOrigins": ["*"]
+ },
+ {
+ "clientId": "photoz-restful-api",
+ "secret": "secret",
+ "enabled": true,
+ "baseUrl": "/photoz-restful-api",
+ "authorizationServicesEnabled" : true,
+ "redirectUris": [
+ "/photoz-restful-api/*"
+ ],
+ "webOrigins" : ["*"]
+ }
+ ]
+}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml
new file mode 100755
index 0000000..eb33fb5
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/pom.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-test-apps-photoz-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>photoz-restful-api</artifactId>
+ <packaging>war</packaging>
+
+ <name>Keycloak Authz Test: Photoz RESTful API</name>
+ <description>Photoz RESTful API</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.jboss.spec.javax.ws.rs</groupId>
+ <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.spec.javax.servlet</groupId>
+ <artifactId>jboss-servlet-api_3.0_spec</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>javax.persistence</groupId>
+ <artifactId>persistence-api</artifactId>
+ <version>1.0.2</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.spec.javax.ejb</groupId>
+ <artifactId>jboss-ejb-api_3.2_spec</artifactId>
+ <version>1.0.0.Final</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-client</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <finalName>${project.artifactId}</finalName>
+ <plugins>
+ <plugin>
+ <groupId>org.jboss.as.plugins</groupId>
+ <artifactId>jboss-as-maven-plugin</artifactId>
+ <configuration>
+ <skip>false</skip>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.wildfly.plugins</groupId>
+ <artifactId>wildfly-maven-plugin</artifactId>
+ <configuration>
+ <skip>false</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
new file mode 100644
index 0000000..b349e02
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
@@ -0,0 +1,62 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.example.photoz.admin;
+
+import org.keycloak.example.photoz.entity.Album;
+
+import javax.ejb.Stateless;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@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<String, List<Album>> albums = new HashMap<>();
+ List<Album> result = this.entityManager.createQuery("from Album").getResultList();
+
+ for (Album album : result) {
+ albums.computeIfAbsent(album.getUserId(), key -> new ArrayList<>()).add(album);
+ }
+
+ return Response.ok(albums).build();
+ }
+}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
new file mode 100644
index 0000000..388c9e4
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -0,0 +1,159 @@
+package org.keycloak.example.photoz.album;
+
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.representation.ScopeRepresentation;
+import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.example.photoz.ErrorResponse;
+import org.keycloak.example.photoz.entity.Album;
+import org.keycloak.representations.adapters.config.AdapterConfig;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ejb.Stateless;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.persistence.Query;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.security.Principal;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Path("/album")
+@Stateless
+public class AlbumService {
+
+ public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view";
+ public static final String SCOPE_ALBUM_CREATE = "urn:photoz.com:scopes:album:create";
+ public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete";
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @Context
+ private HttpServletRequest request;
+
+ private AuthzClient authzClient;
+
+ public AlbumService() {
+
+ }
+
+ @POST
+ @Consumes("application/json")
+ public Response create(Album newAlbum) {
+ Principal userPrincipal = request.getUserPrincipal();
+
+ newAlbum.setUserId(userPrincipal.getName());
+
+ Query queryDuplicatedAlbum = this.entityManager.createQuery("from Album where name = :name and userId = :userId");
+
+ queryDuplicatedAlbum.setParameter("name", newAlbum.getName());
+ queryDuplicatedAlbum.setParameter("userId", userPrincipal.getName());
+
+ if (!queryDuplicatedAlbum.getResultList().isEmpty()) {
+ throw new ErrorResponse("Name [" + newAlbum.getName() + "] already taken. Choose another one.", Status.CONFLICT);
+ }
+
+ this.entityManager.persist(newAlbum);
+
+ createProtectedResource(newAlbum);
+
+ return Response.ok(newAlbum).build();
+ }
+
+ @Path("{id}")
+ @DELETE
+ public Response delete(@PathParam("id") String id) {
+ Album album = this.entityManager.find(Album.class, Long.valueOf(id));
+
+ try {
+ deleteProtectedResource(album);
+ this.entityManager.remove(album);
+ } catch (Exception e) {
+ throw new RuntimeException("Could not delete album.", e);
+ }
+
+ return Response.ok().build();
+ }
+
+ @GET
+ @Produces("application/json")
+ public Response findAll() {
+ return Response.ok(this.entityManager.createQuery("from Album where userId = '" + request.getUserPrincipal().getName() + "'").getResultList()).build();
+ }
+
+ @GET
+ @Path("{id}")
+ @Produces("application/json")
+ public Response findById(@PathParam("id") String id) {
+ List result = this.entityManager.createQuery("from Album where id = " + id).getResultList();
+
+ if (result.isEmpty()) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(result.get(0)).build();
+ }
+
+ private void createProtectedResource(Album album) {
+ try {
+ HashSet<ScopeRepresentation> scopes = new HashSet<>();
+
+ scopes.add(new ScopeRepresentation(SCOPE_ALBUM_VIEW));
+ scopes.add(new ScopeRepresentation(SCOPE_ALBUM_CREATE));
+ scopes.add(new ScopeRepresentation(SCOPE_ALBUM_DELETE));
+
+ ResourceRepresentation albumResource = new ResourceRepresentation(album.getName(), scopes, "/album/" + album.getId(), "http://photoz.com/album");
+
+ albumResource.setOwner(album.getUserId());
+
+ getAuthzClient().protection().resource().create(albumResource);
+ } catch (Exception e) {
+ throw new RuntimeException("Could not register protected resource.", e);
+ }
+ }
+
+ private void deleteProtectedResource(Album album) {
+ String uri = "/album/" + album.getId();
+
+ try {
+ ProtectionResource protection = getAuthzClient().protection();
+ Set<String> search = protection.resource().findByFilter("uri=" + uri);
+
+ if (search.isEmpty()) {
+ throw new RuntimeException("Could not find protected resource with URI [" + uri + "]");
+ }
+
+ protection.resource().delete(search.iterator().next());
+ } catch (Exception e) {
+ throw new RuntimeException("Could not search protected resource.", e);
+ }
+ }
+
+ private AuthzClient getAuthzClient() {
+ if (this.authzClient == null) {
+ try {
+ AdapterConfig adapterConfig = JsonSerialization.readValue(this.request.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json"), AdapterConfig.class);
+ Configuration configuration = new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), null);
+
+ this.authzClient = AuthzClient.create(configuration);
+ } catch (Exception e) {
+ throw new RuntimeException("Could not create authorization client.", e);
+ }
+ }
+
+ return this.authzClient;
+ }
+}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
new file mode 100644
index 0000000..be638b6
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
@@ -0,0 +1,70 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.example.photoz.album;
+
+import javax.ejb.Stateless;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+import java.security.Principal;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@Path("/profile")
+@Stateless
+public class ProfileService {
+
+ private static final String PROFILE_VIEW = "urn:photoz.com:scopes:profile:view";
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @GET
+ @Produces("application/json")
+ public Response view(@Context HttpServletRequest request) {
+ Principal userPrincipal = request.getUserPrincipal();
+ List albums = this.entityManager.createQuery("from Album where userId = '" + userPrincipal.getName() + "'").getResultList();
+ return Response.ok(new Profile(userPrincipal.getName(), albums.size())).build();
+ }
+
+ public static class Profile {
+ private String userName;
+ private int totalAlbums;
+
+ public Profile(String name, int totalAlbums) {
+ this.userName = name;
+ this.totalAlbums = totalAlbums;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public int getTotalAlbums() {
+ return totalAlbums;
+ }
+ }
+}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
new file mode 100644
index 0000000..978bdea
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
@@ -0,0 +1,79 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.example.photoz.entity;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.OneToMany;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@Entity
+public class Album {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @Column(nullable = false)
+ private String name;
+
+ @OneToMany(mappedBy = "album", fetch = FetchType.EAGER)
+ private List<Photo> 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<Photo> getPhotos() {
+ return this.photos;
+ }
+
+ public void setPhotos(final List<Photo> photos) {
+ this.photos = photos;
+ }
+
+ public void setUserId(final String userId) {
+ this.userId = userId;
+ }
+
+ public String getUserId() {
+ return this.userId;
+ }
+}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java
new file mode 100644
index 0000000..08b7495
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java
@@ -0,0 +1,81 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.example.photoz.entity;
+
+import javax.persistence.Basic;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Lob;
+import javax.persistence.ManyToOne;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@Entity
+public class Photo {
+
+ @Id
+ @GeneratedValue
+ private Long id;
+
+ @Column
+ private String name;
+
+ @ManyToOne
+ private Album album;
+
+ @Lob
+ @Column
+ @Basic(fetch = FetchType.LAZY)
+ private byte[] image;
+
+ public Long getId() {
+ return this.id;
+ }
+
+ public void setId(final Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ public Album getAlbum() {
+ return this.album;
+ }
+
+ public void setAlbum(final Album album) {
+ this.album = album;
+ }
+
+ public byte[] getImage() {
+ return this.image;
+ }
+
+ public void setImage(final byte[] image) {
+ this.image = image;
+ }
+}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java
new file mode 100644
index 0000000..51755d8
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java
@@ -0,0 +1,32 @@
+package org.keycloak.example.photoz;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+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<String, String> errorResponse = new HashMap<>();
+
+ errorResponse.put("message", getMessage());
+
+ return Response.status(status).entity(errorResponse).build();
+ }
+}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java
new file mode 100644
index 0000000..5b8377c
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java
@@ -0,0 +1,12 @@
+package org.keycloak.example.photoz;
+
+import javax.ws.rs.ApplicationPath;
+import javax.ws.rs.core.Application;
+
+/**
+ * Basic auth app.
+ */
+@ApplicationPath("/")
+public class PhotozApplication extends Application {
+
+}
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml
new file mode 100644
index 0000000..957dc8a
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/beans.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ http://java.sun.com/xml/ns/javaee
+ http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
+
+</beans>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml
new file mode 100644
index 0000000..9323182
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/resources/META-INF/persistence.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<persistence version="2.0"
+ xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ http://java.sun.com/xml/ns/persistence
+ http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
+ <persistence-unit name="primary">
+ <non-jta-data-source>java:jboss/datasources/PhotozDS</non-jta-data-source>
+
+ <class>org.keycloak.example.photoz.entity.Album</class>
+ <class>org.keycloak.example.photoz.entity.Photo</class>
+
+ <properties>
+ <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/>
+ <property name="hibernate.hbm2ddl.auto" value="update" />
+ <property name="hibernate.show_sql" value="false" />
+ </properties>
+ </persistence-unit>
+</persistence>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml
new file mode 100644
index 0000000..4b23be6
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ 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.
+ ~
+ -->
+
+<jboss-deployment-structure>
+ <deployment>
+ <dependencies>
+ <module name="org.keycloak.keycloak-authz-client" services="import"/>
+ </dependencies>
+ </deployment>
+</jboss-deployment-structure>
+
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
new file mode 100644
index 0000000..95fb58b
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json
@@ -0,0 +1,45 @@
+{
+ "realm": "photoz",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "http://localhost:8080/auth",
+ "ssl-required": "external",
+ "resource": "photoz-restful-api",
+ "bearer-only" : true,
+ "credentials": {
+ "secret": "secret"
+ },
+ "policy-enforcer": {
+ "paths": [
+ {
+ "path" : "/album/*",
+ "methods" : [
+ {
+ "method": "GET",
+ "scopes" : ["urn:photoz.com:scopes:album:view"]
+ },
+ {
+ "method": "POST",
+ "scopes" : ["urn:photoz.com:scopes:album:create"]
+ }
+ ]
+ },
+ {
+ "name" : "Album Resource",
+ "path" : "/album/{id}",
+ "methods" : [
+ {
+ "method": "DELETE",
+ "scopes" : ["urn:photoz.com:scopes:album:delete"]
+ }
+ ]
+ },
+ {
+ "path" : "/profile"
+ },
+ {
+ "name" : "Admin Resources",
+ "path" : "/admin/*"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml
new file mode 100644
index 0000000..247448f
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml
@@ -0,0 +1,12 @@
+<datasources xmlns="http://www.jboss.org/ironjacamar/schema"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.jboss.org/ironjacamar/schema http://docs.jboss.org/ironjacamar/schema/datasources_1_0.xsd">
+ <datasource jndi-name="java:jboss/datasources/PhotozDS" pool-name="PhotozDS" enabled="true" use-java-context="true">
+ <connection-url>jdbc:h2:${jboss.server.data.dir}/kc-authz-photo;AUTO_SERVER=TRUE</connection-url>
+ <driver>h2</driver>
+ <security>
+ <user-name>sa</user-name>
+ <password>sa</password>
+ </security>
+ </datasource>
+</datasources>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..34cf6bd
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app xmlns="http://java.sun.com/xml/ns/javaee"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+ version="3.0">
+
+ <module-name>photoz-restful-api</module-name>
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>All Resources</web-resource-name>
+ <url-pattern>/*</url-pattern>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>user</role-name>
+ </auth-constraint>
+ </security-constraint>
+
+ <security-constraint>
+ <web-resource-collection>
+ <web-resource-name>All Resources</web-resource-name>
+ <url-pattern>/*</url-pattern>
+ </web-resource-collection>
+ <auth-constraint>
+ <role-name>admin</role-name>
+ </auth-constraint>
+ </security-constraint>
+
+ <login-config>
+ <auth-method>KEYCLOAK</auth-method>
+ <realm-name>photoz</realm-name>
+ </login-config>
+
+ <security-role>
+ <role-name>admin</role-name>
+ </security-role>
+
+ <security-role>
+ <role-name>user</role-name>
+ </security-role>
+</web-app>
diff --git a/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json
new file mode 100644
index 0000000..91c3e0e
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/photoz-restful-api-authz-service.json
@@ -0,0 +1,183 @@
+{
+ "allowRemoteResourceManagement": true,
+ "policyEnforcementMode": "ENFORCING",
+ "resources": [
+ {
+ "name": "User Profile Resource",
+ "uri": "/profile",
+ "type": "http://photoz.com/profile",
+ "scopes": [
+ {
+ "name": "urn:photoz.com:scopes:profile:view"
+ }
+ ]
+ },
+ {
+ "name": "Album Resource",
+ "uri": "/album/*",
+ "type": "http://photoz.com/album",
+ "scopes": [
+ {
+ "name": "urn:photoz.com:scopes:album:view"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:create"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:delete"
+ }
+ ]
+ },
+ {
+ "name": "Admin Resources",
+ "uri": "/admin/*",
+ "type": "http://photoz.com/admin",
+ "scopes": [
+ {
+ "name": "urn:photoz.com:scopes:album:admin:manage"
+ }
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "Only Owner Policy",
+ "description": "Defines that only the resource owner is allowed to do something",
+ "type": "drools",
+ "config": {
+ "mavenArtifactVersion": "2.0.0.CR1-SNAPSHOT",
+ "mavenArtifactId": "photoz-authz-policy",
+ "sessionName": "MainOwnerSession",
+ "mavenArtifactGroupId": "org.keycloak.testsuite",
+ "moduleName": "PhotozAuthzOwnerPolicy",
+ "scannerPeriod": "1",
+ "scannerPeriodUnit": "Hours"
+ }
+ },
+ {
+ "name": "Any Admin Policy",
+ "description": "Defines that adminsitrators can do something",
+ "type": "role",
+ "config": {
+ "roles": "[\"admin\"]"
+ }
+ },
+ {
+ "name": "Any User Policy",
+ "description": "Defines that any user can do something",
+ "type": "role",
+ "config": {
+ "roles": "[\"user\"]"
+ }
+ },
+ {
+ "name": "Only From a Specific Client Address",
+ "description": "Defines that only clients from a specific address can do something",
+ "type": "js",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "code": "var contextAttributes = $evaluation.getContext().getAttributes();\n\nif (contextAttributes.containsValue('kc.client.network.ip_address', '127.0.0.1')) {\n $evaluation.grant();\n}"
+ }
+ },
+ {
+ "name": "Administration Policy",
+ "description": "Defines that only administrators from a specific network address can do something.",
+ "type": "aggregate",
+ "config": {
+ "applyPolicies": "[\"Any Admin Policy\",\"Only From a Specific Client Address\"]"
+ }
+ },
+ {
+ "name": "Only Owner and Administrators Policy",
+ "description": "Defines that only the resource owner and administrators can do something",
+ "type": "aggregate",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "applyPolicies": "[\"Administration Policy\",\"Only Owner Policy\"]"
+ }
+ },
+ {
+ "name": "Only From @keycloak.org or Admin",
+ "description": "Defines that only users from @keycloak.org",
+ "type": "js",
+ "config": {
+ "applyPolicies": "[]",
+ "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}"
+ }
+ },
+ {
+ "name": "Only in the Period",
+ "description": "Access granted only during the morning",
+ "type": "time",
+ "config": {
+ "noa": "2016-01-03 23:59:59",
+ "expirationUnit": "Minutes",
+ "nbf": "2016-01-01 00:00:00",
+ "expirationTime": "1"
+ }
+ },
+ {
+ "name": "Album Resource Permission",
+ "description": "General policies that apply to all album resources.",
+ "type": "resource",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "defaultResourceType": "http://photoz.com/album",
+ "default": "true",
+ "applyPolicies": "[\"Any User Policy\",\"Administration Policy\"]"
+ }
+ },
+ {
+ "name": "Admin Resource Permission",
+ "description": "General policy for any administrative resource.",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "defaultResourceType": "http://photoz.com/admin",
+ "default": "true",
+ "applyPolicies": "[\"Administration Policy\"]"
+ }
+ },
+ {
+ "name": "View User Permission",
+ "description": "Defines who is allowed to view an user profile",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "applyPolicies": "[\"Only From @keycloak.org or Admin\"]",
+ "scopes": "[\"urn:photoz.com:scopes:profile:view\"]"
+ }
+ },
+ {
+ "name": "Delete Album Permission",
+ "description": "A policy that only allows the owner to delete his albums.",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "applyPolicies": "[\"Only Owner and Administrators Policy\"]",
+ "scopes": "[\"urn:photoz.com:scopes:album:delete\"]"
+ }
+ }
+ ],
+ "scopes": [
+ {
+ "name": "urn:photoz.com:scopes:profile:view"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:view"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:create"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:delete"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:admin:manage"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/photoz/pom.xml b/testsuite/integration-arquillian/test-apps/photoz/pom.xml
new file mode 100755
index 0000000..fdf7946
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/photoz/pom.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>integration-arquillian-test-apps</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>integration-arquillian-test-apps-photoz-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <name>Keycloak Authz: PhotoZ Test Parent</name>
+ <description>PhotoZ Test Application</description>
+
+ <modules>
+ <module>photoz-restful-api</module>
+ <module>photoz-html5-client</module>
+ <module>photoz-authz-policy</module>
+ </modules>
+</project>
diff --git a/testsuite/integration-arquillian/test-apps/pom.xml b/testsuite/integration-arquillian/test-apps/pom.xml
index 5f13130..21c9179 100644
--- a/testsuite/integration-arquillian/test-apps/pom.xml
+++ b/testsuite/integration-arquillian/test-apps/pom.xml
@@ -18,5 +18,7 @@
<module>js-console</module>
<module>test-apps-dist</module>
<module>js-database</module>
+ <module>photoz</module>
+ <module>hello-world-authz-service</module>
</modules>
</project>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml
index e48e088..9621ae5 100755
--- a/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml
+++ b/testsuite/integration-arquillian/test-apps/test-apps-dist/build.xml
@@ -27,5 +27,21 @@
<exclude name="**/subsystem-config.xml"/>
</fileset>
</copy>
+ <copy todir="target/test-apps/photoz" overwrite="true">
+ <fileset dir="../photoz">
+ <exclude name="**/target/**"/>
+ <exclude name="**/*.iml"/>
+ <exclude name="**/*.unconfigured"/>
+ <exclude name="**/subsystem-config.xml"/>
+ </fileset>
+ </copy>
+ <copy todir="target/test-apps/hello-world-authz-service" overwrite="true">
+ <fileset dir="../hello-world-authz-service">
+ <exclude name="**/target/**"/>
+ <exclude name="**/*.iml"/>
+ <exclude name="**/*.unconfigured"/>
+ <exclude name="**/subsystem-config.xml"/>
+ </fileset>
+ </copy>
</target>
</project>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
new file mode 100644
index 0000000..09bf6a9
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/PhotozClientAuthzTestApp.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.page;
+
+import org.jboss.arquillian.container.test.api.OperateOnDeployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.keycloak.testsuite.auth.page.login.OIDCLogin;
+import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
+import org.keycloak.testsuite.page.Form;
+import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import java.net.URL;
+
+import static org.keycloak.testsuite.util.WaitUtils.pause;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PhotozClientAuthzTestApp extends AbstractPageWithInjectedUrl {
+
+ public static final String DEPLOYMENT_NAME = "photoz-html5-client";
+
+ @ArquillianResource
+ @OperateOnDeployment(DEPLOYMENT_NAME)
+ private URL url;
+
+ @Page
+ protected OIDCLogin loginPage;
+
+ public void createAlbum(String name) {
+ this.driver.findElement(By.id("create-album")).click();
+ Form.setInputValue(this.driver.findElement(By.id("album.name")), name);
+ this.driver.findElement(By.id("save-album")).click();
+ pause(500);
+ }
+
+ @Override
+ public URL getInjectedUrl() {
+ return this.url;
+ }
+
+ public void deleteAlbum(String name) {
+ By id = By.id("delete-" + name);
+ WaitUtils.waitUntilElement(id);
+ this.driver.findElements(id).forEach(WebElement::click);
+ pause(500);
+ }
+
+ public void navigateToAdminAlbum() {
+ this.driver.navigate().to(this.getInjectedUrl().toString() + "/#/admin/album");
+ pause(500);
+ }
+
+ public void logOut() {
+ navigateTo();
+ By by = By.xpath("//a[text() = 'Sign Out']");
+ WaitUtils.waitUntilElement(by);
+ this.driver.findElement(by).click();
+ pause(500);
+ }
+
+ public void login(String username, String password) {
+ navigateTo();
+
+ if (this.driver.getCurrentUrl().startsWith(getInjectedUrl().toString())) {
+ logOut();
+ }
+
+ this.loginPage.form().login(username, password);
+ }
+
+ public boolean wasDenied() {
+ return this.driver.findElement(By.id("output")).getText().contains("You can not access");
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java
new file mode 100644
index 0000000..82b3ec4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractDefaultAuthzConfigAdapterTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import org.jboss.arquillian.container.test.api.Deployer;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractDefaultAuthzConfigAdapterTest extends AbstractExampleAdapterTest {
+
+ private static final String REALM_NAME = "hello-world-authz";
+ private static final String RESOURCE_SERVER_ID = "hello-world-authz-service";
+
+ @ArquillianResource
+ private Deployer deployer;
+
+ @Override
+ public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
+ testRealms.add(
+ loadRealm(new File(TEST_APPS_HOME_DIR + "/hello-world-authz-service/hello-world-authz-realm.json")));
+ }
+
+ @Deployment(name = RESOURCE_SERVER_ID, managed = false)
+ public static WebArchive deployment() throws IOException {
+ return exampleDeployment(RESOURCE_SERVER_ID);
+ }
+
+ @Test
+ public void testDefaultAuthzConfig() throws Exception {
+ configureAuthorizationServices();
+ deploy();
+ navigateToResourceServer();
+ login();
+
+ assertTrue(this.driver.getPageSource().contains("Your permissions are"));
+ assertTrue(this.driver.getPageSource().contains("Default Resource"));
+ }
+
+ private void login() {
+ this.loginPage.form().login("alice", "alice");
+ }
+
+ private void navigateToResourceServer() throws MalformedURLException {
+ this.driver.navigate().to(getResourceServerUrl());
+ }
+
+ private URL getResourceServerUrl() throws MalformedURLException {
+ return this.appServerContextRootPage.getUriBuilder().path(RESOURCE_SERVER_ID).build().toURL();
+ }
+
+ private void deploy() {
+ this.deployer.deploy(RESOURCE_SERVER_ID);
+ }
+
+ private void configureAuthorizationServices() {
+ ClientsResource clients = realmsResouce().realm(REALM_NAME).clients();
+ ClientRepresentation client = clients.findByClientId(RESOURCE_SERVER_ID).get(0);
+
+ client.setAuthorizationServicesEnabled(false);
+
+ // disables authorization services and remove authorization configuration from the client app
+ clients.get(client.getId()).update(client);
+
+ client.setAuthorizationServicesEnabled(true);
+
+ // enable authorization services in order to generate the default config and continue with tests
+ clients.get(client.getId()).update(client);
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
new file mode 100644
index 0000000..b2a0354
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractPhotozExampleAdapterTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example.authorization;
+
+import org.apache.commons.io.IOUtils;
+import org.jboss.arquillian.container.test.api.Deployer;
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.arquillian.test.api.ArquillianResource;
+import org.jboss.shrinkwrap.api.asset.StringAsset;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
+import org.keycloak.testsuite.adapter.AbstractExampleAdapterTest;
+import org.keycloak.testsuite.adapter.page.PhotozClientAuthzTestApp;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.util.IOUtil.loadJson;
+import static org.keycloak.testsuite.util.IOUtil.loadRealm;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractPhotozExampleAdapterTest extends AbstractExampleAdapterTest {
+
+ private static final String REALM_NAME = "photoz";
+ private static final String RESOURCE_SERVER_ID = "photoz-restful-api";
+ private static int TOKEN_LIFESPAN_LEEWAY = 3; // seconds
+
+ @ArquillianResource
+ private Deployer deployer;
+
+ @Page
+ private PhotozClientAuthzTestApp clientPage;
+
+ @Override
+ public void addAdapterTestRealms(List<RealmRepresentation> testRealms) {
+ RealmRepresentation realm = loadRealm(new File(TEST_APPS_HOME_DIR + "/photoz/photoz-realm.json"));
+
+ realm.setAccessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY); // seconds
+
+ testRealms.add(realm);
+ }
+
+ @Deployment(name = PhotozClientAuthzTestApp.DEPLOYMENT_NAME)
+ public static WebArchive deploymentClient() throws IOException {
+ return exampleDeployment(PhotozClientAuthzTestApp.DEPLOYMENT_NAME);
+ }
+
+ @Deployment(name = RESOURCE_SERVER_ID, managed = false)
+ public static WebArchive deploymentResourceServer() throws IOException {
+ return exampleDeployment(RESOURCE_SERVER_ID);
+ }
+
+ @Override
+ public void beforeAbstractKeycloakTest() throws Exception {
+ super.beforeAbstractKeycloakTest();
+ importResourceServerSettings();
+ }
+
+ @Test
+ public void testCreateDeleteAlbum() throws Exception {
+ try {
+ this.deployer.deploy(RESOURCE_SERVER_ID);
+
+ this.clientPage.login("alice", "alice");
+ this.clientPage.createAlbum("Alice Family Album");
+
+ List<ResourceRepresentation> resources = getAuthorizationResource().resources().resources();
+
+ assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty());
+
+ this.clientPage.deleteAlbum("Alice Family Album");
+
+ resources = getAuthorizationResource().resources().resources();
+
+ assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty());
+ } finally {
+ this.deployer.undeploy(RESOURCE_SERVER_ID);
+ }
+ }
+
+ @Test
+ public void testOnlyOwnerCanDeleteAlbum() throws Exception {
+ try {
+ this.deployer.deploy(RESOURCE_SERVER_ID);
+ this.clientPage.login("alice", "alice");
+ this.clientPage.createAlbum("Alice Family Album");
+ this.clientPage.login("admin", "admin");
+ this.clientPage.navigateToAdminAlbum();
+
+ List<ResourceRepresentation> resources = getAuthorizationResource().resources().resources();
+
+ assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty());
+
+ for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) {
+ if ("Delete Album Permission".equals(policy.getName())) {
+ policy.getConfig().put("applyPolicies", "[\"Only Owner Policy\"]");
+ getAuthorizationResource().policies().policy(policy.getId()).update(policy);
+ }
+ }
+
+ this.clientPage.login("admin", "admin");
+ this.clientPage.navigateToAdminAlbum();
+ this.clientPage.deleteAlbum("Alice Family Album");
+
+ resources = getAuthorizationResource().resources().resources();
+
+ assertFalse(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty());
+
+ for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) {
+ if ("Delete Album Permission".equals(policy.getName())) {
+ policy.getConfig().put("applyPolicies", "[\"Only Owner and Administrators Policy\"]");
+ getAuthorizationResource().policies().policy(policy.getId()).update(policy);
+ }
+ }
+
+ this.clientPage.login("admin", "admin");
+ this.clientPage.navigateToAdminAlbum();
+ this.clientPage.deleteAlbum("Alice Family Album");
+
+ resources = getAuthorizationResource().resources().resources();
+
+ assertTrue(resources.stream().filter(resource -> resource.getOwner().getName().equals("alice")).collect(Collectors.toList()).isEmpty());
+ } finally {
+ this.deployer.undeploy(RESOURCE_SERVER_ID);
+ }
+ }
+
+ @Test
+ public void testRegularUserCanNotAccessAdminResources() throws Exception {
+ try {
+ this.deployer.deploy(RESOURCE_SERVER_ID);
+ this.clientPage.login("alice", "alice");
+ this.clientPage.navigateToAdminAlbum();
+
+ assertTrue(this.clientPage.wasDenied());
+ } finally {
+ this.deployer.undeploy(RESOURCE_SERVER_ID);
+ }
+ }
+
+ @Test
+ public void testAdminOnlyFromSpecificAddress() throws Exception {
+ try {
+ this.deployer.deploy(RESOURCE_SERVER_ID);
+ this.clientPage.login("admin", "admin");
+ this.clientPage.navigateToAdminAlbum();
+
+ assertFalse(this.clientPage.wasDenied());
+
+ for (PolicyRepresentation policy : getAuthorizationResource().policies().policies()) {
+ if ("Only From a Specific Client Address".equals(policy.getName())) {
+ String code = policy.getConfig().get("code");
+ policy.getConfig().put("code", code.replaceAll("127.0.0.1", "127.3.3.3"));
+ getAuthorizationResource().policies().policy(policy.getId()).update(policy);
+ }
+ }
+
+ this.clientPage.login("admin", "admin");
+ this.clientPage.navigateToAdminAlbum();
+
+ assertTrue(this.clientPage.wasDenied());
+ } finally {
+ this.deployer.undeploy(RESOURCE_SERVER_ID);
+ }
+ }
+
+ private void importResourceServerSettings() throws FileNotFoundException {
+ getAuthorizationResource().importSettings(loadJson(new FileInputStream(new File(TEST_APPS_HOME_DIR + "/photoz/photoz-restful-api-authz-service.json")), ResourceServerRepresentation.class));
+ }
+
+ private AuthorizationResource getAuthorizationResource() throws FileNotFoundException {
+ ClientsResource clients = this.realmsResouce().realm(REALM_NAME).clients();
+ ClientRepresentation resourceServer = clients.findByClientId(RESOURCE_SERVER_ID).get(0);
+ return clients.get(resourceServer.getId()).authorization();
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java
new file mode 100644
index 0000000..c4979e0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AbstractAuthorizationTest.java
@@ -0,0 +1,93 @@
+/*
+ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ and other contributors as indicated by the @author tags.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ */
+package org.keycloak.testsuite.admin.client.authorization;
+
+import org.junit.After;
+import org.junit.Before;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ResourceScopeResource;
+import org.keycloak.admin.client.resource.ResourceScopesResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+import org.keycloak.testsuite.admin.client.AbstractClientTest;
+
+import javax.ws.rs.core.Response;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractAuthorizationTest extends AbstractClientTest {
+
+ protected static final String RESOURCE_SERVER_CLIENT_ID = "test-resource-server";
+
+ @Before
+ public void onBeforeAuthzTests() {
+ createOidcClient(RESOURCE_SERVER_CLIENT_ID);
+
+ ClientRepresentation resourceServer = getResourceServer();
+
+ assertEquals(RESOURCE_SERVER_CLIENT_ID, resourceServer.getName());
+ assertFalse(resourceServer.getAuthorizationServicesEnabled());
+ }
+
+ @After
+ public void onAfterAuthzTests() {
+ getClientResource().remove();
+ }
+
+ protected ClientResource getClientResource() {
+ return findClientResource(RESOURCE_SERVER_CLIENT_ID);
+ }
+
+ protected ClientRepresentation getResourceServer() {
+ return findClientRepresentation(RESOURCE_SERVER_CLIENT_ID);
+ }
+
+ protected void enableAuthorizationServices() {
+ ClientRepresentation resourceServer = getResourceServer();
+
+ resourceServer.setAuthorizationServicesEnabled(true);
+ resourceServer.setServiceAccountsEnabled(true);
+
+ getClientResource().update(resourceServer);
+ }
+
+ protected ResourceScopeResource createDefaultScope() {
+ return createScope("Test Scope", "Scope Icon");
+ }
+
+ protected ResourceScopeResource createScope(String name, String iconUri) {
+ ScopeRepresentation newScope = new ScopeRepresentation();
+
+ newScope.setName(name);
+ newScope.setIconUri(iconUri);
+
+ ResourceScopesResource resources = getClientResource().authorization().scopes();
+
+ Response response = resources.create(newScope);
+
+ assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+
+ ScopeRepresentation stored = response.readEntity(ScopeRepresentation.class);
+
+ return resources.scope(stored.getId());
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AuthorizationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AuthorizationTest.java
new file mode 100644
index 0000000..55936b2
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/AuthorizationTest.java
@@ -0,0 +1,58 @@
+/*
+ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ and other contributors as indicated by the @author tags.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ */
+
+package org.keycloak.testsuite.admin.client.authorization;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationTest extends AbstractAuthorizationTest {
+
+ @Test
+ public void testEnableAuthorizationServices() {
+ ClientResource clientResource = getClientResource();
+ ClientRepresentation resourceServer = getResourceServer();
+
+ enableAuthorizationServices();
+
+ ResourceServerRepresentation settings = clientResource.authorization().getSettings();
+
+ assertEquals(PolicyEnforcerConfig.EnforcementMode.ENFORCING.name(), settings.getPolicyEnforcementMode().name());
+ assertEquals(resourceServer.getId(), settings.getClientId());
+ List<ResourceRepresentation> defaultResources = clientResource.authorization().resources().resources();
+
+ assertEquals(1, defaultResources.size());
+
+ List<PolicyRepresentation> defaultPolicies = clientResource.authorization().policies().policies();
+
+ assertEquals(2, defaultPolicies.size());
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java
new file mode 100644
index 0000000..e6f83d8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GenericPolicyManagementTest.java
@@ -0,0 +1,290 @@
+/*
+ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ and other contributors as indicated by the @author tags.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ */
+package org.keycloak.testsuite.admin.client.authorization;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.PoliciesResource;
+import org.keycloak.admin.client.resource.PolicyResource;
+import org.keycloak.admin.client.resource.ResourceResource;
+import org.keycloak.admin.client.resource.ResourceScopeResource;
+import org.keycloak.admin.client.resource.ResourceScopesResource;
+import org.keycloak.admin.client.resource.ResourcesResource;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
+import javax.ws.rs.core.Response;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GenericPolicyManagementTest extends AbstractAuthorizationTest {
+
+ private static final String[] EXPECTED_BUILTIN_POLICY_PROVIDERS = {"test", "user", "role", "drools", "js", "time", "aggregate", "scope", "resource"};
+
+ @Before
+ @Override
+ public void onBeforeAuthzTests() {
+ super.onBeforeAuthzTests();
+ enableAuthorizationServices();
+ }
+
+ @After
+ @Override
+ public void onAfterAuthzTests() {
+ super.onAfterAuthzTests();
+ }
+
+ @Test
+ public void testCreate() {
+ PolicyRepresentation newPolicy = createTestingPolicy().toRepresentation();
+
+ assertEquals("Test Generic Policy", newPolicy.getName());
+ assertEquals("test", newPolicy.getType());
+ assertEquals(Logic.POSITIVE, newPolicy.getLogic());
+ assertEquals(DecisionStrategy.UNANIMOUS, newPolicy.getDecisionStrategy());
+ assertEquals("configuration for A", newPolicy.getConfig().get("configA"));
+ assertEquals("configuration for B", newPolicy.getConfig().get("configB"));
+ assertEquals("configuration for C", newPolicy.getConfig().get("configC"));
+
+ List<PolicyRepresentation> policies = getClientResource().authorization().policies().policies();
+
+ assertEquals(6, policies.size());
+
+ assertAssociatedPolicy("Test Associated A", newPolicy);
+ assertAssociatedPolicy("Test Associated B", newPolicy);
+ assertAssociatedPolicy("Test Associated C", newPolicy);
+
+ assertAssociatedResource("Test Resource A", newPolicy);
+ assertAssociatedResource("Test Resource B", newPolicy);
+ assertAssociatedResource("Test Resource C", newPolicy);
+
+ assertAssociatedScope("Test Scope A", newPolicy);
+ assertAssociatedScope("Test Scope B", newPolicy);
+ assertAssociatedScope("Test Scope C", newPolicy);
+ }
+
+ @Test
+ public void testUpdate() {
+ PolicyResource policyResource = createTestingPolicy();
+ PolicyRepresentation resource = policyResource.toRepresentation();
+
+ resource.setName("changed");
+ resource.setLogic(Logic.NEGATIVE);
+ resource.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+ resource.getConfig().put("configA", "changed configuration for A");
+ resource.getConfig().remove("configB");
+ resource.getConfig().put("configC", "changed configuration for C");
+
+ policyResource.update(resource);
+
+ resource = policyResource.toRepresentation();
+
+ assertEquals("changed", resource.getName());
+ assertEquals(Logic.NEGATIVE, resource.getLogic());
+
+ assertEquals(DecisionStrategy.AFFIRMATIVE, resource.getDecisionStrategy());
+ assertEquals("changed configuration for A", resource.getConfig().get("configA"));
+ assertNull(resource.getConfig().get("configB"));
+ assertEquals("changed configuration for C", resource.getConfig().get("configC"));
+
+ Map<String, String> config = resource.getConfig();
+
+ config.put("applyPolicies", buildConfigOption(findPolicyByName("Test Associated C").getId()));
+
+ config.put("resources", buildConfigOption(findResourceByName("Test Resource B").getId()));
+
+ config.put("scopes", buildConfigOption(findScopeByName("Test Scope A").getId()));
+
+ policyResource.update(resource);
+
+ resource = policyResource.toRepresentation();
+ config = resource.getConfig();
+
+ assertAssociatedPolicy("Test Associated C", resource);
+ assertFalse(config.get("applyPolicies").contains(findPolicyByName("Test Associated A").getId()));
+ assertFalse(config.get("applyPolicies").contains(findPolicyByName("Test Associated B").getId()));
+
+ assertAssociatedResource("Test Resource B", resource);
+ assertFalse(config.get("resources").contains(findResourceByName("Test Resource A").getId()));
+ assertFalse(config.get("resources").contains(findResourceByName("Test Resource C").getId()));
+
+ assertAssociatedScope("Test Scope A", resource);
+ assertFalse(config.get("scopes").contains(findScopeByName("Test Scope B").getId()));
+ assertFalse(config.get("scopes").contains(findScopeByName("Test Scope C").getId()));
+ }
+
+ @Test
+ public void testDefaultPolicyProviders() {
+ List<String> providers = getClientResource().authorization().policies()
+ .policyProviders().stream().map(PolicyProviderRepresentation::getType).collect(Collectors.toList());
+
+ assertFalse(providers.isEmpty());
+ assertTrue(providers.containsAll(Arrays.asList(EXPECTED_BUILTIN_POLICY_PROVIDERS)));
+ }
+
+ private PolicyResource createTestingPolicy() {
+ Map<String, String> config = new HashMap<>();
+
+ config.put("configA", "configuration for A");
+ config.put("configB", "configuration for B");
+ config.put("configC", "configuration for C");
+
+ config.put("applyPolicies", buildConfigOption(
+ createPolicy("Test Associated A", new HashMap<>()).toRepresentation().getId(),
+ createPolicy("Test Associated B", new HashMap<>()).toRepresentation().getId(),
+ createPolicy("Test Associated C", new HashMap<>()).toRepresentation().getId()
+ ));
+
+ config.put("resources", buildConfigOption(
+ createResource("Test Resource A").toRepresentation().getId(),
+ createResource("Test Resource B").toRepresentation().getId(),
+ createResource("Test Resource C").toRepresentation().getId()
+ ));
+
+ config.put("scopes", buildConfigOption(
+ createScope("Test Scope A").toRepresentation().getId(),
+ createScope("Test Scope B").toRepresentation().getId(),
+ createScope("Test Scope C").toRepresentation().getId()
+ ));
+
+ return createPolicy("Test Generic Policy", config);
+ }
+
+ private PolicyResource createPolicy(String name, Map<String, String> config) {
+ PolicyRepresentation newPolicy = new PolicyRepresentation();
+
+ newPolicy.setName(name);
+ newPolicy.setType("test");
+ newPolicy.setConfig(config);
+
+ PoliciesResource policies = getClientResource().authorization().policies();
+ Response response = policies.create(newPolicy);
+
+ assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+
+ PolicyRepresentation stored = response.readEntity(PolicyRepresentation.class);
+
+ return policies.policy(stored.getId());
+ }
+
+ private ResourceResource createResource(String name) {
+ ResourceRepresentation newResource = new ResourceRepresentation();
+
+ newResource.setName(name);
+
+ ResourcesResource resources = getClientResource().authorization().resources();
+
+ Response response = resources.create(newResource);
+
+ assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+
+ ResourceRepresentation stored = response.readEntity(ResourceRepresentation.class);
+
+ return resources.resource(stored.getId());
+ }
+
+ private ResourceScopeResource createScope(String name) {
+ ScopeRepresentation newScope = new ScopeRepresentation();
+
+ newScope.setName(name);
+
+ ResourceScopesResource scopes = getClientResource().authorization().scopes();
+
+ Response response = scopes.create(newScope);
+
+ assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+
+ ScopeRepresentation stored = response.readEntity(ScopeRepresentation.class);
+
+ return scopes.scope(stored.getId());
+ }
+
+ private String buildConfigOption(String... values) {
+ StringBuilder builder = new StringBuilder();
+
+ for (String value : values) {
+ if (builder.length() > 0) {
+ builder.append(",");
+ }
+ builder.append("\"" + value + "\"");
+ }
+
+ return builder.insert(0, "[").append("]").toString();
+ }
+
+ private PolicyRepresentation findPolicyByName(String name) {
+ return getClientResource().authorization().policies().policies()
+ .stream().filter(policyRepresentation -> policyRepresentation.getName().equals(name))
+ .findFirst().orElse(null);
+ }
+
+ private ResourceRepresentation findResourceByName(String name) {
+ return getClientResource().authorization().resources().resources()
+ .stream().filter(resource -> resource.getName().equals(name))
+ .findFirst().orElse(null);
+ }
+
+ private ScopeRepresentation findScopeByName(String name) {
+ return getClientResource().authorization().scopes().scopes()
+ .stream().filter(scope -> scope.getName().equals(name))
+ .findFirst().orElse(null);
+ }
+
+ private void assertAssociatedPolicy(String associatedPolicyName, PolicyRepresentation dependentPolicy) {
+ PolicyRepresentation associatedPolicy = findPolicyByName(associatedPolicyName);
+ assertNotNull(associatedPolicy);
+ assertTrue(dependentPolicy.getConfig().get("applyPolicies").contains(associatedPolicy.getId()));
+ assertEquals(1, associatedPolicy.getDependentPolicies().size());
+ assertEquals(dependentPolicy.getId(), associatedPolicy.getDependentPolicies().get(0).getId());
+ }
+
+ private void assertAssociatedResource(String resourceName, PolicyRepresentation policy) {
+ ResourceRepresentation resource = findResourceByName(resourceName);
+ assertNotNull(resource);
+ assertTrue(policy.getConfig().get("resources").contains(resource.getId()));
+ assertEquals(1, resource.getPolicies().size());
+ assertTrue(resource.getPolicies().stream().map(PolicyRepresentation::getId).collect(Collectors.toList())
+ .contains(policy.getId()));
+ }
+
+ private void assertAssociatedScope(String scopeName, PolicyRepresentation policy) {
+ ScopeRepresentation scope = findScopeByName(scopeName);
+ assertNotNull(scope);
+ assertTrue(policy.getConfig().get("scopes").contains(scope.getId()));
+ assertEquals(1, scope.getPolicies().size());
+ assertTrue(scope.getPolicies().stream().map(PolicyRepresentation::getId).collect(Collectors.toList())
+ .contains(policy.getId()));
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java
new file mode 100644
index 0000000..9907472
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java
@@ -0,0 +1,185 @@
+/*
+ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ and other contributors as indicated by the @author tags.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ */
+
+package org.keycloak.testsuite.admin.client.authorization;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.ResourceResource;
+import org.keycloak.admin.client.resource.ResourcesResource;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceManagementTest extends AbstractAuthorizationTest {
+
+ @Before
+ @Override
+ public void onBeforeAuthzTests() {
+ super.onBeforeAuthzTests();
+ enableAuthorizationServices();
+ }
+
+ @Test
+ public void testCreate() {
+ ResourceRepresentation newResource = createResource().toRepresentation();
+
+ assertEquals("Test Resource", newResource.getName());
+ assertEquals("/test/*", newResource.getUri());
+ assertEquals("test-resource", newResource.getType());
+ assertEquals("icon-test-resource", newResource.getIconUri());
+ }
+
+ @Test
+ public void testUpdate() {
+ ResourceResource resourceResource = createResource();
+ ResourceRepresentation resource = resourceResource.toRepresentation();
+
+ resource.setType("changed");
+ resource.setIconUri("changed");
+ resource.setUri("changed");
+
+ resourceResource.update(resource);
+
+ resource = resourceResource.toRepresentation();
+
+ assertEquals("changed", resource.getIconUri());
+ assertEquals("changed", resource.getType());
+ assertEquals("changed", resource.getUri());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testDelete() {
+ ResourceResource resourceResource = createResource();
+
+ resourceResource.remove();
+
+ resourceResource.toRepresentation();
+ }
+
+ @Test
+ public void testAssociateScopes() {
+ ResourceResource resourceResource = createResourceWithDefaultScopes();
+ ResourceRepresentation updated = resourceResource.toRepresentation();
+
+ assertEquals(3, updated.getScopes().size());
+
+ assertTrue(containsScope("Scope A", updated));
+ assertTrue(containsScope("Scope B", updated));
+ assertTrue(containsScope("Scope C", updated));
+ }
+
+ @Test
+ public void testUpdateScopes() {
+ ResourceResource resourceResource = createResourceWithDefaultScopes();
+ ResourceRepresentation resource = resourceResource.toRepresentation();
+ Set<ScopeRepresentation> scopes = new HashSet<>(resource.getScopes());
+
+ assertEquals(3, scopes.size());
+ assertTrue(scopes.removeIf(scopeRepresentation -> scopeRepresentation.getName().equals("Scope B")));
+
+ resource.setScopes(scopes);
+
+ resourceResource.update(resource);
+
+ ResourceRepresentation updated = resourceResource.toRepresentation();
+
+ assertEquals(2, resource.getScopes().size());
+
+ assertFalse(containsScope("Scope B", updated));
+ assertTrue(containsScope("Scope A", updated));
+ assertTrue(containsScope("Scope C", updated));
+
+ scopes = new HashSet<>(updated.getScopes());
+
+ assertTrue(scopes.removeIf(scopeRepresentation -> scopeRepresentation.getName().equals("Scope A")));
+ assertTrue(scopes.removeIf(scopeRepresentation -> scopeRepresentation.getName().equals("Scope C")));
+
+ updated.setScopes(scopes);
+
+ resourceResource.update(updated);
+
+ updated = resourceResource.toRepresentation();
+
+ assertEquals(0, updated.getScopes().size());
+ }
+
+ private ResourceResource createResourceWithDefaultScopes() {
+ ResourceResource resourceResource = createResource();
+ ResourceRepresentation resource = resourceResource.toRepresentation();
+
+ assertEquals(0, resource.getScopes().size());
+
+ HashSet<ScopeRepresentation> scopes = new HashSet<>();
+
+ scopes.add(createScope("Scope A", "").toRepresentation());
+ scopes.add(createScope("Scope B", "").toRepresentation());
+ scopes.add(createScope("Scope C", "").toRepresentation());
+
+ resource.setScopes(scopes);
+
+ resourceResource.update(resource);
+
+ return resourceResource;
+ }
+
+ private boolean containsScope(String scopeName, ResourceRepresentation resource) {
+ Set<ScopeRepresentation> scopes = resource.getScopes();
+
+ if (scopes != null) {
+ for (ScopeRepresentation scope : scopes) {
+ if (scope.getName().equals(scopeName)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private ResourceResource createResource() {
+ ResourceRepresentation newResource = new ResourceRepresentation();
+
+ newResource.setName("Test Resource");
+ newResource.setUri("/test/*");
+ newResource.setType("test-resource");
+ newResource.setIconUri("icon-test-resource");
+
+ ResourcesResource resources = getClientResource().authorization().resources();
+
+ Response response = resources.create(newResource);
+
+ assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
+
+ ResourceRepresentation stored = response.readEntity(ResourceRepresentation.class);
+
+ return resources.resource(stored.getId());
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ScopeManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ScopeManagementTest.java
new file mode 100644
index 0000000..bcb8b1b
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ScopeManagementTest.java
@@ -0,0 +1,75 @@
+/*
+ Copyright 2016 Red Hat, Inc. and/or its affiliates
+ and other contributors as indicated by the @author tags.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ */
+
+package org.keycloak.testsuite.admin.client.authorization;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.ResourceScopeResource;
+import org.keycloak.representations.idm.authorization.ScopeRepresentation;
+
+import javax.ws.rs.NotFoundException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScopeManagementTest extends AbstractAuthorizationTest {
+
+ @Before
+ @Override
+ public void onBeforeAuthzTests() {
+ super.onBeforeAuthzTests();
+ enableAuthorizationServices();
+ }
+
+ @Test
+ public void testCreate() {
+ ScopeRepresentation newScope = createDefaultScope().toRepresentation();
+
+ assertEquals("Test Scope", newScope.getName());
+ assertEquals("Scope Icon", newScope.getIconUri());
+ }
+
+ @Test
+ public void testUpdate() {
+ ResourceScopeResource scopeResource = createDefaultScope();
+ ScopeRepresentation scope = scopeResource.toRepresentation();
+
+ scope.setName("changed");
+ scope.setIconUri("changed");
+
+ scopeResource.update(scope);
+
+ scope = scopeResource.toRepresentation();
+
+ assertEquals("changed", scope.getName());
+ assertEquals("changed", scope.getIconUri());
+ }
+
+ @Test(expected = NotFoundException.class)
+ public void testDelete() {
+ ResourceScopeResource scopeResource = createDefaultScope();
+
+ scopeResource.remove();
+
+ scopeResource.toRepresentation();
+ }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java
new file mode 100644
index 0000000..712daa0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyDefaultAuthzConfigAdapterTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example;
+
+import org.keycloak.testsuite.adapter.example.authorization.AbstractDefaultAuthzConfigAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@AppServerContainer("app-server-wildfly")
+//@AdapterLibsLocationProperty("adapter.libs.wildfly")
+public class WildflyDefaultAuthzConfigAdapterTest extends AbstractDefaultAuthzConfigAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java
new file mode 100644
index 0000000..d9e2c34
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/src/test/java/org/keycloak/testsuite/adapter/example/WildflyPhotozExampleAdapterTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.adapter.example;
+
+import org.keycloak.testsuite.adapter.example.authorization.AbstractPhotozExampleAdapterTest;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+
+/**
+ *
+ * @author tkyjovsk
+ */
+@AppServerContainer("app-server-wildfly")
+//@AdapterLibsLocationProperty("adapter.libs.wildfly")
+public class WildflyPhotozExampleAdapterTest extends AbstractPhotozExampleAdapterTest {
+
+}
diff --git a/testsuite/integration-arquillian/tests/other/adapters/pom.xml b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
index b53c739..1038d28 100644
--- a/testsuite/integration-arquillian/tests/other/adapters/pom.xml
+++ b/testsuite/integration-arquillian/tests/other/adapters/pom.xml
@@ -284,6 +284,24 @@
<version>${project.version}</version>
<type>war</type>
</artifactItem>
+ <artifactItem>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>hello-world-authz-service</artifactId>
+ <version>${project.version}</version>
+ <type>war</type>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>photoz-html5-client</artifactId>
+ <version>${project.version}</version>
+ <type>war</type>
+ </artifactItem>
+ <artifactItem>
+ <groupId>org.keycloak.testsuite</groupId>
+ <artifactId>photoz-restful-api</artifactId>
+ <version>${project.version}</version>
+ <type>war</type>
+ </artifactItem>
</artifactItems>
<outputDirectory>${examples.home}</outputDirectory>
<overWriteIfNewer>true</overWriteIfNewer>
@@ -309,7 +327,7 @@
<artifactId>integration-arquillian-test-apps-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
- <includes>**/*realm.json,**/testsaml.json</includes>
+ <includes>**/*realm.json,**/*authz-service.json,**/testsaml.json</includes>
</artifactItem>
</artifactItems>
<outputDirectory>${examples.home}</outputDirectory>
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
index ecbe9d5..216a5a9 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -6,7 +6,7 @@ module.controller('ResourceServerCtrl', function($scope, realm, ResourceServer)
});
});
-module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $location, $upload, realm, ResourceServer, client, AuthzDialog, Notifications) {
+module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $location, $upload, $modal, realm, ResourceServer, client, AuthzDialog, Notifications) {
$scope.realm = realm;
$scope.client = client;
@@ -31,8 +31,7 @@ module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $l
}
$scope.reset = function() {
- $scope.server = angular.copy(data);
- $scope.changed = false;
+ $route.reload();
}
$scope.export = function() {
@@ -54,38 +53,29 @@ module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $l
delete $scope.settings
}
- $scope.onFileSelect = function($files) {
- $scope.files = $files;
+ $scope.onFileSelect = function($fileContent) {
+ $scope.server = angular.copy(JSON.parse($fileContent));
+ $scope.importing = true;
};
- $scope.clearFileSelect = function() {
- $scope.files = null;
- }
-
- $scope.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.");
- });
- }
+ $scope.viewImportDetails = function() {
+ $modal.open({
+ templateUrl: resourceUrl + '/partials/modal/view-object.html',
+ controller: 'ObjectModalCtrl',
+ resolve: {
+ object: function () {
+ return $scope.server;
+ }
+ }
+ })
};
+
+ $scope.import = function () {
+ ResourceServer.import({realm : realm.realm, client : client.id}, $scope.server, function() {
+ $route.reload();
+ Notifications.success("The resource server has been updated.");
+ });
+ }
});
});
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
index c74db28..e611430 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
@@ -4,6 +4,7 @@ module.factory('ResourceServer', function($resource) {
client: '@client'
}, {
'update' : {method : 'PUT'},
+ 'import' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/import', method : 'POST'},
'settings' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/settings', method : 'GET'}
});
});
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html
index 35d471d..fcd9f67 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html
@@ -3,7 +3,6 @@
<kc-tabs-resource-server></kc-tabs-resource-server>
<table class="table table-striped table-bordered">
- <caption class="hidden">Table of identity providers</caption>
<thead>
<tr>
<th class="kc-table-actions" colspan="5">
@@ -34,6 +33,7 @@
<th>Permission Name</th>
<th>Description</th>
<th>Type</th>
+ <th>Associated Policies</th>
</tr>
</thead>
<tbody>
@@ -41,6 +41,14 @@
<td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{policy.type}}/{{policy.id}}">{{policy.name}}</a></td>
<td>{{policy.description}}</td>
<td>{{policy.type}}</td>
+ <td>
+ <span data-ng-show="!policy.associatedPolicies.length">No policies assigned.</span>
+ <span data-ng-show="policy.associatedPolicies.length > 0">
+ <span ng-repeat="policy in policy.associatedPolicies">
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy/{{policy.type}}/{{policy.id}}">{{policy.name}}</a>{{$last ? '' : ', '}}
+ </span>
+ </span>
+ </td>
</tr>
<tr data-ng-show="(policies | filter:search).length == 0">
<td class="text-muted" colspan="3" data-ng-show="search.name">No results</td>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html
index 918cadf..2b3cd70 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html
@@ -7,22 +7,24 @@
<div class="form-group">
<label for="import-file" class="col-sm-2 control-label">Import</label>
<div class="col-md-6">
- <div class="controls kc-button-input-file" data-ng-show="!files || files.length == 0">
+ <div class="controls kc-button-input-file" data-ng-show="!importing">
<label for="import-file" class="btn btn-default">Select file <i class="pficon pficon-import"></i></label>
- <input id="import-file" type="file" class="hidden" ng-file-select="onFileSelect($files)">
+ <input id="import-file" type="file" class="hidden" kc-on-read-file="onFileSelect($fileContent)">
+ </div>
+ <div class="col-md-6" data-ng-show="importing">
+ <input type="button" class="btn btn-default" data-ng-click="viewImportDetails()" value="{{:: 'view-details' | translate}}"/>
</div>
- <span class="kc-uploaded-file" data-ng-show="files.length > 0">{{files[0].name}}</span>
</div>
<kc-tooltip>Import a JSON file containing all settings for this resource server.</kc-tooltip>
</div>
<div class="form-group">
- <div class="col-md-10 col-md-offset-2">
- <button type="submit" data-ng-disabled="files.length == 0" data-ng-click="uploadFile()" class="btn btn-primary">Upload</button>
- <button type="submit" data-ng-disabled="files.length == 0" data-ng-click="clearFileSelect()" class="btn btn-default" data-ng-show="files.length > 0">Cancel</button>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="importing">
+ <button class="btn btn-default" data-ng-click="import()" data-ng-disabled="!changed">Import</button>
+ <button kc-cancel data-ng-click="reset()">Cancel</button>
</div>
</div>
</fieldset>
- <fieldset class="border-top" data-ng-hide="files.length > 0">
+ <fieldset class="border-top" data-ng-hide="importing">
<div class="form-group">
<label class="col-md-2 control-label" for="server.policyEnforcementMode">Policy Enforcement Mode</label>
<div class="col-md-2">
@@ -54,7 +56,7 @@
</div>
</fieldset>
- <fieldset class="border-top" data-ng-show="!files || files.length == 0">
+ <fieldset class="border-top" data-ng-show="server.id">
<legend><span class="text">Export Settings</span>
<kc-tooltip>Here you can export all settings for this resource server.</kc-tooltip>
</legend>