keycloak-uncached
Changes
adapters/oidc/adapter-core/pom.xml 5(+5 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java 27(+26 -1)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java 243(+243 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java 80(+80 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java 119(+119 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java 103(+103 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java 205(+205 -0)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java 10(+9 -1)
adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java 5(+5 -0)
adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java 4(+2 -2)
authz/client/pom.xml 53(+53 -0)
authz/client/src/main/java/org/keycloak/authorization/client/AuthorizationDeniedException.java 29(+29 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java 48(+48 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java 42(+42 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java 34(+34 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementResponse.java 42(+42 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java 60(+60 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java 62(+62 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java 38(+38 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java 49(+49 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java 184(+184 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/ScopeRepresentation.java 98(+98 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java 235(+235 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java 43(+43 -0)
authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java 58(+58 -0)
authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java 54(+54 -0)
authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java 50(+50 -0)
authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java 91(+91 -0)
authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java 57(+57 -0)
authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java 50(+50 -0)
authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java 61(+61 -0)
authz/client/src/main/java/org/keycloak/authorization/client/util/HttpResponseException.java 46(+46 -0)
authz/client/src/main/java/org/keycloak/authorization/client/util/HttpResponseProcessor.java 26(+26 -0)
authz/policy/aggregate/pom.xml 33(+33 -0)
authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyAdminResource.java 69(+69 -0)
authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyProvider.java 75(+75 -0)
authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyProviderFactory.java 62(+62 -0)
authz/policy/aggregate/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 19(+19 -0)
authz/policy/drools/pom.xml 45(+45 -0)
authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicy.java 71(+71 -0)
authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyAdminResource.java 65(+65 -0)
authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProvider.java 43(+43 -0)
authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java 96(+96 -0)
authz/policy/drools/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 19(+19 -0)
authz/policy/javascript/pom.xml 33(+33 -0)
authz/policy/javascript/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java 57(+57 -0)
authz/policy/javascript/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java 62(+62 -0)
authz/policy/javascript/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 19(+19 -0)
authz/policy/pom.xml 31(+31 -0)
authz/policy/resource/pom.xml 33(+33 -0)
authz/policy/resource/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java 41(+41 -0)
authz/policy/resource/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java 62(+62 -0)
authz/policy/resource/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 19(+19 -0)
authz/policy/role/pom.xml 51(+51 -0)
authz/policy/role/src/main/java/org/keycloak/authorization/policy/provider/identity/RolePolicyProvider.java 75(+75 -0)
authz/policy/role/src/main/java/org/keycloak/authorization/policy/provider/identity/RolePolicyProviderFactory.java 132(+132 -0)
authz/policy/role/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 19(+19 -0)
authz/policy/scope/pom.xml 33(+33 -0)
authz/policy/scope/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java 43(+43 -0)
authz/policy/scope/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java 62(+62 -0)
authz/policy/scope/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 19(+19 -0)
authz/policy/time/pom.xml 32(+32 -0)
authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyAdminResource.java 64(+64 -0)
authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java 86(+86 -0)
authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java 62(+62 -0)
authz/policy/time/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 19(+19 -0)
authz/policy/user/pom.xml 51(+51 -0)
authz/policy/user/src/main/java/org/keycloak/authorization/policy/provider/identity/UserPolicyProvider.java 60(+60 -0)
authz/policy/user/src/main/java/org/keycloak/authorization/policy/provider/identity/UserPolicyProviderFactory.java 132(+132 -0)
authz/policy/user/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 19(+19 -0)
authz/pom.xml 30(+30 -0)
core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java 209(+209 -0)
dependencies/server-all/pom.xml 280(+280 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml 1(+1 -0)
distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml 42(+42 -0)
distribution/examples-dist/build.xml 8(+8 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json 4(+4 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/web.xml 1(+1 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml 3(+3 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/drools/main/module.xml 82(+82 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-policy-drools/main/module.xml 35(+35 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-policy-provider/main/module.xml 41(+41 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-server/main/module.xml 9(+9 -0)
examples/authz/hello-world/pom.xml 43(+43 -0)
examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java 162(+162 -0)
examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com.photoz.authz.policy.admin/Main.drl 14(+14 -0)
examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com.photoz.authz.policy.resource.owner/Main.drl 15(+15 -0)
examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com.photoz.authz.policy.user/Main.drl 14(+14 -0)
examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com/photoz/authz/policy/contextual/Main.drl 15(+15 -0)
examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/META-INF/kmodule.xml 21(+21 -0)
examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular.min.js 214(+214 -0)
examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular-resource.min.js 13(+13 -0)
examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular-route.min.js 14(+14 -0)
examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/admin/albums.html 19(+19 -0)
examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/create.html 7(+7 -0)
examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/detail.html 1(+1 -0)
examples/authz/photoz-uma/photoz-uma-realm.json 111(+111 -0)
examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java 62(+62 -0)
examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java 132(+132 -0)
examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java 70(+70 -0)
examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java 79(+79 -0)
examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java 81(+81 -0)
examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java 32(+32 -0)
examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java 12(+12 -0)
examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/persistence.xml 19(+19 -0)
examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml 26(+26 -0)
examples/authz/photoz-uma/pom.xml 24(+24 -0)
examples/authz/photoz-uma/README.md 99(+99 -0)
examples/authz/pom.xml 30(+30 -0)
examples/authz/servlet-authz/pom.xml 54(+54 -0)
examples/authz/servlet-authz/README.md 50(+50 -0)
examples/pom.xml 1(+1 -0)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java 4(+4 -0)
model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java 1(+1 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java 407(+407 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java 187(+187 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java 299(+299 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java 195(+195 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedPolicy.java 213(+213 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResource.java 131(+131 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResourceServer.java 73(+73 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedScope.java 78(+78 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java 109(+109 -0)
model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreProviderFactory.java 56(+56 -0)
model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.authorization.CachedStoreProviderFactory 19(+19 -0)
model/jpa/pom.xml 5(+5 -0)
model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceServerEntity.java 113(+113 -0)
model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAAuthorizationStoreFactory.java 56(+56 -0)
model/jpa/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory 19(+19 -0)
model/mongo/pom.xml 5(+5 -0)
model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java 106(+106 -0)
model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceServerAdapter.java 56(+56 -0)
model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceEntity.java 142(+142 -0)
model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceServerEntity.java 67(+67 -0)
model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java 53(+53 -0)
model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceServerStore.java 90(+90 -0)
model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java 141(+141 -0)
model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java 4(+4 -0)
model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java 39(+35 -4)
model/mongo/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory 37(+37 -0)
pom.xml 56(+56 -0)
server-spi/pom.xml 5(+5 -0)
server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java 53(+53 -0)
server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java 31(+31 -0)
server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java 43(+43 -0)
server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java 102(+102 -0)
server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java 105(+105 -0)
server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java 156(+156 -0)
server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/EvaluationContext.java 45(+45 -0)
server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java 38(+38 -0)
server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderAdminService.java 33(+33 -0)
server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java 39(+39 -0)
server-spi/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java 51(+51 -0)
server-spi/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java 50(+50 -0)
server-spi/src/main/java/org/keycloak/authorization/store/syncronization/Synchronizer.java 31(+31 -0)
server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactoryProvider.java 27(+27 -0)
server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactorySpi.java 48(+48 -0)
server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreProviderFactory.java 27(+27 -0)
server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProviderFactory.java 28(+28 -0)
services/pom.xml 2(+2 -0)
services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java 123(+123 -0)
services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java 223(+223 -0)
services/src/main/java/org/keycloak/authorization/admin/representation/PolicyProviderRepresentation.java 53(+53 -0)
services/src/main/java/org/keycloak/authorization/admin/representation/PolicyRepresentation.java 119(+119 -0)
services/src/main/java/org/keycloak/authorization/admin/representation/ResourceOwnerRepresentation.java 44(+44 -0)
services/src/main/java/org/keycloak/authorization/admin/representation/ResourceRepresentation.java 170(+170 -0)
services/src/main/java/org/keycloak/authorization/admin/representation/ResourceServerRepresentation.java 104(+104 -0)
services/src/main/java/org/keycloak/authorization/admin/representation/ScopeRepresentation.java 108(+108 -0)
services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java 236(+236 -0)
services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java 49(+49 -0)
services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java 43(+43 -0)
services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java 54(+54 -0)
services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java 73(+73 -0)
services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java 25(+25 -0)
services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java 42(+42 -0)
services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java 86(+86 -0)
services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProviderFactory.java 54(+54 -0)
services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java 102(+102 -0)
services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java 59(+59 -0)
services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java 46(+46 -0)
services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java 58(+58 -0)
services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java 67(+67 -0)
services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java 39(+39 -0)
services/src/main/java/org/keycloak/authorization/protection/resource/RegistrationResponse.java 50(+50 -0)
services/src/main/java/org/keycloak/authorization/protection/resource/representation/RegistrationResponse.java 50(+50 -0)
services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java 150(+150 -0)
services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaScopeRepresentation.java 98(+98 -0)
services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java 228(+228 -0)
services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProviderFactory.java 55(+55 -0)
services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenIntrospectionEndpoint.java 60(+14 -46)
services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java 26(+26 -0)
services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProviderFactory.java 38(+38 -0)
services/src/main/resources/META-INF/services/org.keycloak.authorization.AuthorizationProviderFactory 19(+19 -0)
services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.TokenIntrospectionProviderFactory 21(+21 -0)
services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory 3(+2 -1)
testsuite/integration/pom.xml 7(+7 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractAuthorizationTest.java 127(+127 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java 370(+370 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AttributeTest.java 73(+73 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/KeycloakAuthorizationServerRule.java 47(+47 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java 163(+163 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java 364(+364 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java 148(+148 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java 4(+2 -2)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java 11(+6 -5)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java 2(+1 -1)
testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json 4(+4 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-resource-detail.html 101(+101 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html 113(+113 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html 53(+53 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html 89(+89 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-drools-detail.html 128(+128 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-js-detail.html 71(+71 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-role-detail.html 115(+115 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-time-detail.html 81(+81 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-user-detail.html 96(+96 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html 283(+283 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html 68(+68 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html 53(+53 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html 82(+82 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-list.html 49(+49 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html 79(+79 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html 85(+85 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-detail.html 47(+47 -0)
themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html 41(+41 -0)
Details
adapters/oidc/adapter-core/pom.xml 5(+5 -0)
diff --git a/adapters/oidc/adapter-core/pom.xml b/adapters/oidc/adapter-core/pom.xml
index 015fbf4..ff3a205 100755
--- a/adapters/oidc/adapter-core/pom.xml
+++ b/adapters/oidc/adapter-core/pom.xml
@@ -66,6 +66,11 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-client</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<scope>provided</scope>
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java
index 18c3f4a..121adf1 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AuthenticatedActionsHandler.java
@@ -18,10 +18,12 @@
package org.keycloak.adapters;
import org.jboss.logging.Logger;
+import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.authorization.PolicyEnforcer;
+import org.keycloak.common.util.UriUtils;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.representations.AccessToken;
-import org.keycloak.common.util.UriUtils;
import java.io.IOException;
import java.util.Set;
@@ -55,6 +57,9 @@ public class AuthenticatedActionsHandler {
queryBearerToken();
return true;
}
+ if (!isAuthorized()) {
+ return true;
+ }
return false;
}
@@ -124,4 +129,24 @@ public class AuthenticatedActionsHandler {
}
return false;
}
+
+ private boolean isAuthorized() {
+ PolicyEnforcer policyEnforcer = this.deployment.getPolicyEnforcer();
+
+ if (policyEnforcer == null) {
+ log.debugv("Policy enforcement is disabled.");
+ return true;
+ }
+ try {
+ OIDCHttpFacade facade = (OIDCHttpFacade) this.facade;
+ AuthorizationContext authorizationContext = policyEnforcer.enforce(facade);
+ RefreshableKeycloakSecurityContext session = (RefreshableKeycloakSecurityContext) facade.getSecurityContext();
+
+ session.setAuthorizationContext(authorizationContext);
+
+ return authorizationContext.isGranted();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to enforce policy decisions.", e);
+ }
+ }
}
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
new file mode 100644
index 0000000..add1c83
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/AbstractPolicyEnforcer.java
@@ -0,0 +1,243 @@
+/*
+ * 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.adapters.authorization;
+
+import org.jboss.logging.Logger;
+import org.keycloak.AuthorizationContext;
+import org.keycloak.adapters.OIDCHttpFacade;
+import org.keycloak.adapters.spi.HttpFacade.Request;
+import org.keycloak.adapters.spi.HttpFacade.Response;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.resource.ProtectedResource;
+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 java.net.URI;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractPolicyEnforcer {
+
+ private static Logger LOGGER = Logger.getLogger(AbstractPolicyEnforcer.class);
+ private final PolicyEnforcerConfig enforcerConfig;
+ private final PolicyEnforcer policyEnforcer;
+
+ private List<PathConfig> paths;
+ private AuthzClient authzClient;
+ private PathMatcher pathMatcher;
+
+ public AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
+ this.policyEnforcer = policyEnforcer;
+ this.enforcerConfig = policyEnforcer.getEnforcerConfig();
+ this.authzClient = policyEnforcer.getClient();
+ this.pathMatcher = new PathMatcher();
+ this.paths = policyEnforcer.getPaths();
+ }
+
+ public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
+ EnforcementMode enforcementMode = this.enforcerConfig.getEnforcementMode();
+
+ if (EnforcementMode.DISABLED.equals(enforcementMode)) {
+ return createEmptyAuthorizationContext(true);
+ }
+
+ AccessToken accessToken = httpFacade.getSecurityContext().getToken();
+ Request request = httpFacade.getRequest();
+ Response response = httpFacade.getResponse();
+ String pathInfo = URI.create(request.getURI()).getPath().substring(1);
+ String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
+ PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
+
+ LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
+
+ if (pathConfig == null) {
+ if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
+ return createAuthorizationContext(accessToken);
+ }
+
+ LOGGER.debugf("Could not find a configuration for path [%s]", path);
+ response.sendError(403, "Could not find a configuration for path [" + path + "].");
+
+ return createEmptyAuthorizationContext(false);
+ }
+
+ PathConfig actualPathConfig = resolvePathConfig(pathConfig, request);
+ Set<String> requiredScopes = getRequiredScopes(actualPathConfig, request);
+
+ if (isAuthorized(actualPathConfig, requiredScopes, accessToken, httpFacade)) {
+ try {
+ return createAuthorizationContext(accessToken);
+ } catch (Exception e) {
+ throw new RuntimeException("Error processing path [" + actualPathConfig.getPath() + "].", e);
+ }
+ }
+
+ if (!challenge(actualPathConfig, requiredScopes, httpFacade)) {
+ LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
+ response.sendError(403, "Authorization failed.");
+ }
+
+ return createEmptyAuthorizationContext(false);
+ }
+
+ protected abstract boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade);
+
+ protected boolean isAuthorized(PathConfig actualPathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
+ Request request = httpFacade.getRequest();
+ PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
+ String accessDeniedPath = enforcerConfig.getAccessDeniedPath();
+
+ if (accessDeniedPath != null) {
+ if (request.getURI().contains(accessDeniedPath)) {
+ return true;
+ }
+ }
+
+ AccessToken.Authorization authorization = accessToken.getAuthorization();
+
+ if (authorization == null) {
+ return false;
+ }
+
+ List<Permission> permissions = authorization.getPermissions();
+
+ for (Permission permission : permissions) {
+ Set<String> allowedScopes = permission.getScopes();
+
+ if (permission.getResourceSetId() != null) {
+ if (permission.getResourceSetId().equals(actualPathConfig.getId())) {
+ if (((allowedScopes == null || allowedScopes.isEmpty()) && requiredScopes.isEmpty()) || allowedScopes.containsAll(requiredScopes)) {
+ LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
+ if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
+ this.paths.remove(actualPathConfig);
+ }
+ return true;
+ }
+ }
+ } else {
+ if ((allowedScopes.isEmpty() && requiredScopes.isEmpty()) || allowedScopes.containsAll(requiredScopes)) {
+ LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
+ return true;
+ }
+ }
+ }
+
+ LOGGER.debugf("Authorization FAILED for path [%s]. No enough permissions [%s].", actualPathConfig, permissions);
+
+ return false;
+ }
+
+ protected AuthzClient getAuthzClient() {
+ return this.authzClient;
+ }
+
+ protected PolicyEnforcerConfig getEnforcerConfig() {
+ return enforcerConfig;
+ }
+
+ protected PolicyEnforcer getPolicyEnforcer() {
+ return policyEnforcer;
+ }
+
+ private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
+ return new AuthorizationContext() {
+ @Override
+ public boolean hasPermission(String resourceName, String scopeName) {
+ return granted;
+ }
+
+ @Override
+ public boolean hasResourcePermission(String resourceName) {
+ return granted;
+ }
+
+ @Override
+ public boolean hasScopePermission(String scopeName) {
+ return granted;
+ }
+
+ @Override
+ public List<Permission> getPermissions() {
+ return Collections.EMPTY_LIST;
+ }
+
+ @Override
+ public boolean isGranted() {
+ return granted;
+ }
+ };
+ }
+
+ private PathConfig resolvePathConfig(PathConfig originalConfig, Request request) {
+ if (originalConfig.hasPattern()) {
+ String pathInfo = URI.create(request.getURI()).getPath().substring(1);
+ String path = pathInfo.substring(pathInfo.indexOf('/'), pathInfo.length());
+ ProtectedResource resource = this.authzClient.protection().resource();
+ Set<String> search = resource.findByFilter("uri=" + path);
+
+ if (!search.isEmpty()) {
+ // resource does exist on the server, cache it
+ ResourceRepresentation targetResource = resource.findById(search.iterator().next()).getResourceDescription();
+ PathConfig config = new PathConfig();
+
+ config.setId(targetResource.getId());
+ config.setName(targetResource.getName());
+ config.setType(targetResource.getType());
+ config.setPath(targetResource.getUri());
+ config.setScopes(originalConfig.getScopes());
+ config.setMethods(originalConfig.getMethods());
+ config.setInstance(true);
+
+ this.paths.add(config);
+
+ return config;
+ }
+ }
+
+ return originalConfig;
+ }
+
+ private Set<String> getRequiredScopes(PathConfig pathConfig, Request request) {
+ Set<String> requiredScopes = new HashSet<>();
+
+ requiredScopes.addAll(pathConfig.getScopes());
+
+ String method = request.getMethod();
+
+ for (PolicyEnforcerConfig.MethodConfig methodConfig : pathConfig.getMethods()) {
+ if (methodConfig.getMethod().equals(method)) {
+ requiredScopes.addAll(methodConfig.getScopes());
+ }
+ }
+
+ return requiredScopes;
+ }
+
+ private AuthorizationContext createAuthorizationContext(AccessToken accessToken) {
+ return new AuthorizationContext(accessToken, this.paths);
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java
new file mode 100644
index 0000000..efa2b2b
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/BearerTokenPolicyEnforcer.java
@@ -0,0 +1,80 @@
+/*
+ * 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.adapters.authorization;
+
+import org.jboss.logging.Logger;
+import org.keycloak.adapters.OIDCHttpFacade;
+import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.authorization.client.resource.PermissionResource;
+import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class BearerTokenPolicyEnforcer extends AbstractPolicyEnforcer {
+
+ private static Logger LOGGER = Logger.getLogger(BearerTokenPolicyEnforcer.class);
+
+ public BearerTokenPolicyEnforcer(PolicyEnforcer enforcer) {
+ super(enforcer);
+ }
+
+ @Override
+ protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
+ if (getEnforcerConfig().getUmaProtocolConfig() != null) {
+ challengeUmaAuthentication(pathConfig, requiredScopes, facade);
+ } else {
+ challengeEntitlementAuthentication(facade);
+ }
+ return true;
+ }
+
+ private void challengeEntitlementAuthentication(OIDCHttpFacade facade) {
+ HttpFacade.Response response = facade.getResponse();
+ AuthzClient authzClient = getAuthzClient();
+ String clientId = authzClient.getConfiguration().getClientId();
+ String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/entitlement";
+ response.setStatus(401);
+ response.setHeader("WWW-Authenticate", "KC_ETT realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\"");
+ }
+
+ private void challengeUmaAuthentication(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
+ HttpFacade.Response response = facade.getResponse();
+ AuthzClient authzClient = getAuthzClient();
+ String ticket = getPermissionTicket(pathConfig, requiredScopes, authzClient);
+ String clientId = authzClient.getConfiguration().getClientId();
+ String authorizationServerUri = authzClient.getServerConfiguration().getIssuer().toString() + "/authz/authorize";
+ response.setStatus(401);
+ response.setHeader("WWW-Authenticate", "UMA realm=\"" + clientId + "\",as_uri=\"" + authorizationServerUri + "\",ticket=\"" + ticket + "\"");
+ }
+
+ private String getPermissionTicket(PathConfig pathConfig, Set<String> requiredScopes, AuthzClient authzClient) {
+ ProtectionResource protection = authzClient.protection();
+ PermissionResource permission = protection.permission();
+ PermissionRequest permissionRequest = new PermissionRequest();
+ permissionRequest.setResourceSetId(pathConfig.getId());
+ permissionRequest.setScopes(requiredScopes);
+ return permission.forResource(permissionRequest).getTicket();
+ }
+}
\ No newline at end of file
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
new file mode 100644
index 0000000..e151f76
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java
@@ -0,0 +1,119 @@
+/*
+ * 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.adapters.authorization;
+
+import org.jboss.logging.Logger;
+import org.keycloak.RSATokenVerifier;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.OIDCHttpFacade;
+import org.keycloak.adapters.spi.HttpFacade;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.representation.EntitlementResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.authorization.client.representation.PermissionResponse;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
+
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer {
+
+ private static Logger LOGGER = Logger.getLogger(KeycloakAdapterPolicyEnforcer.class);
+
+ public KeycloakAdapterPolicyEnforcer(PolicyEnforcer policyEnforcer) {
+ super(policyEnforcer);
+ }
+
+ @Override
+ protected boolean isAuthorized(PathConfig pathConfig, Set<String> requiredScopes, AccessToken accessToken, OIDCHttpFacade httpFacade) {
+ int retry = 2;
+ AccessToken original = accessToken;
+
+ while (retry >= 0) {
+ if (super.isAuthorized(pathConfig, requiredScopes, accessToken, httpFacade)) {
+ original.setAuthorization(accessToken.getAuthorization());
+ return true;
+ }
+
+ accessToken = requestAuthorizationToken(pathConfig, requiredScopes, httpFacade);
+
+ if (accessToken == null) {
+ return false;
+ }
+
+ retry--;
+ }
+
+ return false;
+ }
+
+ @Override
+ protected boolean challenge(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade facade) {
+ String accessDeniedPath = getEnforcerConfig().getAccessDeniedPath();
+ HttpFacade.Response response = facade.getResponse();
+
+ if (accessDeniedPath != null) {
+ response.setStatus(302);
+ response.setHeader("Location", accessDeniedPath);
+ } else {
+ response.sendError(403);
+ }
+
+ return true;
+ }
+
+ private AccessToken requestAuthorizationToken(PathConfig pathConfig, Set<String> requiredScopes, OIDCHttpFacade httpFacade) {
+ try {
+ String accessToken = httpFacade.getSecurityContext().getTokenString();
+ AuthzClient authzClient = getAuthzClient();
+ KeycloakDeployment deployment = getPolicyEnforcer().getDeployment();
+
+ if (getEnforcerConfig().getUmaProtocolConfig() != null) {
+ LOGGER.debug("Obtaining authorization for authenticated user.");
+ PermissionRequest permissionRequest = new PermissionRequest();
+
+ permissionRequest.setResourceSetId(pathConfig.getId());
+ permissionRequest.setScopes(requiredScopes);
+
+ PermissionResponse permissionResponse = authzClient.protection().permission().forResource(permissionRequest);
+ AuthorizationRequest authzRequest = new AuthorizationRequest(permissionResponse.getTicket());
+ AuthorizationResponse authzResponse = authzClient.authorization(accessToken).authorize(authzRequest);
+
+ if (authzResponse != null) {
+ return RSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment.getRealmKey(), deployment.getRealmInfoUrl());
+ }
+
+ return null;
+ } else {
+ LOGGER.debug("Obtaining entitlements for authenticated user.");
+ EntitlementResponse authzResponse = authzClient.entitlement(accessToken).getAll(authzClient.getConfiguration().getClientId());
+ return RSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment.getRealmKey(), deployment.getRealmInfoUrl());
+ }
+ } catch (AuthorizationDeniedException e) {
+ return null;
+ } catch (Exception e) {
+ throw new RuntimeException("Unexpected error during authorization request.", e);
+ }
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
new file mode 100644
index 0000000..5ac1b79
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PathMatcher.java
@@ -0,0 +1,103 @@
+/*
+ * 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.adapters.authorization;
+
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+class PathMatcher {
+
+ private static final String ANY_RESOURCE_PATTERN = "/*";
+
+ PathConfig matches(final String requestedUri, List<PathConfig> paths) {
+ PathConfig actualConfig = null;
+
+ for (PathConfig entry : paths) {
+ String protectedUri = entry.getPath();
+ String selectedUri = null;
+
+ if (protectedUri.equals(ANY_RESOURCE_PATTERN) && actualConfig == null) {
+ selectedUri = protectedUri;
+ }
+
+ int suffixIndex = protectedUri.indexOf(ANY_RESOURCE_PATTERN + ".");
+
+ if (suffixIndex != -1) {
+ String protectedSuffix = protectedUri.substring(suffixIndex + ANY_RESOURCE_PATTERN.length());
+
+ if (requestedUri.endsWith(protectedSuffix)) {
+ selectedUri = protectedUri;
+ }
+ }
+
+ if (protectedUri.equals(requestedUri)) {
+ selectedUri = protectedUri;
+ }
+
+ if (protectedUri.endsWith(ANY_RESOURCE_PATTERN)) {
+ String formattedPattern = removeWildCardsFromUri(protectedUri);
+
+ if (!formattedPattern.equals("/") && requestedUri.startsWith(formattedPattern)) {
+ selectedUri = protectedUri;
+ }
+
+ if (!formattedPattern.equals("/") && formattedPattern.endsWith("/") && formattedPattern.substring(0, formattedPattern.length() - 1).equals(requestedUri)) {
+ selectedUri = protectedUri;
+ }
+ }
+
+ int startRegex = protectedUri.indexOf('{');
+
+ if (startRegex != -1) {
+ String prefix = protectedUri.substring(0, startRegex);
+
+ if (requestedUri.startsWith(prefix)) {
+ selectedUri = protectedUri;
+ }
+ }
+
+ if (selectedUri != null) {
+ selectedUri = protectedUri;
+ }
+
+ if (selectedUri != null) {
+ if (actualConfig == null) {
+ actualConfig = entry;
+ } else {
+ if (actualConfig.equals(ANY_RESOURCE_PATTERN)) {
+ actualConfig = entry;
+ }
+
+ if (protectedUri.startsWith(removeWildCardsFromUri(actualConfig.getPath()))) {
+ actualConfig = entry;
+ }
+ }
+ }
+ }
+
+ return actualConfig;
+ }
+
+ private String removeWildCardsFromUri(String protectedUri) {
+ return protectedUri.replaceAll("/[*]", "/");
+ }
+}
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
new file mode 100644
index 0000000..d413327
--- /dev/null
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -0,0 +1,205 @@
+/*
+ * 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.adapters.authorization;
+
+import org.jboss.logging.Logger;
+import org.keycloak.AuthorizationContext;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.OIDCHttpFacade;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.RegistrationResponse;
+import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.representation.ScopeRepresentation;
+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 java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyEnforcer {
+
+ private static Logger LOGGER = Logger.getLogger(PolicyEnforcer.class);
+
+ private final KeycloakDeployment deployment;
+ private final PathMatcher pathMatcher;
+ private final AuthzClient authzClient;
+ private final PolicyEnforcerConfig enforcerConfig;
+ private final List<PathConfig> paths;
+
+ public PolicyEnforcer(KeycloakDeployment deployment, AdapterConfig adapterConfig) {
+ this.deployment = deployment;
+ this.enforcerConfig = adapterConfig.getPolicyEnforcerConfig();
+ this.authzClient = AuthzClient.create(new Configuration(adapterConfig.getAuthServerUrl(), adapterConfig.getRealm(), adapterConfig.getResource(), adapterConfig.getCredentials(), deployment.getClient()));
+ this.pathMatcher = new PathMatcher();
+ this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Initialization complete. Path configurations:");
+ for (PathConfig pathConfig : this.paths) {
+ LOGGER.debug(pathConfig);
+ }
+ }
+ }
+
+ public AuthorizationContext enforce(OIDCHttpFacade facade) {
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugv("Policy enforcement is enable. Enforcing policy decisions for path [{0}].", facade.getRequest().getURI());
+ }
+
+ AuthorizationContext context;
+
+ if (deployment.isBearerOnly()) {
+ context = new BearerTokenPolicyEnforcer(this).authorize(facade);
+ } else {
+ context = new KeycloakAdapterPolicyEnforcer(this).authorize(facade);
+ }
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debugv("Policy enforcement result for path [{0}] is : {1}", facade.getRequest().getURI(), context.isGranted() ? "GRANTED" : "DENIED");
+ LOGGER.debugv("Returning authorization context with permissions:");
+ for (Permission permission : context.getPermissions()) {
+ LOGGER.debug(permission);
+ }
+ }
+
+ return context;
+ }
+
+ PolicyEnforcerConfig getEnforcerConfig() {
+ return enforcerConfig;
+ }
+
+ AuthzClient getClient() {
+ return authzClient;
+ }
+
+ List<PathConfig> getPaths() {
+ return paths;
+ }
+
+ KeycloakDeployment getDeployment() {
+ return deployment;
+ }
+
+ private List<PathConfig> configurePaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
+ if (enforcerConfig.getPaths().isEmpty()) {
+ LOGGER.info("No path provided in configuration.");
+ return configureAllPathsForResourceServer(protectedResource);
+ } else {
+ LOGGER.info("Paths provided in configuration.");
+ return configureDefinedPaths(protectedResource, enforcerConfig);
+ }
+ }
+
+ private List<PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
+ List<PathConfig> paths = new ArrayList<>();
+
+ for (PathConfig pathConfig : enforcerConfig.getPaths()) {
+ Set<String> search;
+ String resourceName = pathConfig.getName();
+ String path = pathConfig.getPath();
+
+ if (resourceName != null) {
+ LOGGER.debugf("Trying to find resource with name [%s] for path [%s].", resourceName, path);
+ search = protectedResource.findByFilter("name=" + resourceName);
+ } else {
+ LOGGER.debugf("Trying to find resource with uri [%s] for path [%s].", path, path);
+ search = protectedResource.findByFilter("uri=" + path);
+ }
+
+ if (search.isEmpty()) {
+ if (enforcerConfig.isCreateResources()) {
+ LOGGER.debugf("Creating resource on server for path [%s].", pathConfig);
+ ResourceRepresentation resource = new ResourceRepresentation();
+
+ resource.setName(resourceName);
+ resource.setType(pathConfig.getType());
+ resource.setUri(path);
+
+ HashSet<ScopeRepresentation> scopes = new HashSet<>();
+
+ for (String scopeName : pathConfig.getScopes()) {
+ ScopeRepresentation scope = new ScopeRepresentation();
+
+ scope.setName(scopeName);
+
+ scopes.add(scope);
+ }
+
+ resource.setScopes(scopes);
+
+ RegistrationResponse registrationResponse = protectedResource.create(resource);
+
+ pathConfig.setId(registrationResponse.getId());
+ } else {
+ throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + ". Make sure you have created a resource on the server that matches with the path configuration.");
+ }
+ } else {
+ pathConfig.setId(search.iterator().next());
+ }
+
+ paths.add(pathConfig);
+ }
+
+ return paths;
+ }
+
+ private List<PathConfig> configureAllPathsForResourceServer(ProtectedResource protectedResource) {
+ LOGGER.info("Querying the server for all resources associated with this application.");
+ List<PathConfig> paths = new ArrayList<>();
+
+ for (String id : protectedResource.findAll()) {
+ RegistrationResponse response = protectedResource.findById(id);
+ ResourceRepresentation resourceDescription = response.getResourceDescription();
+
+ if (resourceDescription.getUri() != null) {
+ paths.add(createPathConfig(resourceDescription));
+ }
+ }
+
+ return paths;
+ }
+
+ private PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
+ PathConfig pathConfig = new PathConfig();
+
+ pathConfig.setId(resourceDescription.getId());
+ pathConfig.setName(resourceDescription.getName());
+ pathConfig.setPath(resourceDescription.getUri());
+
+ List<String> scopeNames = new ArrayList<>();
+
+ for (ScopeRepresentation scope : resourceDescription.getScopes()) {
+ scopeNames.add(scope.getName());
+ }
+
+ pathConfig.setScopes(scopeNames);
+ pathConfig.setType(resourceDescription.getType());
+
+ return pathConfig;
+ }
+}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index f95e3f8..901b3ea 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -20,6 +20,7 @@ package org.keycloak.adapters;
import org.apache.http.client.HttpClient;
import org.jboss.logging.Logger;
import org.keycloak.adapters.authentication.ClientCredentialsProvider;
+import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.constants.ServiceUrlConstants;
import org.keycloak.common.enums.RelativeUrlsUsed;
import org.keycloak.common.enums.SslRequired;
@@ -78,6 +79,7 @@ public class KeycloakDeployment {
protected volatile int notBefore;
protected int tokenMinimumTimeToLive;
+ private PolicyEnforcer policyEnforcer;
public KeycloakDeployment() {
}
@@ -366,4 +368,12 @@ public class KeycloakDeployment {
public void setTokenMinimumTimeToLive(final int tokenMinimumTimeToLive) {
this.tokenMinimumTimeToLive = tokenMinimumTimeToLive;
}
+
+ public void setPolicyEnforcer(PolicyEnforcer policyEnforcer) {
+ this.policyEnforcer = policyEnforcer;
+ }
+
+ public PolicyEnforcer getPolicyEnforcer() {
+ return policyEnforcer;
+ }
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index 89f2a75..5d54df9 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -21,10 +21,12 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.logging.Logger;
import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
+import org.keycloak.adapters.authorization.PolicyEnforcer;
import org.keycloak.common.enums.SslRequired;
+import org.keycloak.common.util.PemUtils;
import org.keycloak.enums.TokenStore;
import org.keycloak.representations.adapters.config.AdapterConfig;
-import org.keycloak.common.util.PemUtils;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.util.SystemPropertiesJsonParserFactory;
import java.io.IOException;
@@ -110,6 +112,12 @@ public class KeycloakDeploymentBuilder {
deployment.setTurnOffChangeSessionIdOnLogin(adapterConfig.getTurnOffChangeSessionIdOnLogin());
}
+ PolicyEnforcerConfig policyEnforcerConfig = adapterConfig.getPolicyEnforcerConfig();
+
+ if (policyEnforcerConfig != null) {
+ deployment.setPolicyEnforcer(new PolicyEnforcer(deployment, adapterConfig));
+ }
+
log.debug("Use authServerUrl: " + deployment.getAuthServerBaseUrl() + ", tokenUrl: " + deployment.getTokenUrl() + ", relativeUrls: " + deployment.getRelativeUrls());
return deployment;
}
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
index cdebcd6..7cfc7a6 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java
@@ -18,6 +18,7 @@
package org.keycloak.adapters;
import org.jboss.logging.Logger;
+import org.keycloak.AuthorizationContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.RSATokenVerifier;
import org.keycloak.common.VerificationException;
@@ -157,4 +158,8 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
tokenStore.refreshCallback(this);
return true;
}
+
+ public void setAuthorizationContext(AuthorizationContext authorizationContext) {
+ this.authorizationContext = authorizationContext;
+ }
}
diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java
index 3dc705f..27ddae8 100755
--- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java
+++ b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/OIDCServletUndertowHttpFacade.java
@@ -17,16 +17,16 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
-import io.undertow.util.AttachmentKey;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.OIDCHttpFacade;
+import static org.keycloak.adapters.undertow.OIDCUndertowHttpFacade.KEYCLOAK_SECURITY_CONTEXT_KEY;
+
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OIDCServletUndertowHttpFacade extends ServletHttpFacade implements OIDCHttpFacade {
- public static final AttachmentKey<KeycloakSecurityContext> KEYCLOAK_SECURITY_CONTEXT_KEY = AttachmentKey.create(KeycloakSecurityContext.class);
public OIDCServletUndertowHttpFacade(HttpServerExchange exchange) {
super(exchange);
authz/client/pom.xml 53(+53 -0)
diff --git a/authz/client/pom.xml b/authz/client/pom.xml
new file mode 100644
index 0000000..2007904
--- /dev/null
+++ b/authz/client/pom.xml
@@ -0,0 +1,53 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-client</artifactId>
+ <packaging>jar</packaging>
+
+ <name>KeyCloak Authz: Client API</name>
+ <description>KeyCloak AuthZ: Client API</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ <version>${jboss.logging.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/AuthorizationDeniedException.java b/authz/client/src/main/java/org/keycloak/authorization/client/AuthorizationDeniedException.java
new file mode 100644
index 0000000..ffab2b5
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/AuthorizationDeniedException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.client;
+
+import org.keycloak.authorization.client.util.HttpResponseException;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationDeniedException extends RuntimeException {
+ public AuthorizationDeniedException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
new file mode 100644
index 0000000..0221af6
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/AuthzClient.java
@@ -0,0 +1,131 @@
+/*
+ * 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.authorization.client;
+
+import org.keycloak.authorization.client.representation.ServerConfiguration;
+import org.keycloak.authorization.client.resource.AuthorizationResource;
+import org.keycloak.authorization.client.resource.EntitlementResource;
+import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.authorization.client.util.Http;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+
+/**
+ * <p>This is class serves as an entry point for clients looking for access to Keycloak Authorization Services.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthzClient {
+
+ private final Http http;
+
+ public static AuthzClient create() {
+ InputStream configStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("keycloak.json");
+
+ if (configStream == null) {
+ throw new RuntimeException("Could not find any keycloak.json file in classpath.");
+ }
+
+ try {
+ return create(JsonSerialization.readValue(configStream, Configuration.class));
+ } catch (IOException e) {
+ throw new RuntimeException("Could not parse configuration.", e);
+ }
+ }
+
+ public static AuthzClient create(Configuration configuration) {
+ return new AuthzClient(configuration);
+ }
+
+ private final ServerConfiguration serverConfiguration;
+ private final Configuration deployment;
+
+ private AuthzClient(Configuration configuration) {
+ if (configuration == null) {
+ throw new IllegalArgumentException("Client configuration can not be null.");
+ }
+
+ String configurationUrl = configuration.getAuthServerUrl();
+
+ if (configurationUrl == null) {
+ throw new IllegalArgumentException("Configuration URL can not be null.");
+ }
+
+ configurationUrl += "/realms/" + configuration.getRealm() + "/.well-known/uma-configuration";
+
+ this.http = new Http(configuration);
+
+ try {
+ this.serverConfiguration = this.http.<ServerConfiguration>get(URI.create(configurationUrl))
+ .response().json(ServerConfiguration.class)
+ .execute();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not obtain configuration from server [" + configurationUrl + "].", e);
+ }
+
+ this.http.setServerConfiguration(this.serverConfiguration);
+
+ this.deployment = configuration;
+ }
+
+ public ProtectionResource protection() {
+ return new ProtectionResource(this.http, obtainAccessToken().getToken());
+ }
+
+ public AuthorizationResource authorization(String accesstoken) {
+ return new AuthorizationResource(this.http, accesstoken);
+ }
+
+ public AuthorizationResource authorization(String userName, String password) {
+ return new AuthorizationResource(this.http, obtainAccessToken(userName, password).getToken());
+ }
+
+ public EntitlementResource entitlement(String eat) {
+ return new EntitlementResource(this.http, eat);
+ }
+
+ public AccessTokenResponse obtainAccessToken() {
+ return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint())
+ .authentication()
+ .oauth2ClientCredentials()
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ }
+
+ public AccessTokenResponse obtainAccessToken(String userName, String password) {
+ return this.http.<AccessTokenResponse>post(this.serverConfiguration.getTokenEndpoint())
+ .authentication()
+ .oauth2ResourceOwnerPassword(userName, password)
+ .response()
+ .json(AccessTokenResponse.class)
+ .execute();
+ }
+
+ public ServerConfiguration getServerConfiguration() {
+ return this.serverConfiguration;
+ }
+
+ public Configuration getConfiguration() {
+ return this.deployment;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
new file mode 100644
index 0000000..076c2db
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/ClientAuthenticator.java
@@ -0,0 +1,27 @@
+/*
+ * 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.client;
+
+import java.util.HashMap;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ClientAuthenticator {
+ void configureClientCredentials(HashMap<String, String> requestParams, HashMap<String, String> requestHeaders);
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.java
new file mode 100644
index 0000000..835c830
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/Configuration.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.authorization.client;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.keycloak.util.BasicAuthHelper;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class Configuration {
+
+ @JsonIgnore
+ private HttpClient httpClient;
+
+ @JsonProperty("auth-server-url")
+ protected String authServerUrl;
+
+ @JsonProperty("realm")
+ protected String realm;
+
+ @JsonProperty("resource")
+ protected String clientId;
+
+ @JsonProperty("credentials")
+ protected Map<String, Object> clientCredentials = new HashMap<>();
+
+ public Configuration() {
+
+ }
+
+ public Configuration(String authServerUrl, String realm, String clientId, Map<String, Object> clientCredentials, HttpClient httpClient) {
+ this.authServerUrl = authServerUrl;
+ this.realm = realm;
+ this.clientId = clientId;
+ this.clientCredentials = clientCredentials;
+ this.httpClient = httpClient;
+ }
+
+ @JsonIgnore
+ private ClientAuthenticator clientAuthenticator = new ClientAuthenticator() {
+ @Override
+ public void configureClientCredentials(HashMap<String, String> requestParams, HashMap<String, String> requestHeaders) {
+ String secret = (String) clientCredentials.get("secret");
+
+ if (secret == null) {
+ throw new RuntimeException("Client secret not provided.");
+ }
+
+ requestHeaders.put("Authorization", BasicAuthHelper.createHeader(clientId, secret));
+ }
+ };
+
+ public HttpClient getHttpClient() {
+ if (this.httpClient == null) {
+ this.httpClient = HttpClients.createDefault();
+ }
+
+ return httpClient;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public String getAuthServerUrl() {
+ return authServerUrl;
+ }
+
+ public ClientAuthenticator getClientAuthenticator() {
+ return this.clientAuthenticator;
+ }
+
+ public Map<String, Object> getClientCredentials() {
+ return clientCredentials;
+ }
+
+ public String getRealm() {
+ return realm;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java
new file mode 100644
index 0000000..ef53586
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.authorization.client.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationRequest {
+
+ private String ticket;
+ private String rpt;
+
+ public AuthorizationRequest(String ticket, String rpt) {
+ this.ticket = ticket;
+ this.rpt = rpt;
+ }
+
+ public AuthorizationRequest(String ticket) {
+ this(ticket, null);
+ }
+
+ public AuthorizationRequest() {
+ this(null, null);
+ }
+
+ public String getTicket() {
+ return this.ticket;
+ }
+
+ public String getRpt() {
+ return this.rpt;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java
new file mode 100644
index 0000000..472c89a
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationResponse.java
@@ -0,0 +1,42 @@
+/*
+ * 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.authorization.client.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationResponse {
+
+ private String rpt;
+
+ public AuthorizationResponse(String rpt) {
+ this.rpt = rpt;
+ }
+
+ public AuthorizationResponse() {
+ this(null);
+ }
+
+ public String getRpt() {
+ return this.rpt;
+ }
+
+ public void setRpt(final String rpt) {
+ this.rpt = rpt;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java
new file mode 100644
index 0000000..daec233
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java
@@ -0,0 +1,34 @@
+package org.keycloak.authorization.client.representation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class EntitlementRequest {
+
+ private String rpt;
+
+ private List<PermissionRequest> permissions = new ArrayList<>();
+
+ public List<PermissionRequest> getPermissions() {
+ return permissions;
+ }
+
+ public String getRpt() {
+ return rpt;
+ }
+
+ public void setRpt(String rpt) {
+ this.rpt = rpt;
+ }
+
+ public void setPermissions(List<PermissionRequest> permissions) {
+ this.permissions = permissions;
+ }
+
+ public void addPermission(PermissionRequest request) {
+ getPermissions().add(request);
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementResponse.java
new file mode 100644
index 0000000..747c144
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementResponse.java
@@ -0,0 +1,42 @@
+/*
+ * 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.authorization.client.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class EntitlementResponse {
+
+ private String rpt;
+
+ public EntitlementResponse(String rpt) {
+ this.rpt = rpt;
+ }
+
+ public EntitlementResponse() {
+ this(null);
+ }
+
+ public String getRpt() {
+ return this.rpt;
+ }
+
+ public void setRpt(final String rpt) {
+ this.rpt = rpt;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java
new file mode 100644
index 0000000..fd3dc63
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ErrorResponse.java
@@ -0,0 +1,60 @@
+/*
+ * 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.authorization.client.representation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ErrorResponse {
+
+ private String error;
+
+ @JsonProperty("error_description")
+ private String description;
+
+ @JsonProperty("error_uri")
+ private String uri;
+
+ public ErrorResponse(final String error, final String description, final String uri) {
+ this.error = error;
+ this.description = description;
+ this.uri = uri;
+ }
+
+ public ErrorResponse(final String error) {
+ this(error, null, null);
+ }
+
+ public ErrorResponse() {
+ this(null, null, null);
+ }
+
+ public String getError() {
+ return this.error;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public String getUri() {
+ return this.uri;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java
new file mode 100644
index 0000000..bc9037e
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.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.authorization.client.representation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionRequest {
+
+ @JsonProperty("resource_set_id")
+ private String resourceSetId;
+
+ @JsonProperty("resource_set_name")
+ private String resourceSetName;
+
+ private Set<String> scopes;
+
+ public String getResourceSetId() {
+ return this.resourceSetId;
+ }
+
+ public void setResourceSetId(String resourceSetId) {
+ this.resourceSetId = resourceSetId;
+ }
+
+ public Set<String> getScopes() {
+ return this.scopes;
+ }
+
+ public void setScopes(Set<String> scopes) {
+ this.scopes = scopes;
+ }
+
+ public String getResourceSetName() {
+ return this.resourceSetName;
+ }
+
+ public void setResourceSetName(String resourceSetName) {
+ this.resourceSetName = resourceSetName;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java
new file mode 100644
index 0000000..1002bb5
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionResponse.java
@@ -0,0 +1,38 @@
+/*
+ * 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.authorization.client.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionResponse {
+
+ private final String ticket;
+
+ public PermissionResponse(String ticket) {
+ this.ticket = ticket;
+ }
+
+ public PermissionResponse() {
+ this(null);
+ }
+
+ public String getTicket() {
+ return this.ticket;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java
new file mode 100644
index 0000000..0f279bc
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/RegistrationResponse.java
@@ -0,0 +1,49 @@
+/*
+ * 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.authorization.client.representation;
+
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RegistrationResponse {
+
+ private final ResourceRepresentation resourceDescription;
+
+ public RegistrationResponse(ResourceRepresentation resourceDescription) {
+ this.resourceDescription = resourceDescription;
+ }
+
+ public RegistrationResponse() {
+ this(null);
+ }
+
+ @JsonUnwrapped
+ public ResourceRepresentation getResourceDescription() {
+ return this.resourceDescription;
+ }
+
+ public String getId() {
+ if (this.resourceDescription != null) {
+ return this.resourceDescription.getId();
+ }
+
+ return null;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
new file mode 100644
index 0000000..b0b6948
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
@@ -0,0 +1,184 @@
+/*
+ * 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.authorization.client.representation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * <p>One or more resources that the resource server manages as a set of protected resources.
+ *
+ * <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.2">OAuth-resource-reg</a>.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceRepresentation {
+
+ @JsonProperty("_id")
+ private String id;
+
+ private String name;
+ private String uri;
+ private String type;
+ private Set<ScopeRepresentation> scopes;
+
+ @JsonProperty("icon_uri")
+ private String iconUri;
+ private String owner;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param name a human-readable string describing a set of one or more resources
+ * @param uri a {@link URI} that provides the network location for the resource set being registered
+ * @param type a string uniquely identifying the semantics of the resource set
+ * @param scopes the available scopes for this resource set
+ * @param iconUri a {@link URI} for a graphic icon representing the resource set
+ */
+ public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, String uri, String type, String iconUri) {
+ this.name = name;
+ this.scopes = scopes;
+ this.uri = uri;
+ this.type = type;
+ this.iconUri = iconUri;
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param name a human-readable string describing a set of one or more resources
+ * @param uri a {@link URI} that provides the network location for the resource set being registered
+ * @param type a string uniquely identifying the semantics of the resource set
+ * @param scopes the available scopes for this resource set
+ */
+ public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, String uri, String type) {
+ this(name, scopes, uri, type, null);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param name a human-readable string describing a set of one or more resources
+ * @param serverUri a {@link URI} that identifies this resource server
+ * @param scopes the available scopes for this resource set
+ */
+ public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes) {
+ this(name, scopes, null, null, null);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ */
+ public ResourceRepresentation() {
+ this(null, null, null, null, null);
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getUri() {
+ return this.uri;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public Set<ScopeRepresentation> getScopes() {
+ return Collections.unmodifiableSet(this.scopes);
+ }
+
+ public String getIconUri() {
+ return this.iconUri;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public void setScopes(Set<ScopeRepresentation> scopes) {
+ this.scopes = scopes;
+ }
+
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public void addScope(ScopeRepresentation scopeRepresentation) {
+ if (this.scopes == null) {
+ this.scopes = new HashSet<>();
+ }
+ this.scopes.add(scopeRepresentation);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ResourceRepresentation that = (ResourceRepresentation) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+ @Override
+ public String toString() {
+ return "ResourceRepresentation{" +
+ "id='" + id + '\'' +
+ ", name='" + name + '\'' +
+ ", uri='" + uri + '\'' +
+ ", type='" + type + '\'' +
+ ", owner='" + owner + '\'' +
+ ", scopes=" + scopes +
+ '}';
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ScopeRepresentation.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ScopeRepresentation.java
new file mode 100644
index 0000000..4fcfc35
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ScopeRepresentation.java
@@ -0,0 +1,98 @@
+/*
+ * 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.authorization.client.representation;
+
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * <p>A bounded extent of access that is possible to perform on a resource set. In authorization policy terminology,
+ * a scope is one of the potentially many "verbs" that can logically apply to a resource set ("object").
+ *
+ * <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.1">OAuth-resource-reg</a>.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScopeRepresentation {
+
+ private String id;
+ private String name;
+ private String iconUri;
+
+ /**
+ * Creates an instance.
+ *
+ * @param name the a human-readable string describing some scope (extent) of access
+ * @param iconUri a {@link URI} for a graphic icon representing the scope
+ */
+ public ScopeRepresentation(String name, String iconUri) {
+ this.name = name;
+ this.iconUri = iconUri;
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param name the a human-readable string describing some scope (extent) of access
+ */
+ public ScopeRepresentation(String name) {
+ this(name, null);
+ }
+
+ /**
+ * Creates an instance.
+ */
+ public ScopeRepresentation() {
+ this(null, null);
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getIconUri() {
+ return this.iconUri;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ScopeRepresentation scope = (ScopeRepresentation) o;
+ return Objects.equals(getName(), scope.getName());
+ }
+
+ public int hashCode() {
+ return Objects.hash(getName());
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+}
\ No newline at end of file
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java
new file mode 100644
index 0000000..6716165
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ServerConfiguration.java
@@ -0,0 +1,235 @@
+/*
+ * 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.authorization.client.representation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.net.URI;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ServerConfiguration {
+
+ private String version;
+ private URI issuer;
+
+ @JsonProperty("pat_profiles_supported")
+ private Set<String> patProfiles;
+
+ @JsonProperty("pat_grant_types_supported")
+ private Set<String> patGrantTypes;
+
+ @JsonProperty("aat_profiles_supported")
+ private Set<String> aatProfiles;
+
+ @JsonProperty("aat_grant_types_supported")
+ private Set<String> aatGrantTypes;
+
+ @JsonProperty("rpt_profiles_supported")
+ private Set<String> rptProfiles;
+
+ @JsonProperty("claim_token_profiles_supported")
+ private Set<String> claimTokenProfiles;
+
+ @JsonProperty("dynamic_client_endpoint")
+ private URI dynamicClientEndpoint;
+
+ @JsonProperty("token_endpoint")
+ private URI tokenEndpoint;
+
+ @JsonProperty("authorization_endpoint")
+ private URI authorizationEndpoint;
+
+ @JsonProperty("requesting_party_claims_endpoint")
+ private URI requestingPartyClaimsEndpoint;
+
+ @JsonProperty("resource_set_registration_endpoint")
+ private URI resourceSetRegistrationEndpoint;
+
+ @JsonProperty("introspection_endpoint")
+ private URI introspectionEndpoint;
+
+ @JsonProperty("permission_registration_endpoint")
+ private URI permissionRegistrationEndpoint;
+
+ @JsonProperty("rpt_endpoint")
+ private URI rptEndpoint;
+
+ /**
+ * Non-standard, Keycloak specific configuration options
+ */
+ private String realm;
+
+ private String realmPublicKey;
+
+ private URI serverUrl;
+
+ public String getVersion() {
+ return this.version;
+ }
+
+ void setVersion(final String version) {
+ this.version = version;
+ }
+
+ public URI getIssuer() {
+ return this.issuer;
+ }
+
+ void setIssuer(final URI issuer) {
+ this.issuer = issuer;
+ }
+
+ public Set<String> getPatProfiles() {
+ return this.patProfiles;
+ }
+
+ void setPatProfiles(final Set<String> patProfiles) {
+ this.patProfiles = patProfiles;
+ }
+
+ public Set<String> getPatGrantTypes() {
+ return this.patGrantTypes;
+ }
+
+ void setPatGrantTypes(final Set<String> patGrantTypes) {
+ this.patGrantTypes = patGrantTypes;
+ }
+
+ public Set<String> getAatProfiles() {
+ return this.aatProfiles;
+ }
+
+ void setAatProfiles(final Set<String> aatProfiles) {
+ this.aatProfiles = aatProfiles;
+ }
+
+ public Set<String> getAatGrantTypes() {
+ return this.aatGrantTypes;
+ }
+
+ void setAatGrantTypes(final Set<String> aatGrantTypes) {
+ this.aatGrantTypes = aatGrantTypes;
+ }
+
+ public Set<String> getRptProfiles() {
+ return this.rptProfiles;
+ }
+
+ void setRptProfiles(final Set<String> rptProfiles) {
+ this.rptProfiles = rptProfiles;
+ }
+
+ public Set<String> getClaimTokenProfiles() {
+ return this.claimTokenProfiles;
+ }
+
+ void setClaimTokenProfiles(final Set<String> claimTokenProfiles) {
+ this.claimTokenProfiles = claimTokenProfiles;
+ }
+
+ public URI getDynamicClientEndpoint() {
+ return this.dynamicClientEndpoint;
+ }
+
+ void setDynamicClientEndpoint(final URI dynamicClientEndpoint) {
+ this.dynamicClientEndpoint = dynamicClientEndpoint;
+ }
+
+ public URI getTokenEndpoint() {
+ return this.tokenEndpoint;
+ }
+
+ void setTokenEndpoint(final URI tokenEndpoint) {
+ this.tokenEndpoint = tokenEndpoint;
+ }
+
+ public URI getAuthorizationEndpoint() {
+ return this.authorizationEndpoint;
+ }
+
+ void setAuthorizationEndpoint(final URI authorizationEndpoint) {
+ this.authorizationEndpoint = authorizationEndpoint;
+ }
+
+ public URI getRequestingPartyClaimsEndpoint() {
+ return this.requestingPartyClaimsEndpoint;
+ }
+
+ void setRequestingPartyClaimsEndpoint(final URI requestingPartyClaimsEndpoint) {
+ this.requestingPartyClaimsEndpoint = requestingPartyClaimsEndpoint;
+ }
+
+ public URI getResourceSetRegistrationEndpoint() {
+ return this.resourceSetRegistrationEndpoint;
+ }
+
+ void setResourceSetRegistrationEndpoint(final URI resourceSetRegistrationEndpoint) {
+ this.resourceSetRegistrationEndpoint = resourceSetRegistrationEndpoint;
+ }
+
+ public URI getIntrospectionEndpoint() {
+ return this.introspectionEndpoint;
+ }
+
+ void setIntrospectionEndpoint(final URI introspectionEndpoint) {
+ this.introspectionEndpoint = introspectionEndpoint;
+ }
+
+ public URI getPermissionRegistrationEndpoint() {
+ return this.permissionRegistrationEndpoint;
+ }
+
+ void setPermissionRegistrationEndpoint(final URI permissionRegistrationEndpoint) {
+ this.permissionRegistrationEndpoint = permissionRegistrationEndpoint;
+ }
+
+ public URI getRptEndpoint() {
+ return this.rptEndpoint;
+ }
+
+ void setRptEndpoint(final URI rptEndpoint) {
+ this.rptEndpoint = rptEndpoint;
+ }
+
+ public String getRealm() {
+ return this.realm;
+ }
+
+ public void setRealm(final String realm) {
+ this.realm = realm;
+ }
+
+ public String getRealmPublicKey() {
+ return this.realmPublicKey;
+ }
+
+ public void setRealmPublicKey(String realmPublicKey) {
+ this.realmPublicKey = realmPublicKey;
+ }
+
+ public URI getServerUrl() {
+ return this.serverUrl;
+ }
+
+ public void setServerUrl(URI serverUrl) {
+ this.serverUrl = serverUrl;
+ }
+}
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
new file mode 100644
index 0000000..7eaccb4
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/TokenIntrospectionResponse.java
@@ -0,0 +1,43 @@
+/*
+ * 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.client.representation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.representations.JsonWebToken;
+import org.keycloak.representations.authorization.Permission;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class TokenIntrospectionResponse extends JsonWebToken {
+
+ @JsonProperty
+ private Boolean active;
+
+ private List<Permission> permissions;
+
+ public Boolean getActive() {
+ return this.active;
+ }
+
+ public List<Permission> getPermissions() {
+ return this.permissions;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java
new file mode 100644
index 0000000..49b6d2d
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/AuthorizationResource.java
@@ -0,0 +1,58 @@
+/*
+ * 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.authorization.client.resource;
+
+
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.util.Http;
+import org.keycloak.authorization.client.util.HttpResponseException;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationResource {
+
+ private final Http http;
+ private final String accessToken;
+
+ public AuthorizationResource(Http http, String aat) {
+ this.http = http;
+ this.accessToken = aat;
+ }
+
+ public AuthorizationResponse authorize(AuthorizationRequest request) {
+ try {
+ return this.http.<AuthorizationResponse>post("/authz/authorize")
+ .authorizationBearer(this.accessToken)
+ .json(JsonSerialization.writeValueAsBytes(request))
+ .response().json(AuthorizationResponse.class).execute();
+ } catch (HttpResponseException e) {
+ if (403 == e.getStatusCode()) {
+ throw new AuthorizationDeniedException(e);
+ }
+ throw new RuntimeException("Failed to obtain authorization data.", e);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to obtain authorization data.", e);
+ }
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java
new file mode 100644
index 0000000..55c0abd
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java
@@ -0,0 +1,54 @@
+package org.keycloak.authorization.client.resource;
+
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.representation.EntitlementRequest;
+import org.keycloak.authorization.client.representation.EntitlementResponse;
+import org.keycloak.authorization.client.util.Http;
+import org.keycloak.authorization.client.util.HttpResponseException;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class EntitlementResource {
+
+ private final Http http;
+ private final String eat;
+
+ public EntitlementResource(Http http, String eat) {
+ this.http = http;
+ this.eat = eat;
+ }
+
+ public EntitlementResponse getAll(String resourceServerId) {
+ try {
+ return this.http.<EntitlementResponse>get("/authz/entitlement/" + resourceServerId)
+ .authorizationBearer(this.eat)
+ .response()
+ .json(EntitlementResponse.class).execute();
+ } catch (HttpResponseException e) {
+ if (403 == e.getStatusCode()) {
+ throw new AuthorizationDeniedException(e);
+ }
+ throw new RuntimeException("Failed to obtain entitlements.", e);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to obtain entitlements.", e);
+ }
+ }
+
+ public EntitlementResponse get(String resourceServerId, EntitlementRequest request) {
+ try {
+ return this.http.<EntitlementResponse>post("/authz/entitlement/" + resourceServerId)
+ .authorizationBearer(this.eat)
+ .json(JsonSerialization.writeValueAsBytes(request))
+ .response().json(EntitlementResponse.class).execute();
+ } catch (HttpResponseException e) {
+ if (403 == e.getStatusCode()) {
+ throw new AuthorizationDeniedException(e);
+ }
+ throw new RuntimeException("Failed to obtain entitlements.", e);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to obtain entitlements.", e);
+ }
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java
new file mode 100644
index 0000000..72247b2
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/PermissionResource.java
@@ -0,0 +1,50 @@
+/*
+ * 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.authorization.client.resource;
+
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.authorization.client.representation.PermissionResponse;
+import org.keycloak.authorization.client.util.Http;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionResource {
+
+ private final Http http;
+ private final String pat;
+
+ public PermissionResource(Http http, String pat) {
+ this.http = http;
+ this.pat = pat;
+ }
+
+ public PermissionResponse forResource(PermissionRequest request) {
+ try {
+ return this.http.<PermissionResponse>post("/authz/protection/permission")
+ .authorizationBearer(this.pat)
+ .json(JsonSerialization.writeValueAsBytes(request))
+ .response().json(PermissionResponse.class).execute();
+ } catch (IOException e) {
+ throw new RuntimeException("Error obtaining permission ticket.", e);
+ }
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
new file mode 100644
index 0000000..e237642
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java
@@ -0,0 +1,91 @@
+/*
+ * 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.authorization.client.resource;
+
+import org.keycloak.authorization.client.representation.RegistrationResponse;
+import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.util.Http;
+import org.keycloak.util.JsonSerialization;
+
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ProtectedResource {
+
+ private final Http http;
+ private final String pat;
+
+ public ProtectedResource(Http http, String pat) {
+ this.http = http;
+ this.pat = pat;
+ }
+
+ public RegistrationResponse create(ResourceRepresentation resource) {
+ try {
+ return this.http.<RegistrationResponse>post("/authz/protection/resource_set")
+ .authorizationBearer(this.pat)
+ .json(JsonSerialization.writeValueAsBytes(resource))
+ .response().json(RegistrationResponse.class).execute();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not create resource.", e);
+ }
+ }
+
+ public RegistrationResponse findById(String id) {
+ try {
+ return this.http.<RegistrationResponse>get("/authz/protection/resource_set/" + id)
+ .authorizationBearer(this.pat)
+ .response().json(RegistrationResponse.class).execute();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not find resource.", e);
+ }
+ }
+
+ public Set<String> findByFilter(String filter) {
+ try {
+ return this.http.<Set>get("/authz/protection/resource_set")
+ .authorizationBearer(this.pat)
+ .param("filter", filter)
+ .response().json(Set.class).execute();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not find resource.", e);
+ }
+ }
+
+ public Set<String> findAll() {
+ try {
+ return this.http.<Set>get("/authz/protection/resource_set")
+ .authorizationBearer(this.pat)
+ .response().json(Set.class).execute();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not find resource.", e);
+ }
+ }
+
+ public void delete(String id) {
+ try {
+ this.http.delete("/authz/protection/resource_set/" + id)
+ .authorizationBearer(this.pat)
+ .execute();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not delete resource.", e);
+ }
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java
new file mode 100644
index 0000000..536b188
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectionResource.java
@@ -0,0 +1,57 @@
+/*
+ * 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.client.resource;
+
+import org.keycloak.authorization.client.representation.TokenIntrospectionResponse;
+import org.keycloak.authorization.client.util.Http;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ProtectionResource {
+
+ private final String pat;
+ private final Http http;
+
+ public ProtectionResource(Http http, String pat) {
+ if (pat == null) {
+ throw new RuntimeException("No access token was provided when creating client for Protection API.");
+ }
+
+ this.http = http;
+ this.pat = pat;
+ }
+
+ public ProtectedResource resource() {
+ return new ProtectedResource(http, pat);
+ }
+
+ public PermissionResource permission() {
+ return new PermissionResource(http, pat);
+ }
+
+ public TokenIntrospectionResponse introspectRequestingPartyToken(String rpt) {
+ return this.http.<TokenIntrospectionResponse>post("/protocol/openid-connect/token/introspect")
+ .authentication()
+ .oauth2ClientCredentials()
+ .form()
+ .param("token_type_hint", "requesting_party_token")
+ .param("token", rpt)
+ .response().json(TokenIntrospectionResponse.class).execute();
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
new file mode 100644
index 0000000..503772f
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java
@@ -0,0 +1,65 @@
+/*
+ * 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.client.util;
+
+import org.apache.http.client.methods.RequestBuilder;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.ServerConfiguration;
+
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class Http {
+
+ private final Configuration configuration;
+ private ServerConfiguration serverConfiguration;
+
+ public Http(Configuration configuration) {
+ this.configuration = configuration;
+ }
+
+ public <R> HttpMethod<R> get(String path) {
+ return method(RequestBuilder.get(this.serverConfiguration.getIssuer() + path));
+ }
+
+ public <R> HttpMethod<R> get(URI path) {
+ return method(RequestBuilder.get(path));
+ }
+
+ public <R> HttpMethod<R> post(URI path) {
+ return method(RequestBuilder.post(path));
+ }
+
+ public <R> HttpMethod<R> post(String path) {
+ return method(RequestBuilder.post(this.serverConfiguration.getIssuer() + path));
+ }
+
+ public <R> HttpMethod<R> delete(String path) {
+ return method(RequestBuilder.delete(this.serverConfiguration.getIssuer() + path));
+ }
+
+ private <R> HttpMethod<R> method(RequestBuilder builder) {
+ return new HttpMethod(this.configuration, builder);
+ }
+
+ public void setServerConfiguration(ServerConfiguration serverConfiguration) {
+ this.serverConfiguration = serverConfiguration;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
new file mode 100644
index 0000000..a693263
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethod.java
@@ -0,0 +1,158 @@
+/*
+ * 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.client.util;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.StatusLine;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.keycloak.authorization.client.Configuration;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class HttpMethod<R> {
+
+ private final HttpClient httpClient;
+ private final RequestBuilder builder;
+ protected final Configuration configuration;
+ protected final HashMap<String, String> headers;
+ protected final HashMap<String, String> params;
+ private HttpMethodResponse<R> response;
+
+ public HttpMethod(Configuration configuration, RequestBuilder builder) {
+ this(configuration, builder, new HashMap<>(), new HashMap<>());
+ }
+
+ public HttpMethod(Configuration configuration, RequestBuilder builder, HashMap<String, String> params, HashMap<String, String> headers) {
+ this.configuration = configuration;
+ this.httpClient = configuration.getHttpClient();
+ this.builder = builder;
+ this.params = params;
+ this.headers = headers;
+ }
+
+ public void execute() {
+ execute(new HttpResponseProcessor<R>() {
+ @Override
+ public R process(byte[] entity) {
+ return null;
+ }
+ });
+ }
+
+ public R execute(HttpResponseProcessor<R> responseProcessor) {
+ byte[] bytes = null;
+
+ try {
+ for (Map.Entry<String, String> header : this.headers.entrySet()) {
+ this.builder.setHeader(header.getKey(), header.getValue());
+ }
+
+ preExecute(this.builder);
+
+ HttpResponse response = this.httpClient.execute(this.builder.build());
+ HttpEntity entity = response.getEntity();
+
+ if (entity != null) {
+ bytes = EntityUtils.toByteArray(entity);
+ }
+
+ StatusLine statusLine = response.getStatusLine();
+ int statusCode = statusLine.getStatusCode();
+
+ if (statusCode < 200 || statusCode >= 300) {
+ throw new HttpResponseException(statusCode, statusLine.getReasonPhrase(), bytes);
+ }
+
+ if (bytes == null) {
+ return null;
+ }
+
+ return responseProcessor.process(bytes);
+ } catch (HttpResponseException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new RuntimeException("Error executing http method [" + builder + "]. Response : " + new String(bytes), e);
+ }
+ }
+
+ protected void preExecute(RequestBuilder builder) {
+ for (Map.Entry<String, String> param : params.entrySet()) {
+ builder.addParameter(param.getKey(), param.getValue());
+ }
+ }
+
+ public HttpMethod<R> authorizationBearer(String bearer) {
+ this.builder.addHeader("Authorization", "Bearer " + bearer);
+ return this;
+ }
+
+ public HttpMethodResponse<R> response() {
+ this.response = new HttpMethodResponse(this);
+ return this.response;
+ }
+
+ public HttpMethodAuthenticator<R> authentication() {
+ return new HttpMethodAuthenticator<R>(this);
+ }
+
+ public HttpMethod<R> param(String name, String value) {
+ this.params.put(name, value);
+ return this;
+ }
+
+ public HttpMethod<R> json(byte[] entity) {
+ this.builder.addHeader("Content-Type", "application/json");
+ this.builder.setEntity(new ByteArrayEntity(entity));
+ return this;
+ }
+
+ public HttpMethod<R> form() {
+ return new HttpMethod<R>(this.configuration, this.builder, this.params, this.headers) {
+ @Override
+ protected void preExecute(RequestBuilder builder) {
+ if (params != null) {
+ List<NameValuePair> formparams = new ArrayList<>();
+
+ for (Map.Entry<String, String> param : params.entrySet()) {
+ formparams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
+ }
+
+ try {
+ builder.setEntity(new UrlEncodedFormEntity(formparams, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("Error creating form parameters");
+ }
+ }
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.java
new file mode 100644
index 0000000..d67090a
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodAuthenticator.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.authorization.client.util;
+
+import org.keycloak.OAuth2Constants;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class HttpMethodAuthenticator<R> {
+
+ private final HttpMethod<R> method;
+
+ public HttpMethodAuthenticator(HttpMethod<R> method) {
+ this.method = method;
+ }
+
+ public HttpMethod<R> oauth2ClientCredentials() {
+ this.method.params.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS);
+ configureClientCredentials();
+ return this.method;
+ }
+
+ public HttpMethod<R> oauth2ResourceOwnerPassword(String userName, String password) {
+ this.method.params.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
+ this.method.params.put("username", userName);
+ this.method.params.put("password", password);
+ configureClientCredentials();
+ return this.method;
+ }
+
+ private void configureClientCredentials() {
+ this.method.configuration.getClientAuthenticator().configureClientCredentials(this.method.params, this.method.headers);
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.java
new file mode 100644
index 0000000..4155240
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpMethodResponse.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.authorization.client.util;
+
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class HttpMethodResponse<R> {
+
+ private final HttpMethod<R> method;
+
+ public HttpMethodResponse(HttpMethod method) {
+ this.method = method;
+ }
+
+ public R execute() {
+ return this.method.execute(new HttpResponseProcessor<R>() {
+ @Override
+ public R process(byte[] entity) {
+ return null;
+ }
+ });
+ }
+
+ public HttpMethodResponse<R> json(Class<R> responseType) {
+ return new HttpMethodResponse<R>(this.method) {
+ @Override
+ public R execute() {
+ return method.execute(new HttpResponseProcessor<R>() {
+ @Override
+ public R process(byte[] entity) {
+ try {
+ return JsonSerialization.readValue(entity, responseType);
+ } catch (IOException e) {
+ throw new RuntimeException("Error parsing JSON response.", e);
+ }
+ }
+ });
+ }
+ };
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpResponseException.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpResponseException.java
new file mode 100644
index 0000000..9b783e7
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpResponseException.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.authorization.client.util;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class HttpResponseException extends RuntimeException {
+
+ private final int statusCode;
+ private final String reasonPhrase;
+ private final byte[] bytes;
+
+ public HttpResponseException(int statusCode, String reasonPhrase, byte[] bytes) {
+ this.statusCode = statusCode;
+ this.reasonPhrase = reasonPhrase;
+ this.bytes = bytes;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public String getReasonPhrase() {
+ return reasonPhrase;
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+}
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpResponseProcessor.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpResponseProcessor.java
new file mode 100644
index 0000000..72eba04
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/HttpResponseProcessor.java
@@ -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.
+ *
+ */
+package org.keycloak.authorization.client.util;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface HttpResponseProcessor<R> {
+
+ R process(byte[] entity);
+}
authz/policy/aggregate/pom.xml 33(+33 -0)
diff --git a/authz/policy/aggregate/pom.xml b/authz/policy/aggregate/pom.xml
new file mode 100644
index 0000000..325b154
--- /dev/null
+++ b/authz/policy/aggregate/pom.xml
@@ -0,0 +1,33 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-provider-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-policy-aggregate</artifactId>
+ <packaging>jar</packaging>
+
+ <name>KeyCloak AuthZ: Aggregate Policy Provider</name>
+ <description>KeyCloak AuthZ: Aggregate Policy Provider</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyAdminResource.java b/authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyAdminResource.java
new file mode 100644
index 0000000..22ce794
--- /dev/null
+++ b/authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyAdminResource.java
@@ -0,0 +1,69 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.provider.time;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AggregatePolicyAdminResource implements PolicyProviderAdminService {
+
+ private final ResourceServer resourceServer;
+
+ public AggregatePolicyAdminResource(ResourceServer resourceServer) {
+ this.resourceServer = resourceServer;
+ }
+
+ @Override
+ public void onCreate(Policy policy) {
+ verifyCircularReference(policy, new ArrayList<>());
+ }
+
+ @Override
+ public void onUpdate(Policy policy) {
+ verifyCircularReference(policy, new ArrayList<>());
+ }
+
+ private void verifyCircularReference(Policy policy, List<String> ids) {
+ if (!policy.getType().equals("aggregate")) {
+ return;
+ }
+
+ if (ids.contains(policy.getId())) {
+ throw new RuntimeException("Circular reference found [" + policy.getName() + "].");
+ }
+
+ ids.add(policy.getId());
+
+ for (Policy associated : policy.getAssociatedPolicies()) {
+ verifyCircularReference(associated, ids);
+ }
+ }
+
+ @Override
+ public void onRemove(Policy policy) {
+
+ }
+}
diff --git a/authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyProvider.java b/authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyProvider.java
new file mode 100644
index 0000000..960eb1f
--- /dev/null
+++ b/authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyProvider.java
@@ -0,0 +1,75 @@
+/*
+ * 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.authorization.policy.provider.time;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
+import org.keycloak.authorization.policy.evaluation.Result;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AggregatePolicyProvider implements PolicyProvider {
+
+ private final Policy policy;
+ private final AuthorizationProvider authorization;
+
+ public AggregatePolicyProvider(Policy policy, AuthorizationProvider authorization) {
+ this.policy = policy;
+ this.authorization = authorization;
+ }
+
+ @Override
+ public void evaluate(Evaluation evaluation) {
+ //TODO: need to detect deep recursions
+ DecisionResultCollector decision = new DecisionResultCollector() {
+ @Override
+ protected void onComplete(List<Result> results) {
+ if (results.isEmpty()) {
+ evaluation.deny();
+ } else {
+ Result result = results.iterator().next();
+
+ if (Effect.PERMIT.equals(result.getEffect())) {
+ evaluation.grant();
+ }
+ }
+ }
+ };
+
+ this.policy.getAssociatedPolicies().forEach(associatedPolicy -> {
+ PolicyProviderFactory providerFactory = authorization.getProviderFactory(associatedPolicy.getType());
+ PolicyProvider policyProvider = providerFactory.create(associatedPolicy, authorization);
+ policyProvider.evaluate(new DefaultEvaluation(evaluation.getPermission(), evaluation.getContext(), policy, associatedPolicy, decision));
+ });
+
+ decision.onComplete();
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyProviderFactory.java b/authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyProviderFactory.java
new file mode 100644
index 0000000..b4f5d97
--- /dev/null
+++ b/authz/policy/aggregate/src/main/java/org/keycloak/authorization/policy/provider/time/AggregatePolicyProviderFactory.java
@@ -0,0 +1,62 @@
+package org.keycloak.authorization.policy.provider.time;
+
+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.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;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AggregatePolicyProviderFactory implements PolicyProviderFactory {
+
+ @Override
+ public String getName() {
+ return "Aggregated";
+ }
+
+ @Override
+ public String getGroup() {
+ return "Others";
+ }
+
+ @Override
+ public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
+ return new AggregatePolicyProvider(policy, authorization);
+ }
+
+ @Override
+ public PolicyProviderAdminService getAdminResource(ResourceServer resourceServer) {
+ return new AggregatePolicyAdminResource(resourceServer);
+ }
+
+ @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 "aggregate";
+ }
+}
diff --git a/authz/policy/aggregate/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/aggregate/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
new file mode 100644
index 0000000..d17f63f
--- /dev/null
+++ b/authz/policy/aggregate/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.policy.provider.time.AggregatePolicyProviderFactory
\ No newline at end of file
authz/policy/drools/pom.xml 45(+45 -0)
diff --git a/authz/policy/drools/pom.xml b/authz/policy/drools/pom.xml
new file mode 100644
index 0000000..0f13d2a
--- /dev/null
+++ b/authz/policy/drools/pom.xml
@@ -0,0 +1,45 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-provider-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-policy-drools</artifactId>
+ <packaging>jar</packaging>
+
+ <name>KeyCloak AuthZ: Drools Policy Provider</name>
+ <description>KeyCloak AuthZ: Drools Policy Provider</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-services</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.drools</groupId>
+ <artifactId>drools-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.jboss.spec.javax.ws.rs</groupId>
+ <artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicy.java b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicy.java
new file mode 100644
index 0000000..c8848c1
--- /dev/null
+++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicy.java
@@ -0,0 +1,71 @@
+package org.keycloak.authorization.policy.provider.drools;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.kie.api.KieServices;
+import org.kie.api.builder.KieScanner;
+import org.kie.api.runtime.KieContainer;
+import org.kie.api.runtime.KieSession;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+class DroolsPolicy {
+
+ private static final int SESSION_POOL_SIZE = 10;
+
+ private final KieContainer kc;
+ private final KieScanner kcs;
+ private final String sessionName;
+
+ DroolsPolicy(KieServices ks, Policy associatedPolicy) {
+ String groupId = associatedPolicy.getConfig().get("mavenArtifactGroupId");
+ String artifactId = associatedPolicy.getConfig().get("mavenArtifactId");
+ String version = associatedPolicy.getConfig().get("mavenArtifactVersion");
+ String scannerPeriod = associatedPolicy.getConfig().get("scannerPeriod");
+ String scannerPeriodUnit = associatedPolicy.getConfig().get("scannerPeriodUnit");
+ this.sessionName = associatedPolicy.getConfig().get("sessionName");
+
+ this.kc = ks.newKieContainer(ks.newReleaseId(groupId, artifactId, version));
+ this.kcs = ks.newKieScanner(this.kc);
+ this.kcs.start(toMillis(scannerPeriod, scannerPeriodUnit));
+
+ KieSession session = this.kc.newKieSession(this.sessionName);
+
+ if (session == null) {
+ throw new RuntimeException("Could not obtain session with name [" + this.sessionName + "].");
+ }
+
+ session.dispose();
+ }
+
+ void evaluate(Evaluation evaluation) {
+ KieSession session = this.kc.newKieSession(this.sessionName);
+
+ session.insert(evaluation);
+ session.fireAllRules();
+
+ session.dispose();
+ }
+
+ void dispose() {
+ this.kcs.stop();
+ }
+
+ private long toMillis(final String scannerPeriod, final String scannerPeriodUnit) {
+ switch (scannerPeriodUnit) {
+ case "Seconds":
+ return TimeUnit.SECONDS.toMillis(Integer.valueOf(scannerPeriod));
+ case "Minutes":
+ return TimeUnit.MINUTES.toMillis(Integer.valueOf(scannerPeriod));
+ case "Hours":
+ return TimeUnit.HOURS.toMillis(Integer.valueOf(scannerPeriod));
+ case "Days":
+ return TimeUnit.DAYS.toMillis(Integer.valueOf(scannerPeriod));
+ }
+
+ throw new RuntimeException("Invalid time period [" + scannerPeriodUnit + "].");
+ }
+}
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
new file mode 100644
index 0000000..1ee1d34
--- /dev/null
+++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyAdminResource.java
@@ -0,0 +1,65 @@
+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.kie.api.runtime.KieContainer;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class DroolsPolicyAdminResource implements PolicyProviderAdminService {
+
+ private final ResourceServer resourceServer;
+ private final DroolsPolicyProviderFactory factory;
+
+ public DroolsPolicyAdminResource(ResourceServer resourceServer, DroolsPolicyProviderFactory factory) {
+ this.resourceServer = resourceServer;
+ this.factory = factory;
+ }
+
+ @Override
+ public void onCreate(Policy policy) {
+ this.factory.update(policy);
+ }
+
+ @Override
+ public void onUpdate(Policy policy) {
+ this.factory.update(policy);
+ }
+
+ @Override
+ public void onRemove(Policy policy) {
+ this.factory.remove(policy);
+ }
+
+ @Path("/resolveModules")
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response resolveModules(PolicyRepresentation policy) {
+ return Response.ok(getContainer(policy).getKieBaseNames()).build();
+ }
+
+ @Path("/resolveSessions")
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response resolveSessions(PolicyRepresentation policy) {
+ return Response.ok(getContainer(policy).getKieSessionNamesInKieBase(policy.getConfig().get("moduleName"))).build();
+ }
+
+ private KieContainer getContainer(PolicyRepresentation policy) {
+ String groupId = policy.getConfig().get("mavenArtifactGroupId");
+ String artifactId = policy.getConfig().get("mavenArtifactId");
+ String version = policy.getConfig().get("mavenArtifactVersion");
+ return this.factory.getKieContainer(groupId, artifactId, version);
+ }
+}
diff --git a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProvider.java b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProvider.java
new file mode 100644
index 0000000..c53e361
--- /dev/null
+++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProvider.java
@@ -0,0 +1,43 @@
+/*
+ * 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.authorization.policy.provider.drools;
+
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class DroolsPolicyProvider implements PolicyProvider {
+
+ private final DroolsPolicy policy;
+
+ public DroolsPolicyProvider(DroolsPolicy policy) {
+ this.policy = policy;
+ }
+
+ @Override
+ public void evaluate(Evaluation evaluationt) {
+ this.policy.evaluate(evaluationt);
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java
new file mode 100644
index 0000000..3df4210
--- /dev/null
+++ b/authz/policy/drools/src/main/java/org/keycloak/authorization/policy/provider/drools/DroolsPolicyProviderFactory.java
@@ -0,0 +1,96 @@
+package org.keycloak.authorization.policy.provider.drools;
+
+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.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 org.keycloak.provider.ProviderFactory;
+import org.kie.api.KieServices;
+import org.kie.api.KieServices.Factory;
+import org.kie.api.runtime.KieContainer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class DroolsPolicyProviderFactory implements PolicyProviderFactory {
+
+ private KieServices ks;
+ private final Map<String, DroolsPolicy> containers = new HashMap<>();
+
+ @Override
+ public String getName() {
+ return "Drools";
+ }
+
+ @Override
+ public String getGroup() {
+ return "Rule Based";
+ }
+
+ @Override
+ public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
+ if (!this.containers.containsKey(policy.getId())) {
+ update(policy);
+ }
+
+ return new DroolsPolicyProvider(this.containers.get(policy.getId()));
+ }
+
+ @Override
+ public PolicyProviderAdminService getAdminResource(ResourceServer resourceServer) {
+ return new DroolsPolicyAdminResource(resourceServer, this);
+ }
+
+ @Override
+ public PolicyProvider create(KeycloakSession session) {
+ return null;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ this.ks = Factory.get();
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ ProviderFactory<AuthorizationProvider> providerFactory = factory.getProviderFactory(AuthorizationProvider.class);
+ AuthorizationProvider authorization = providerFactory.create(factory.create());
+ authorization.getStoreFactory().getPolicyStore().findByType(getId()).forEach(this::update);
+ }
+
+ @Override
+ public void close() {
+ this.containers.values().forEach(DroolsPolicy::dispose);
+ this.containers.clear();
+ }
+
+ @Override
+ public String getId() {
+ return "drools";
+ }
+
+ void update(Policy policy) {
+ remove(policy);
+ this.containers.put(policy.getId(), new DroolsPolicy(this.ks, policy));
+ }
+
+ void remove(Policy policy) {
+ DroolsPolicy holder = this.containers.remove(policy.getId());
+
+ if (holder != null) {
+ holder.dispose();
+ }
+ }
+
+ KieContainer getKieContainer(String groupId, String artifactId, String version) {
+ return this.ks.newKieContainer(this.ks.newReleaseId(groupId, artifactId, version));
+ }
+}
diff --git a/authz/policy/drools/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/drools/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
new file mode 100644
index 0000000..512e334
--- /dev/null
+++ b/authz/policy/drools/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.policy.provider.drools.DroolsPolicyProviderFactory
\ No newline at end of file
authz/policy/javascript/pom.xml 33(+33 -0)
diff --git a/authz/policy/javascript/pom.xml b/authz/policy/javascript/pom.xml
new file mode 100644
index 0000000..53e35ee
--- /dev/null
+++ b/authz/policy/javascript/pom.xml
@@ -0,0 +1,33 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-provider-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-policy-js</artifactId>
+ <packaging>jar</packaging>
+
+ <name>KeyCloak AuthZ: Javascript-based Policy Provider</name>
+ <description>KeyCloak AuthZ: Javascript-based Policy Provider</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/authz/policy/javascript/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java b/authz/policy/javascript/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
new file mode 100644
index 0000000..eeeb3ea
--- /dev/null
+++ b/authz/policy/javascript/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProvider.java
@@ -0,0 +1,57 @@
+/*
+ * 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.authorization.policy.provider.js;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JSPolicyProvider implements PolicyProvider {
+
+ private final Policy policy;
+
+ public JSPolicyProvider(Policy policy) {
+ this.policy = policy;
+ }
+
+ @Override
+ public void evaluate(Evaluation evaluation) {
+ ScriptEngineManager manager = new ScriptEngineManager();
+ ScriptEngine engine = manager.getEngineByName("nashorn");
+
+ engine.put("$evaluation", evaluation);
+
+ try {
+ engine.eval(policy.getConfig().get("code"));
+ } catch (ScriptException e) {
+ throw new RuntimeException("Error evaluating JS Policy [" + policy.getName() + "].", e);
+ }
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/authz/policy/javascript/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java b/authz/policy/javascript/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
new file mode 100644
index 0000000..8134d95
--- /dev/null
+++ b/authz/policy/javascript/src/main/java/org/keycloak/authorization/policy/provider/js/JSPolicyProviderFactory.java
@@ -0,0 +1,62 @@
+package org.keycloak.authorization.policy.provider.js;
+
+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.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;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JSPolicyProviderFactory implements PolicyProviderFactory {
+
+ @Override
+ public String getName() {
+ return "JavaScript";
+ }
+
+ @Override
+ public String getGroup() {
+ return "Rule Based";
+ }
+
+ @Override
+ public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
+ return new JSPolicyProvider(policy);
+ }
+
+ @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 "js";
+ }
+}
diff --git a/authz/policy/javascript/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/javascript/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
new file mode 100644
index 0000000..09d56cc
--- /dev/null
+++ b/authz/policy/javascript/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.policy.provider.js.JSPolicyProviderFactory
\ No newline at end of file
authz/policy/pom.xml 31(+31 -0)
diff --git a/authz/policy/pom.xml b/authz/policy/pom.xml
new file mode 100644
index 0000000..64766a4
--- /dev/null
+++ b/authz/policy/pom.xml
@@ -0,0 +1,31 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-provider-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <name>KeyCloak AuthZ: Provider Parent</name>
+ <description>KeyCloak AuthZ: Provider Parent</description>
+
+ <modules>
+ <module>user</module>
+ <module>role</module>
+ <module>drools</module>
+ <module>resource</module>
+ <module>scope</module>
+ <module>javascript</module>
+ <module>time</module>
+ <module>aggregate</module>
+ </modules>
+
+</project>
\ No newline at end of file
authz/policy/resource/pom.xml 33(+33 -0)
diff --git a/authz/policy/resource/pom.xml b/authz/policy/resource/pom.xml
new file mode 100644
index 0000000..b59226d
--- /dev/null
+++ b/authz/policy/resource/pom.xml
@@ -0,0 +1,33 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-provider-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-policy-resource</artifactId>
+ <packaging>jar</packaging>
+
+ <name>KeyCloak Authz: Resource Policy Provider</name>
+ <description>KeyCloak Authz: Resource Policy Provider</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/authz/policy/resource/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java b/authz/policy/resource/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java
new file mode 100644
index 0000000..c76a989
--- /dev/null
+++ b/authz/policy/resource/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProvider.java
@@ -0,0 +1,41 @@
+/*
+ * 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.authorization.policy.provider.resource;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourcePolicyProvider implements PolicyProvider {
+
+ public ResourcePolicyProvider(Policy policy) {
+
+ }
+
+ @Override
+ public void evaluate(Evaluation evaluation) {
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/authz/policy/resource/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java b/authz/policy/resource/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java
new file mode 100644
index 0000000..8ea4800
--- /dev/null
+++ b/authz/policy/resource/src/main/java/org/keycloak/authorization/policy/provider/resource/ResourcePolicyProviderFactory.java
@@ -0,0 +1,62 @@
+package org.keycloak.authorization.policy.provider.resource;
+
+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.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;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourcePolicyProviderFactory implements PolicyProviderFactory {
+
+ @Override
+ public String getName() {
+ return "Resource-Based";
+ }
+
+ @Override
+ public String getGroup() {
+ return "Permission";
+ }
+
+ @Override
+ public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
+ return new ResourcePolicyProvider(policy);
+ }
+
+ @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 "resource";
+ }
+}
diff --git a/authz/policy/resource/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/resource/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
new file mode 100644
index 0000000..c5c6976
--- /dev/null
+++ b/authz/policy/resource/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.policy.provider.resource.ResourcePolicyProviderFactory
\ No newline at end of file
authz/policy/role/pom.xml 51(+51 -0)
diff --git a/authz/policy/role/pom.xml b/authz/policy/role/pom.xml
new file mode 100644
index 0000000..64bbbd2
--- /dev/null
+++ b/authz/policy/role/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-provider-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-policy-role</artifactId>
+ <packaging>jar</packaging>
+
+ <name>KeyCloak AuthZ: Role Policy Provider</name>
+ <description>KeyCloak AuthZ: Role Policy Provider</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/authz/policy/role/src/main/java/org/keycloak/authorization/policy/provider/identity/RolePolicyProvider.java b/authz/policy/role/src/main/java/org/keycloak/authorization/policy/provider/identity/RolePolicyProvider.java
new file mode 100644
index 0000000..f671153
--- /dev/null
+++ b/authz/policy/role/src/main/java/org/keycloak/authorization/policy/provider/identity/RolePolicyProvider.java
@@ -0,0 +1,75 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.policy.provider.identity;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.identity.Identity;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.evaluation.EvaluationContext;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+
+import static org.keycloak.authorization.policy.provider.identity.RolePolicyProviderFactory.getRoles;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RolePolicyProvider implements PolicyProvider {
+
+ private final Policy policy;
+ private final AuthorizationProvider authorization;
+
+ public RolePolicyProvider(Policy policy, AuthorizationProvider authorization) {
+ this.policy = policy;
+ this.authorization = authorization;
+ }
+
+ public RolePolicyProvider() {
+ this(null, null);
+ }
+
+ @Override
+ public void evaluate(Evaluation evaluation) {
+ EvaluationContext context = evaluation.getContext();
+ String[] roleIds = getRoles(this.policy);
+
+ if (roleIds.length > 0) {
+ Identity identity = context.getIdentity();
+
+ for (String roleId : roleIds) {
+ RoleModel role = getCurrentRealm().getRoleById(roleId);
+
+ if (role != null && identity.hasRole(role.getName())) {
+ evaluation.grant();
+ break;
+ }
+ }
+ }
+ }
+
+ private RealmModel getCurrentRealm() {
+ return this.authorization.getKeycloakSession().getContext().getRealm();
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/authz/policy/role/src/main/java/org/keycloak/authorization/policy/provider/identity/RolePolicyProviderFactory.java b/authz/policy/role/src/main/java/org/keycloak/authorization/policy/provider/identity/RolePolicyProviderFactory.java
new file mode 100644
index 0000000..2aa8c80
--- /dev/null
+++ b/authz/policy/role/src/main/java/org/keycloak/authorization/policy/provider/identity/RolePolicyProviderFactory.java
@@ -0,0 +1,132 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.provider.identity;
+
+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.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RoleContainerModel.RoleRemovedEvent;
+import org.keycloak.models.RoleModel;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RolePolicyProviderFactory implements PolicyProviderFactory {
+
+ @Override
+ public String getName() {
+ return "Role-Based";
+ }
+
+ @Override
+ public String getGroup() {
+ return "Identity Based";
+ }
+
+ @Override
+ public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
+ return new RolePolicyProvider(policy, authorization);
+ }
+
+ @Override
+ public PolicyProviderAdminService getAdminResource(ResourceServer resourceServer) {
+ return null;
+ }
+
+ @Override
+ public PolicyProvider create(KeycloakSession session) {
+ return new RolePolicyProvider();
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ factory.register(event -> {
+ if (event instanceof RoleRemovedEvent) {
+ KeycloakSession keycloakSession = ((RoleRemovedEvent) event).getKeycloakSession();
+ AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
+ PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
+ RoleModel removedRole = ((RoleRemovedEvent) event).getRole();
+
+ policyStore.findByType(getId()).forEach(policy -> {
+ List<String> roles = new ArrayList<>();
+
+ for (String roleId : getRoles(policy)) {
+ if (!roleId.equals(removedRole.getId())) {
+ roles.add(roleId);
+ }
+ }
+
+ try {
+ if (roles.isEmpty()) {
+ policyStore.findDependentPolicies(policy.getId()).forEach(dependentPolicy -> {
+ dependentPolicy.removeAssociatedPolicy(policy);
+ });
+ policyStore.delete(policy.getId());
+ } else {
+ policy.getConfig().put("roles", JsonSerialization.writeValueAsString(roles));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Error while synchronizing roles with policy [" + policy.getName() + "].", e);
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "role";
+ }
+
+ static String[] getRoles(Policy policy) {
+ String roles = policy.getConfig().get("roles");
+
+ if (roles != null) {
+ try {
+ return JsonSerialization.readValue(roles.getBytes(), String[].class);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not parse roles [" + roles + "] from policy config [" + policy.getName() + ".", e);
+ }
+ }
+
+ return new String[]{};
+ }
+}
diff --git a/authz/policy/role/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/role/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
new file mode 100644
index 0000000..bdfa039
--- /dev/null
+++ b/authz/policy/role/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.policy.provider.identity.RolePolicyProviderFactory
\ No newline at end of file
authz/policy/scope/pom.xml 33(+33 -0)
diff --git a/authz/policy/scope/pom.xml b/authz/policy/scope/pom.xml
new file mode 100644
index 0000000..f51802b
--- /dev/null
+++ b/authz/policy/scope/pom.xml
@@ -0,0 +1,33 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-provider-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-policy-scope</artifactId>
+ <packaging>jar</packaging>
+
+ <name>KeyCloak AuthZ: Scope Policy Provider</name>
+ <description>KeyCloak AuthZ: Scope Policy Provider</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/authz/policy/scope/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java b/authz/policy/scope/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java
new file mode 100644
index 0000000..5c10cc3
--- /dev/null
+++ b/authz/policy/scope/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProvider.java
@@ -0,0 +1,43 @@
+/*
+ * 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.authorization.policy.provider.scope;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScopePolicyProvider implements PolicyProvider {
+
+ private final Policy policy;
+
+ public ScopePolicyProvider(Policy policy) {
+ this.policy = policy;
+ }
+
+ @Override
+ public void evaluate(Evaluation evaluation) {
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/authz/policy/scope/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java b/authz/policy/scope/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java
new file mode 100644
index 0000000..6ed0cd5
--- /dev/null
+++ b/authz/policy/scope/src/main/java/org/keycloak/authorization/policy/provider/scope/ScopePolicyProviderFactory.java
@@ -0,0 +1,62 @@
+package org.keycloak.authorization.policy.provider.scope;
+
+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.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;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScopePolicyProviderFactory implements PolicyProviderFactory {
+
+ @Override
+ public String getName() {
+ return "Scope-Based";
+ }
+
+ @Override
+ public String getGroup() {
+ return "Permission";
+ }
+
+ @Override
+ public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
+ return new ScopePolicyProvider(policy);
+ }
+
+ @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 "scope";
+ }
+}
diff --git a/authz/policy/scope/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/scope/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
new file mode 100644
index 0000000..b3adce4
--- /dev/null
+++ b/authz/policy/scope/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.policy.provider.scope.ScopePolicyProviderFactory
\ No newline at end of file
authz/policy/time/pom.xml 32(+32 -0)
diff --git a/authz/policy/time/pom.xml b/authz/policy/time/pom.xml
new file mode 100644
index 0000000..9d1abcf
--- /dev/null
+++ b/authz/policy/time/pom.xml
@@ -0,0 +1,32 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-provider-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-policy-time</artifactId>
+ <packaging>jar</packaging>
+
+ <name>KeyCloak AuthZ: Time-based Policy Provider</name>
+ <description>KeyCloak AuthZ: Time-based Policy Provider</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+</project>
\ No newline at end of file
diff --git a/authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyAdminResource.java b/authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyAdminResource.java
new file mode 100644
index 0000000..75de911
--- /dev/null
+++ b/authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyAdminResource.java
@@ -0,0 +1,64 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.provider.time;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
+
+import java.text.SimpleDateFormat;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class TimePolicyAdminResource implements PolicyProviderAdminService {
+
+ @Override
+ public void onCreate(Policy policy) {
+ validateConfig(policy);
+ }
+
+ private void validateConfig(Policy policy) {
+ String nbf = policy.getConfig().get("nbf");
+ String noa = policy.getConfig().get("noa");
+
+ if (nbf == null && noa == null) {
+ throw new RuntimeException("You must provide NotBefore, NotOnOrAfter or both.");
+ }
+
+ validateFormat(nbf);
+ validateFormat(noa);
+ }
+
+ @Override
+ public void onUpdate(Policy policy) {
+ validateConfig(policy);
+ }
+
+ @Override
+ public void onRemove(Policy policy) {
+ }
+
+ private void validateFormat(String date) {
+ try {
+ new SimpleDateFormat(TimePolicyProvider.DEFAULT_DATE_PATTERN).parse(TimePolicyProvider.format(date));
+ } catch (Exception e) {
+ throw new RuntimeException("Could not parse a date using format [" + date + "]");
+ }
+ }
+}
diff --git a/authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java b/authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java
new file mode 100644
index 0000000..dc6af9f
--- /dev/null
+++ b/authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProvider.java
@@ -0,0 +1,86 @@
+/*
+ * 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.authorization.policy.provider.time;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class TimePolicyProvider implements PolicyProvider {
+
+ static String DEFAULT_DATE_PATTERN = "yyyy-MM-dd hh:mm:ss";
+
+ private final Policy policy;
+ private final SimpleDateFormat dateFormat;
+ private final Date currentDate;
+
+ public TimePolicyProvider(Policy policy) {
+ this.policy = policy;
+ this.dateFormat = new SimpleDateFormat(DEFAULT_DATE_PATTERN);
+ this.currentDate = new Date();
+ }
+
+ @Override
+ public void evaluate(Evaluation evaluation) {
+ try {
+ String notBefore = this.policy.getConfig().get("nbf");
+
+ if (notBefore != null) {
+
+ if (this.currentDate.before(this.dateFormat.parse(format(notBefore)))) {
+ evaluation.deny();
+ return;
+ }
+ }
+
+ String notOnOrAfter = this.policy.getConfig().get("noa");
+
+ if (notOnOrAfter != null) {
+ if (this.currentDate.after(this.dateFormat.parse(format(notOnOrAfter)))) {
+ evaluation.deny();
+ return;
+ }
+ }
+
+ evaluation.grant();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not evaluate time-based policy [" + this.policy.getName() + "].", e);
+ }
+ }
+
+ static String format(String notBefore) {
+ String trimmed = notBefore.trim();
+
+ if (trimmed.length() == 10) {
+ notBefore = trimmed + " 00:00:00";
+ }
+
+ return notBefore;
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java b/authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java
new file mode 100644
index 0000000..efe3cd2
--- /dev/null
+++ b/authz/policy/time/src/main/java/org/keycloak/authorization/policy/provider/time/TimePolicyProviderFactory.java
@@ -0,0 +1,62 @@
+package org.keycloak.authorization.policy.provider.time;
+
+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.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;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class TimePolicyProviderFactory implements PolicyProviderFactory {
+
+ @Override
+ public String getName() {
+ return "Time";
+ }
+
+ @Override
+ public String getGroup() {
+ return "Time Based";
+ }
+
+ @Override
+ public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
+ return new TimePolicyProvider(policy);
+ }
+
+ @Override
+ public PolicyProviderAdminService getAdminResource(ResourceServer resourceServer) {
+ return new TimePolicyAdminResource();
+ }
+
+ @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 "time";
+ }
+}
diff --git a/authz/policy/time/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/time/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
new file mode 100644
index 0000000..7f1ab27
--- /dev/null
+++ b/authz/policy/time/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory
\ No newline at end of file
authz/policy/user/pom.xml 51(+51 -0)
diff --git a/authz/policy/user/pom.xml b/authz/policy/user/pom.xml
new file mode 100644
index 0000000..fd5f598
--- /dev/null
+++ b/authz/policy/user/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-provider-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-policy-user</artifactId>
+ <packaging>jar</packaging>
+
+ <name>KeyCloak AuthZ: User Policy Provider</name>
+ <description>KeyCloak AuthZ: User Policy Provider</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-server-spi</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/authz/policy/user/src/main/java/org/keycloak/authorization/policy/provider/identity/UserPolicyProvider.java b/authz/policy/user/src/main/java/org/keycloak/authorization/policy/provider/identity/UserPolicyProvider.java
new file mode 100644
index 0000000..6416d06
--- /dev/null
+++ b/authz/policy/user/src/main/java/org/keycloak/authorization/policy/provider/identity/UserPolicyProvider.java
@@ -0,0 +1,60 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.policy.provider.identity;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.evaluation.EvaluationContext;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+
+import static org.keycloak.authorization.policy.provider.identity.UserPolicyProviderFactory.getUsers;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UserPolicyProvider implements PolicyProvider {
+
+ private final Policy policy;
+
+ public UserPolicyProvider(Policy policy) {
+ this.policy = policy;
+ }
+
+ @Override
+ public void evaluate(Evaluation evaluation) {
+ EvaluationContext context = evaluation.getContext();
+ String[] userIds = getUsers(this.policy);
+
+ if (userIds.length > 0) {
+ for (String userId : userIds) {
+ if (context.getIdentity().getId().equals(userId)) {
+ evaluation.grant();
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/authz/policy/user/src/main/java/org/keycloak/authorization/policy/provider/identity/UserPolicyProviderFactory.java b/authz/policy/user/src/main/java/org/keycloak/authorization/policy/provider/identity/UserPolicyProviderFactory.java
new file mode 100644
index 0000000..3cd8bb3
--- /dev/null
+++ b/authz/policy/user/src/main/java/org/keycloak/authorization/policy/provider/identity/UserPolicyProviderFactory.java
@@ -0,0 +1,132 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.provider.identity;
+
+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.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserModel.UserRemovedEvent;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UserPolicyProviderFactory implements PolicyProviderFactory {
+
+ @Override
+ public String getName() {
+ return "User-Based";
+ }
+
+ @Override
+ public String getGroup() {
+ return "Identity Based";
+ }
+
+ @Override
+ public PolicyProvider create(Policy policy, AuthorizationProvider authorization) {
+ return new UserPolicyProvider(policy);
+ }
+
+ @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) {
+ factory.register(event -> {
+ if (event instanceof UserRemovedEvent) {
+ KeycloakSession keycloakSession = ((UserRemovedEvent) event).getKeycloakSession();
+ AuthorizationProvider provider = keycloakSession.getProvider(AuthorizationProvider.class);
+ PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
+ UserModel removedUser = ((UserRemovedEvent) event).getUser();
+
+ policyStore.findByType(getId()).forEach(policy -> {
+ List<String> users = new ArrayList<>();
+
+ for (String userId : getUsers(policy)) {
+ if (!userId.equals(removedUser.getId())) {
+ users.add(userId);
+ }
+ }
+
+ try {
+ if (users.isEmpty()) {
+ policyStore.findDependentPolicies(policy.getId()).forEach(dependentPolicy -> {
+ dependentPolicy.removeAssociatedPolicy(policy);
+ });
+ policyStore.delete(policy.getId());
+ } else {
+ policy.getConfig().put("users", JsonSerialization.writeValueAsString(users));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Error while synchronizing users with policy [" + policy.getName() + "].", e);
+ }
+ });
+ }
+ });
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "user";
+ }
+
+ static String[] getUsers(Policy policy) {
+ String roles = policy.getConfig().get("users");
+
+ if (roles != null) {
+ try {
+ return JsonSerialization.readValue(roles.getBytes(), String[].class);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not parse roles [" + roles + "] from policy config [" + policy.getName() + ".", e);
+ }
+ }
+
+ return new String[]{};
+ }
+}
diff --git a/authz/policy/user/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/user/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
new file mode 100644
index 0000000..33608ee
--- /dev/null
+++ b/authz/policy/user/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.policy.provider.identity.UserPolicyProviderFactory
\ No newline at end of file
authz/pom.xml 30(+30 -0)
diff --git a/authz/pom.xml b/authz/pom.xml
new file mode 100644
index 0000000..6616d04
--- /dev/null
+++ b/authz/pom.xml
@@ -0,0 +1,30 @@
+<?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/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <name>KeyCloak Authz: Parent</name>
+ <description>KeyCloak AuthZ: Parent</description>
+
+ <modules>
+ <module>policy</module>
+ <module>client</module>
+ </modules>
+
+ <properties>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ </properties>
+
+</project>
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/AuthorizationContext.java b/core/src/main/java/org/keycloak/AuthorizationContext.java
new file mode 100644
index 0000000..4aa5503
--- /dev/null
+++ b/core/src/main/java/org/keycloak/AuthorizationContext.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;
+
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
+import org.keycloak.representations.authorization.Permission;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationContext {
+
+ private final AccessToken authzToken;
+ private final List<PathConfig> paths;
+ private boolean granted;
+
+ public AuthorizationContext(AccessToken authzToken, List<PathConfig> paths) {
+ this.authzToken = authzToken;
+ this.paths = paths;
+ this.granted = true;
+ }
+
+ public AuthorizationContext() {
+ this(null, null);
+ this.granted = false;
+ }
+
+ public boolean hasPermission(String resourceName, String scopeName) {
+ for (Permission permission : authzToken.getAuthorization().getPermissions()) {
+ for (PathConfig pathHolder : this.paths) {
+ if (pathHolder.getName().equals(resourceName)) {
+ if (pathHolder.getId().equals(permission.getResourceSetId())) {
+ if (permission.getScopes().contains(scopeName)) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public boolean hasResourcePermission(String resourceName) {
+ for (Permission permission : authzToken.getAuthorization().getPermissions()) {
+ for (PathConfig pathHolder : this.paths) {
+ if (pathHolder.getName().equals(resourceName)) {
+ if (pathHolder.getId().equals(permission.getResourceSetId())) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public boolean hasScopePermission(String scopeName) {
+ for (Permission permission : authzToken.getAuthorization().getPermissions()) {
+ if (permission.getScopes().contains(scopeName)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public List<Permission> getPermissions() {
+ return this.authzToken.getAuthorization().getPermissions();
+ }
+
+ public boolean isGranted() {
+ return granted;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
index 118fd1b..68ac957 100755
--- a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
+++ b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
@@ -41,6 +41,7 @@ public class KeycloakSecurityContext implements Serializable {
// Don't store parsed tokens into HTTP session
protected transient AccessToken token;
protected transient IDToken idToken;
+ protected transient AuthorizationContext authorizationContext;
public KeycloakSecurityContext() {
}
@@ -60,6 +61,10 @@ public class KeycloakSecurityContext implements Serializable {
return tokenString;
}
+ public AuthorizationContext getAuthorizationContext() {
+ return authorizationContext;
+ }
+
public IDToken getIdToken() {
return idToken;
}
diff --git a/core/src/main/java/org/keycloak/representations/AccessToken.java b/core/src/main/java/org/keycloak/representations/AccessToken.java
index ac800cd..7d7fdea 100755
--- a/core/src/main/java/org/keycloak/representations/AccessToken.java
+++ b/core/src/main/java/org/keycloak/representations/AccessToken.java
@@ -19,10 +19,12 @@ package org.keycloak.representations;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.representations.authorization.Permission;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -81,6 +83,20 @@ public class AccessToken extends IDToken {
}
}
+ public static class Authorization implements Serializable {
+
+ @JsonProperty("permissions")
+ private List<Permission> permissions;
+
+ public List<Permission> getPermissions() {
+ return permissions;
+ }
+
+ public void setPermissions(List<Permission> permissions) {
+ this.permissions = permissions;
+ }
+ }
+
@JsonProperty("client_session")
protected String clientSession;
@@ -96,6 +112,9 @@ public class AccessToken extends IDToken {
@JsonProperty("resource_access")
protected Map<String, Access> resourceAccess = new HashMap<String, Access>();
+ @JsonProperty("authorization")
+ protected Authorization authorization;
+
public Map<String, Access> getResourceAccess() {
return resourceAccess;
}
@@ -219,5 +238,11 @@ public class AccessToken extends IDToken {
return (AccessToken)super.issuedFor(issuedFor);
}
+ public Authorization getAuthorization() {
+ return authorization;
+ }
+ public void setAuthorization(Authorization authorization) {
+ this.authorization = authorization;
+ }
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
index d226099..91fb5f0 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
@@ -36,7 +36,8 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
"client-keystore", "client-keystore-password", "client-key-password",
"always-refresh-token",
"register-node-at-startup", "register-node-period", "token-store", "principal-attribute",
- "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live"
+ "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live",
+ "policy-enforcer"
})
public class AdapterConfig extends BaseAdapterConfig {
@@ -70,6 +71,8 @@ public class AdapterConfig extends BaseAdapterConfig {
protected Boolean turnOffChangeSessionIdOnLogin;
@JsonProperty("token-minimum-time-to-live")
protected int tokenMinimumTimeToLive = 0;
+ @JsonProperty("policy-enforcer")
+ protected PolicyEnforcerConfig policyEnforcerConfig;
/**
* The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}.
@@ -189,6 +192,14 @@ public class AdapterConfig extends BaseAdapterConfig {
this.turnOffChangeSessionIdOnLogin = turnOffChangeSessionIdOnLogin;
}
+ public PolicyEnforcerConfig getPolicyEnforcerConfig() {
+ return policyEnforcerConfig;
+ }
+
+ public void setPolicyEnforcerConfig(PolicyEnforcerConfig policyEnforcerConfig) {
+ this.policyEnforcerConfig = policyEnforcerConfig;
+ }
+
public String getProxyUrl() {
return proxyUrl;
}
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
new file mode 100644
index 0000000..98a4045
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -0,0 +1,209 @@
+/*
+ * 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.adapters.config;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyEnforcerConfig {
+
+ @JsonProperty("create-resources")
+ private Boolean createResources;
+
+ @JsonProperty("enforcement-mode")
+ private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
+
+ @JsonProperty("user-managed-access")
+ private UmaProtocolConfig umaProtocolConfig;
+
+ @JsonProperty("entitlement")
+ private EntitlementProtocolConfig entitlementProtocolConfig;
+
+ @JsonProperty("paths")
+ private List<PathConfig> paths = new ArrayList<>();
+
+ @JsonProperty("online-introspection")
+ private Boolean onlineIntrospection;
+
+ @JsonProperty("on-deny-redirect-to")
+ private String accessDeniedPath;
+
+ public Boolean isCreateResources() {
+ return this.createResources;
+ }
+
+ public List<PathConfig> getPaths() {
+ if (this.paths == null) {
+ return null;
+ }
+
+ return Collections.unmodifiableList(this.paths);
+ }
+
+ public EnforcementMode getEnforcementMode() {
+ return this.enforcementMode;
+ }
+
+ public void setEnforcementMode(EnforcementMode enforcementMode) {
+ this.enforcementMode = enforcementMode;
+ }
+
+ public UmaProtocolConfig getUmaProtocolConfig() {
+ return this.umaProtocolConfig;
+ }
+
+ public EntitlementProtocolConfig getEntitlementProtocolConfig() {
+ return this.entitlementProtocolConfig;
+ }
+
+ public Boolean isOnlineIntrospection() {
+ return onlineIntrospection;
+ }
+
+ public void setPaths(List<PathConfig> paths) {
+ this.paths = paths;
+ }
+
+ public String getAccessDeniedPath() {
+ return accessDeniedPath;
+ }
+
+ public static class PathConfig {
+
+ private String name;
+ private String type;
+ private String path;
+ private List<MethodConfig> methods = new ArrayList<>();
+ private List<String> scopes = Collections.emptyList();
+ private String id;
+ private boolean instance;
+
+ public String getPath() {
+ return this.path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public List<String> getScopes() {
+ return this.scopes;
+ }
+
+ public void setScopes(List<String> scopes) {
+ this.scopes = scopes;
+ }
+
+ public List<MethodConfig> getMethods() {
+ return methods;
+ }
+
+ public void setMethods(List<MethodConfig> methods) {
+ this.methods = methods;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public String toString() {
+ return "PathConfig{" +
+ "name='" + name + '\'' +
+ ", type='" + type + '\'' +
+ ", path='" + path + '\'' +
+ ", scopes=" + scopes +
+ ", id='" + id + '\'' +
+ '}';
+ }
+
+ public boolean hasPattern() {
+ return getPath().indexOf("{") != -1;
+ }
+
+ public boolean isInstance() {
+ return instance;
+ }
+
+ public void setInstance(boolean instance) {
+ this.instance = instance;
+ }
+ }
+
+ public static class MethodConfig {
+
+ private String method;
+ private List<String> scopes = Collections.emptyList();
+
+ public String getMethod() {
+ return method;
+ }
+
+ public void setMethod(String method) {
+ this.method = method;
+ }
+
+ public List<String> getScopes() {
+ return scopes;
+ }
+
+ public void setScopes(List<String> scopes) {
+ this.scopes = scopes;
+ }
+ }
+
+ public enum EnforcementMode {
+ PERMISSIVE,
+ ENFORCING,
+ DISABLED
+ }
+
+ public static class UmaProtocolConfig {
+
+ }
+
+ public static class EntitlementProtocolConfig {
+
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/authorization/Permission.java b/core/src/main/java/org/keycloak/representations/authorization/Permission.java
new file mode 100644
index 0000000..1daba20
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/authorization/Permission.java
@@ -0,0 +1,72 @@
+/*
+ * 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.authorization;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class Permission {
+
+ @JsonProperty("resource_set_id")
+ private String resourceSetId;
+
+ @JsonProperty("resource_set_name")
+ private final String resourceSetName;
+
+ private Set<String> scopes;
+
+ public Permission() {
+ this(null, null, null);
+ }
+
+ public Permission(final String resourceSetId, String resourceSetName, final Set<String> scopes) {
+ this.resourceSetId = resourceSetId;
+ this.resourceSetName = resourceSetName;
+ this.scopes = scopes;
+ }
+
+ public String getResourceSetId() {
+ return this.resourceSetId;
+ }
+
+ public String getResourceSetName() {
+ return this.resourceSetName;
+ }
+
+ public Set<String> getScopes() {
+ return this.scopes;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("Permission {").append("id=").append(resourceSetId).append(", name=").append(resourceSetName)
+ .append(", scopes=").append(scopes).append("}");
+
+ return builder.toString();
+ }
+
+ public void setScopes(Set<String> scopes) {
+ this.scopes = scopes;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
index a5b3802..f18f4eb 100755
--- a/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/ClientRepresentation.java
@@ -47,6 +47,7 @@ public class ClientRepresentation {
protected Boolean implicitFlowEnabled;
protected Boolean directAccessGrantsEnabled;
protected Boolean serviceAccountsEnabled;
+ protected Boolean authorizationServicesEnabled;
@Deprecated
protected Boolean directGrantsOnly;
protected Boolean publicClient;
@@ -239,6 +240,14 @@ public class ClientRepresentation {
this.serviceAccountsEnabled = serviceAccountsEnabled;
}
+ public Boolean getAuthorizationServicesEnabled() {
+ return authorizationServicesEnabled;
+ }
+
+ public void setAuthorizationServicesEnabled(Boolean authorizationServicesEnabled) {
+ this.authorizationServicesEnabled = authorizationServicesEnabled;
+ }
+
@Deprecated
public Boolean isDirectGrantsOnly() {
return directGrantsOnly;
dependencies/server-all/pom.xml 280(+280 -0)
diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml
index 75bda3e..f989be1 100755
--- a/dependencies/server-all/pom.xml
+++ b/dependencies/server-all/pom.xml
@@ -85,6 +85,286 @@
</exclusion>
</exclusions>
</dependency>
+
+ <!-- Built-in Authorization Policy Providers -->
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-resource</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-scope</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-user</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-role</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-js</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-time</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-aggregate</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <!-- Built-in Authorization Drools Policy Provider-->
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-drools</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <!-- Drools -->
+ <dependency>
+ <groupId>org.kie</groupId>
+ <artifactId>kie-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kie</groupId>
+ <artifactId>kie-ci</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.kie</groupId>
+ <artifactId>kie-internal</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.drools</groupId>
+ <artifactId>drools-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.drools</groupId>
+ <artifactId>drools-compiler</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.aether</groupId>
+ <artifactId>aether-api</artifactId>
+ <version>1.0.0.v20140518</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.aether</groupId>
+ <artifactId>aether-connector-basic</artifactId>
+ <version>1.0.0.v20140518</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.aether</groupId>
+ <artifactId>aether-spi</artifactId>
+ <version>1.0.0.v20140518</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.aether</groupId>
+ <artifactId>aether-impl</artifactId>
+ <version>1.0.0.v20140518</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.aether</groupId>
+ <artifactId>aether-transport-file</artifactId>
+ <version>1.0.0.v20140518</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.aether</groupId>
+ <artifactId>aether-transport-http</artifactId>
+ <version>1.0.0.v20140518</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.aether</groupId>
+ <artifactId>aether-transport-wagon</artifactId>
+ <version>1.0.0.v20140518</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.aether</groupId>
+ <artifactId>aether-util</artifactId>
+ <version>1.0.0.v20140518</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ant</groupId>
+ <artifactId>ant</artifactId>
+ <version>1.8.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.ant</groupId>
+ <artifactId>ant-launcher</artifactId>
+ <version>1.8.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.antlr</groupId>
+ <artifactId>antlr-runtime</artifactId>
+ <version>3.5</version>
+ </dependency>
+ <dependency>
+ <groupId>aopalliance</groupId>
+ <artifactId>aopalliance</artifactId>
+ <version>1.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-aether-provider</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-artifact</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-compat</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-core</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-model</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-model-builder</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-plugin-api</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-repository-metadata</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-settings</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven</groupId>
+ <artifactId>maven-settings-builder</artifactId>
+ <version>3.2.2</version>
+ </dependency>
+ <dependency>
+ <groupId>org.mvel</groupId>
+ <artifactId>mvel2</artifactId>
+ <version>2.2.4.Final</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.sisu</groupId>
+ <artifactId>org.eclipse.sisu.inject</artifactId>
+ <version>0.0.0.M5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.sisu</groupId>
+ <artifactId>org.eclipse.sisu.plexus</artifactId>
+ <version>0.0.0.M5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.sonatype.plexus</groupId>
+ <artifactId>plexus-cipher</artifactId>
+ <version>1.4</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.plexus</groupId>
+ <artifactId>plexus-classworlds</artifactId>
+ <version>2.5.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.plexus</groupId>
+ <artifactId>plexus-component-annotations</artifactId>
+ <version>1.5.5</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.plexus</groupId>
+ <artifactId>plexus-interpolation</artifactId>
+ <version>1.19</version>
+ </dependency>
+ <dependency>
+ <groupId>org.sonatype.plexus</groupId>
+ <artifactId>plexus-sec-dispatcher</artifactId>
+ <version>1.3</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.plexus</groupId>
+ <artifactId>plexus-utils</artifactId>
+ <version>3.0.17</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.wagon</groupId>
+ <artifactId>wagon-http</artifactId>
+ <version>2.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.wagon</groupId>
+ <artifactId>wagon-http-shared4</artifactId>
+ <version>2.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.maven.wagon</groupId>
+ <artifactId>wagon-provider-api</artifactId>
+ <version>1.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.thoughtworks.xstream</groupId>
+ <artifactId>xstream</artifactId>
+ <version>1.4.7</version>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>13.0.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jdt.core.compiler</groupId>
+ <artifactId>ecj</artifactId>
+ <version>4.3.1</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>4.3.6</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore</artifactId>
+ <version>4.3.3</version>
+ </dependency>
+ <dependency>
+ <groupId>com.lowagie</groupId>
+ <artifactId>itext</artifactId>
+ <version>2.1.2</version>
+ <exclusions>
+ <exclusion>
+ <groupId>bouncycastle</groupId>
+ <artifactId>bcmail-jdk14</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>bouncycastle</groupId>
+ <artifactId>bcprov-jdk14</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.sonatype.sisu</groupId>
+ <artifactId>sisu-guice</artifactId>
+ <version>3.1.0</version>
+ </dependency>
</dependencies>
</project>
diff --git a/distribution/adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml b/distribution/adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml
index 8da890a..ece320b 100755
--- a/distribution/adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml
+++ b/distribution/adapters/wildfly-adapter/wildfly-adapter-zip/assembly.xml
@@ -38,6 +38,9 @@
<include>org/keycloak/keycloak-wildfly-subsystem/**</include>
<include>org/keycloak/keycloak-adapter-subsystem/**</include>
<include>org/keycloak/keycloak-servlet-oauth-client/**</include>
+
+ <!-- Authorization -->
+ <include>org/keycloak/keycloak-authz-client/**</include>
</includes>
<excludes>
<exclude>**/*.war</exclude>
diff --git a/distribution/adapters/wildfly-adapter/wildfly-modules/build.xml b/distribution/adapters/wildfly-adapter/wildfly-modules/build.xml
index 4597205..a534b4f 100755
--- a/distribution/adapters/wildfly-adapter/wildfly-modules/build.xml
+++ b/distribution/adapters/wildfly-adapter/wildfly-modules/build.xml
@@ -73,6 +73,10 @@
<maven-resource group="org.keycloak" artifact="keycloak-servlet-oauth-client"/>
</module-def>
+ <!-- Authorization -->
+ <module-def name="org.keycloak.keycloak-authz-client">
+ <maven-resource group="org.keycloak" artifact="keycloak-authz-client"/>
+ </module-def>
</target>
<target name="clean-target">
diff --git a/distribution/adapters/wildfly-adapter/wildfly-modules/pom.xml b/distribution/adapters/wildfly-adapter/wildfly-modules/pom.xml
index 4fb605e..18783ec 100755
--- a/distribution/adapters/wildfly-adapter/wildfly-modules/pom.xml
+++ b/distribution/adapters/wildfly-adapter/wildfly-modules/pom.xml
@@ -82,6 +82,12 @@
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
+
+ <!-- Authorization -->
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-client</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml b/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml
index 8672bf4..84a08f0 100755
--- a/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml
+++ b/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml
@@ -34,6 +34,7 @@
<module name="org.keycloak.keycloak-adapter-spi"/>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-authz-client"/>
</dependencies>
</module>
diff --git a/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml b/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml
new file mode 100755
index 0000000..3cd1abd
--- /dev/null
+++ b/distribution/adapters/wildfly-adapter/wildfly-modules/src/main/resources/modules/org/keycloak/keycloak-authz-client/main/module.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-authz-client">
+ <resources>
+ <!-- Insert resources here -->
+ </resources>
+ <dependencies>
+ <module name="org.bouncycastle" />
+ <module name="javax.api"/>
+ <module name="javax.activation.api"/>
+ <module name="sun.jdk" optional="true" />
+ <module name="javax.ws.rs.api"/>
+ <module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-common"/>
+ <module name="org.apache.httpcomponents"/>
+ <module name="com.fasterxml.jackson.core.jackson-core"/>
+ <module name="com.fasterxml.jackson.core.jackson-annotations"/>
+ <module name="com.fasterxml.jackson.core.jackson-databind"/>
+ <module name="com.fasterxml.jackson.jaxrs.jackson-jaxrs-json-provider"/>
+ </dependencies>
+
+</module>
distribution/examples-dist/build.xml 8(+8 -0)
diff --git a/distribution/examples-dist/build.xml b/distribution/examples-dist/build.xml
index 7714802..6e8b9bc 100755
--- a/distribution/examples-dist/build.xml
+++ b/distribution/examples-dist/build.xml
@@ -136,6 +136,14 @@
<exclude name="**/*.iml"/>
</fileset>
</copy>
+ <copy todir="target/examples/authz" overwrite="true">
+ <fileset dir="../../examples/authz">
+ <exclude name="**/target/**"/>
+ <exclude name="**/*.iml"/>
+ <exclude name="**/*.unconfigured"/>
+ <exclude name="**/subsystem-config.xml"/>
+ </fileset>
+ </copy>
<copy file="../../examples/pom.xml" tofile="target/examples/pom.xml"/>
<copy file="../../examples/README.md" tofile="target/examples/README.md"/>
<move file="target/examples/unconfigured-demo/README.md.unconfigured" tofile="target/examples/unconfigured-demo/README.md"/>
diff --git a/distribution/feature-packs/server-feature-pack/pom.xml b/distribution/feature-packs/server-feature-pack/pom.xml
index 82f2f74..4906417 100644
--- a/distribution/feature-packs/server-feature-pack/pom.xml
+++ b/distribution/feature-packs/server-feature-pack/pom.xml
@@ -51,6 +51,13 @@
<groupId>org.wildfly</groupId>
<artifactId>wildfly-feature-pack</artifactId>
<type>zip</type>
+ <!-- Need to exlcude that in order to use the right guava version for drools -->
+ <exclusions>
+ <exclusion>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
</dependencies>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json
index 77a3f68..89263bf 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json
@@ -32,6 +32,10 @@
"provider": "jpa"
},
+ "authorizationPersister": {
+ "provider": "jpa"
+ },
+
"timer": {
"provider": "basic"
},
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/web.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/web.xml
index 6f19080..6ad93e9 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/web.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/web.xml
@@ -45,6 +45,7 @@
<filter>
<filter-name>Keycloak Session Management</filter-name>
<filter-class>org.keycloak.services.filters.KeycloakSessionServletFilter</filter-class>
+ <async-supported>true</async-supported>
</filter>
<filter-mapping>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml
index ca24001..dea3d84 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-services/main/module.xml
@@ -44,6 +44,9 @@
<module name="org.keycloak.keycloak-services" export="true" services="import"/>
<module name="org.keycloak.keycloak-wildfly-extensions" services="import"/>
+ <!-- Authorization -->
+ <module name="org.keycloak.keycloak-authz-server" services="import"/>
+
<module name="org.freemarker"/>
<module name="javax.ws.rs.api"/>
<module name="javax.mail.api"/>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/drools/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/drools/main/module.xml
new file mode 100644
index 0000000..42887c0
--- /dev/null
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/drools/main/module.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<module xmlns="urn:jboss:module:1.3" name="org.drools">
+ <resources>
+ <artifact name="${org.kie:kie-api}"/>
+ <artifact name="${org.kie:kie-ci}"/>
+ <artifact name="${org.kie:kie-internal}"/>
+ <artifact name="${org.drools:drools-core}"/>
+ <artifact name="${org.drools:drools-compiler}"/>
+ <artifact name="${org.eclipse.aether:aether-api}"/>
+ <artifact name="${org.eclipse.aether:aether-connector-basic}"/>
+ <artifact name="${org.eclipse.aether:aether-spi}"/>
+ <artifact name="${org.eclipse.aether:aether-impl}"/>
+ <artifact name="${org.eclipse.aether:aether-transport-file}"/>
+ <artifact name="${org.eclipse.aether:aether-transport-http}"/>
+ <artifact name="${org.eclipse.aether:aether-transport-wagon}"/>
+ <artifact name="${org.eclipse.aether:aether-util}"/>
+ <artifact name="${org.apache.ant:ant}"/>
+ <artifact name="${org.apache.ant:ant-launcher}"/>
+ <artifact name="${org.antlr:antlr-runtime}"/>
+ <artifact name="${aopalliance:aopalliance}"/>
+ <artifact name="${org.apache.maven:maven-aether-provider}"/>
+ <artifact name="${org.apache.maven:maven-artifact}"/>
+ <artifact name="${org.apache.maven:maven-compat}"/>
+ <artifact name="${org.apache.maven:maven-core}"/>
+ <artifact name="${org.apache.maven:maven-model}"/>
+ <artifact name="${org.apache.maven:maven-model-builder}"/>
+ <artifact name="${org.apache.maven:maven-plugin-api}"/>
+ <artifact name="${org.apache.maven:maven-repository-metadata}"/>
+ <artifact name="${org.apache.maven:maven-settings}"/>
+ <artifact name="${org.apache.maven:maven-settings-builder}"/>
+ <artifact name="${org.mvel:mvel2}"/>
+ <artifact name="${org.eclipse.sisu:org.eclipse.sisu.inject}"/>
+ <artifact name="${org.eclipse.sisu:org.eclipse.sisu.plexus}"/>
+ <artifact name="${org.sonatype.plexus:plexus-cipher}"/>
+ <artifact name="${org.codehaus.plexus:plexus-classworlds}"/>
+ <artifact name="${org.codehaus.plexus:plexus-component-annotations}"/>
+ <artifact name="${org.codehaus.plexus:plexus-interpolation}"/>
+ <artifact name="${org.sonatype.plexus:plexus-sec-dispatcher}"/>
+ <artifact name="${org.codehaus.plexus:plexus-utils}"/>
+ <artifact name="${org.apache.maven.wagon:wagon-http}"/>
+ <artifact name="${org.apache.maven.wagon:wagon-http-shared4}"/>
+ <artifact name="${org.apache.maven.wagon:wagon-provider-api}"/>
+ <artifact name="${com.thoughtworks.xstream:xstream}"/>
+ <artifact name="${com.google.guava:guava}"/>
+ <artifact name="${org.eclipse.jdt.core.compiler:ecj}"/>
+ <artifact name="${org.apache.httpcomponents:httpclient}"/>
+ <artifact name="${org.apache.httpcomponents:httpcore}"/>
+ <artifact name="${com.lowagie:itext}"/>
+ <artifact name="${org.sonatype.sisu:sisu-guice}"/>
+ </resources>
+ <dependencies>
+ <module name="javax.api"/>
+ <module name="javax.inject.api"/>
+ <module name="javax.enterprise.api"/>
+ <module name="org.slf4j"/>
+ <module name="org.apache.commons.logging"/>
+ <module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-common"/>
+ <module name="org.keycloak.keycloak-server-spi"/>
+ </dependencies>
+
+</module>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-policy-drools/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-policy-drools/main/module.xml
new file mode 100644
index 0000000..d821674
--- /dev/null
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-policy-drools/main/module.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-authz-policy-drools">
+ <resources>
+ <artifact name="${org.keycloak:keycloak-authz-policy-drools}"/>
+ </resources>
+ <dependencies>
+ <module name="javax.api"/>
+ <module name="javax.ws.rs.api"/>
+ <module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-common"/>
+ <module name="org.keycloak.keycloak-server-spi"/>
+ <module name="org.keycloak.keycloak-services"/>
+ <module name="org.drools"/>
+ </dependencies>
+</module>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-policy-provider/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-policy-provider/main/module.xml
new file mode 100644
index 0000000..5500a82
--- /dev/null
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-policy-provider/main/module.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-authz-policy-provider">
+ <resources>
+ <artifact name="${org.keycloak:keycloak-authz-policy-aggregate}"/>
+ <artifact name="${org.keycloak:keycloak-authz-policy-js}"/>
+ <artifact name="${org.keycloak:keycloak-authz-policy-resource}"/>
+ <artifact name="${org.keycloak:keycloak-authz-policy-role}"/>
+ <artifact name="${org.keycloak:keycloak-authz-policy-scope}"/>
+ <artifact name="${org.keycloak:keycloak-authz-policy-time}"/>
+ <artifact name="${org.keycloak:keycloak-authz-policy-user}"/>
+ </resources>
+ <dependencies>
+ <module name="javax.api"/>
+ <module name="javax.ws.rs.api"/>
+ <module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-common"/>
+ <module name="org.keycloak.keycloak-server-spi"/>
+ <module name="org.keycloak.keycloak-services"/>
+ </dependencies>
+
+</module>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-server/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-server/main/module.xml
new file mode 100644
index 0000000..ab9ef98
--- /dev/null
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak-authz/org/keycloak/keycloak-authz-server/main/module.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-authz-server">
+ <dependencies>
+ <module name="org.keycloak.keycloak-authz-policy-provider" export="true" services="export"/>
+ <module name="org.keycloak.keycloak-authz-policy-drools" export="true" services="export"/>
+ </dependencies>
+</module>
diff --git a/distribution/server-dist/src/main/modules/layers.conf b/distribution/server-dist/src/main/modules/layers.conf
index 74f4485..afb3024 100644
--- a/distribution/server-dist/src/main/modules/layers.conf
+++ b/distribution/server-dist/src/main/modules/layers.conf
@@ -1 +1 @@
-layers=keycloak
\ No newline at end of file
+layers=keycloak,keycloak-authz
\ No newline at end of file
diff --git a/distribution/server-overlay/assembly.xml b/distribution/server-overlay/assembly.xml
index 532308c..3c6c25d 100755
--- a/distribution/server-overlay/assembly.xml
+++ b/distribution/server-overlay/assembly.xml
@@ -40,6 +40,14 @@
<include>sun/jdk/jgss/**</include>
</includes>
</fileSet>
+ <!-- Authorization -->
+ <fileSet>
+ <directory>${project.build.directory}/unpacked/keycloak-${project.version}/modules/system/layers/keycloak-authz</directory>
+ <outputDirectory>modules/system/add-ons/keycloak-authz</outputDirectory>
+ <includes>
+ <include>**/**</include>
+ </includes>
+ </fileSet>
<fileSet>
<directory>${project.build.directory}/unpacked/keycloak-${project.version}/content</directory>
<outputDirectory></outputDirectory>
diff --git a/docbook/auth-server-docs/reference/en/en-US/master.xml b/docbook/auth-server-docs/reference/en/en-US/master.xml
index a44b126..0111e58 100755
--- a/docbook/auth-server-docs/reference/en/en-US/master.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/master.xml
@@ -157,6 +157,7 @@ This one is short
&Groups;
&DirectAccess;
&ServiceAccounts;
+ &FineGrainedAuthorization;
&CORS;
&Timeouts;
&AdminApi;
diff --git a/examples/authz/hello-world/hello-world-authz-realm.json b/examples/authz/hello-world/hello-world-authz-realm.json
new file mode 100644
index 0000000..a263c69
--- /dev/null
+++ b/examples/authz/hello-world/hello-world-authz-realm.json
@@ -0,0 +1,45 @@
+{
+ "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",
+ "requiredCredentials" : [ "password" ],
+ "users" :
+ [
+ {
+ "username" : "alice",
+ "enabled" : true,
+ "credentials" : [ {
+ "type" : "password",
+ "value" : "password"
+ } ]
+ },
+ {
+ "username" : "jdoe",
+ "enabled" : true,
+ "credentials" : [ {
+ "type" : "password",
+ "value" : "password"
+ } ]
+ },
+ {
+ "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" ],
+ "directAccessGrantsEnabled" : true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/authz/hello-world/hello-world-authz-service.json b/examples/authz/hello-world/hello-world-authz-service.json
new file mode 100644
index 0000000..24bd27f
--- /dev/null
+++ b/examples/authz/hello-world/hello-world-authz-service.json
@@ -0,0 +1,25 @@
+{
+ "resources": [
+ {
+ "name": "Hello World Resource"
+ }
+ ],
+ "policies": [
+ {
+ "name": "Only Special Users Policy",
+ "type": "user",
+ "logic": "POSITIVE",
+ "config": {
+ "users": "[\"alice\"]"
+ }
+ },
+ {
+ "name": "Hello World Resource Permission",
+ "type": "resource",
+ "config": {
+ "resources": "[\"Hello World Resource\"]",
+ "applyPolicies": "[\"Only Special Users Policy\"]"
+ }
+ }
+ ]
+}
\ No newline at end of file
examples/authz/hello-world/pom.xml 43(+43 -0)
diff --git a/examples/authz/hello-world/pom.xml b/examples/authz/hello-world/pom.xml
new file mode 100755
index 0000000..e30d381
--- /dev/null
+++ b/examples/authz/hello-world/pom.xml
@@ -0,0 +1,43 @@
+<?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</groupId>
+ <artifactId>keycloak-authz-example-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-hello-world</artifactId>
+ <packaging>pom</packaging>
+
+ <name>Keycloak Authz: Hello World Example</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+</project>
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
new file mode 100644
index 0000000..75ee0d3
--- /dev/null
+++ b/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java
@@ -0,0 +1,162 @@
+/*
+ * 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.authz.helloworld;
+
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.representation.EntitlementRequest;
+import org.keycloak.authorization.client.representation.EntitlementResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.authorization.client.representation.RegistrationResponse;
+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 java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationClientExample {
+
+ public static void main(String[] args) {
+ obtainEntitlementsForResource();
+ obtainAllEntitlements();
+ createResource();
+ introspectRequestingPartyToken();
+ }
+
+ private static void introspectRequestingPartyToken() {
+ // create a new instance based on the configuration define at keycloak-authz.json
+ AuthzClient authzClient = AuthzClient.create();
+
+ // query the server for a resource with a given name
+ Set<String> resourceId = authzClient.protection()
+ .resource()
+ .findByFilter("name=Hello World Resource");
+
+ // obtian a Entitlement API Token in order to get access to the Entitlement API.
+ // this token is just an access token issued to a client on behalf of an user with a scope kc_entitlement
+ String eat = getEntitlementAPIToken(authzClient);
+
+ // create an entitlement request
+ EntitlementRequest request = new EntitlementRequest();
+ PermissionRequest permission = new PermissionRequest();
+
+ permission.setResourceSetId(resourceId.iterator().next());
+
+ request.addPermission(permission);
+
+ // send the entitlement request to the server in order to obtain a RPT with all permissions granted to the user
+ EntitlementResponse response = authzClient.entitlement(eat).get("hello-world-authz-service", request);
+ String rpt = response.getRpt();
+
+ TokenIntrospectionResponse requestingPartyToken = authzClient.protection().introspectRequestingPartyToken(rpt);
+
+ System.out.println("Token status is: " + requestingPartyToken.getActive());
+ System.out.println("Permissions granted by the server: ");
+
+ for (Permission granted : requestingPartyToken.getPermissions()) {
+ System.out.println(granted);
+ }
+
+ }
+
+ private static void createResource() {
+ // create a new instance based on the configuration define at keycloak-authz.json
+ AuthzClient authzClient = AuthzClient.create();
+
+ // create a new resource representation with the information we want
+ ResourceRepresentation newResource = new ResourceRepresentation();
+
+ newResource.setName("New Resource");
+ newResource.setType("urn:hello-world-authz:resources:example");
+
+ newResource.addScope(new ScopeRepresentation("urn:hello-world-authz:scopes:view"));
+
+ ProtectedResource resourceClient = authzClient.protection().resource();
+ Set<String> existingResource = resourceClient.findByFilter("name=" + newResource.getName());
+
+ if (!existingResource.isEmpty()) {
+ resourceClient.delete(existingResource.iterator().next());
+ }
+
+ // create the resource on the server
+ RegistrationResponse response = resourceClient.create(newResource);
+ String resourceId = response.getId();
+
+ // query the resource using its newly generated id
+ ResourceRepresentation resource = resourceClient.findById(resourceId).getResourceDescription();
+
+ System.out.println(resource);
+ }
+
+ private static void obtainEntitlementsForResource() {
+ // create a new instance based on the configuration define at keycloak-authz.json
+ AuthzClient authzClient = AuthzClient.create();
+
+ // obtian a Entitlement API Token in order to get access to the Entitlement API.
+ // this token is just an access token issued to a client on behalf of an user with a scope kc_entitlement
+ String eat = getEntitlementAPIToken(authzClient);
+
+ // create an entitlement request
+ EntitlementRequest request = new EntitlementRequest();
+ PermissionRequest permission = new PermissionRequest();
+
+ permission.setResourceSetName("Hello World Resource");
+
+ request.addPermission(permission);
+
+ // send the entitlement request to the server in order to obtain a RPT with all permissions granted to the user
+ EntitlementResponse response = authzClient.entitlement(eat).get("hello-world-authz-service", request);
+ String rpt = response.getRpt();
+
+ System.out.println("You got a RPT: " + rpt);
+
+ // now you can use the RPT to access protected resources on the resource server
+ }
+
+ private static void obtainAllEntitlements() {
+ // create a new instance based on the configuration define at keycloak-authz.json
+ AuthzClient authzClient = AuthzClient.create();
+
+ // obtian a Entitlement API Token in order to get access to the Entitlement API.
+ // this token is just an access token issued to a client on behalf of an user with a scope kc_entitlement
+ String eat = getEntitlementAPIToken(authzClient);
+
+ // send the entitlement request to the server in order to obtain a RPT with all permissions granted to the user
+ EntitlementResponse response = authzClient.entitlement(eat).getAll("hello-world-authz-service");
+ String rpt = response.getRpt();
+
+ System.out.println("You got a RPT: " + rpt);
+
+ // now you can use the RPT to access protected resources on the resource server
+ }
+
+ /**
+ * Obtain an Entitlement API Token or EAT from the server. Usually, EATs are going to be obtained by clients using a
+ * authorization_code grant type. Here we are using resource owner credentials for demonstration purposes.
+ *
+ * @param authzClient the authorization client instance
+ * @return a string representing a EAT
+ */
+ private static String getEntitlementAPIToken(AuthzClient authzClient) {
+ return authzClient.obtainAccessToken("alice", "password").getToken();
+ }
+}
diff --git a/examples/authz/hello-world/src/main/resources/keycloak.json b/examples/authz/hello-world/src/main/resources/keycloak.json
new file mode 100644
index 0000000..b337389
--- /dev/null
+++ b/examples/authz/hello-world/src/main/resources/keycloak.json
@@ -0,0 +1,8 @@
+{
+ "realm": "hello-world-authz",
+ "auth-server-url" : "http://localhost:8080/auth",
+ "resource" : "hello-world-authz-service",
+ "credentials": {
+ "secret": "secret"
+ }
+}
\ No newline at end of file
diff --git a/examples/authz/hello-world-authz-service/pom.xml b/examples/authz/hello-world-authz-service/pom.xml
new file mode 100755
index 0000000..5b9b646
--- /dev/null
+++ b/examples/authz/hello-world-authz-service/pom.xml
@@ -0,0 +1,55 @@
+<?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</groupId>
+ <artifactId>keycloak-authz-example-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>hello-world-authz-service</artifactId>
+ <packaging>war</packaging>
+
+ <name>Keycloak Authz: Hello World Example</name>
+
+ <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/examples/authz/hello-world-authz-service/src/main/webapp/error.jsp b/examples/authz/hello-world-authz-service/src/main/webapp/error.jsp
new file mode 100644
index 0000000..00d25b3
--- /dev/null
+++ b/examples/authz/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/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp b/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
new file mode 100644
index 0000000..697f491
--- /dev/null
+++ b/examples/authz/hello-world-authz-service/src/main/webapp/index.jsp
@@ -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.
+ ~
+ --%>
+<%@page import="org.keycloak.AuthorizationContext" %>
+<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
+<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
+<%@ page import="org.keycloak.KeycloakSecurityContext" %>
+<%@ page import="org.keycloak.representations.authorization.Permission" %>
+
+<%
+ KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
+ AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext();
+%>
+<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>
+
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
new file mode 100644
index 0000000..04c0486
--- /dev/null
+++ b/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json
@@ -0,0 +1,13 @@
+{
+ "realm": "hello-world-authz",
+ "realm-public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwmm2Nso+rUOYUYc4hO67LSf4s0pAKcqUbWWycS3fcz6Q4jg/SsBbIBJJXOMVR9GqwyTCVTH5s8Rb0+0pA+UrbZfMG2XIDnJoaGfJj9DvJwQkD+vzTvaS5q0ilP0tPlbusI5pyMi9xx+cjJBOvKR2GxjhcKrgb21lpmGcA1F1CPO3y/DT8GzTKg+9/nPKt1dKEUD7P5Uy5N7d8zz1fuOSLb5G267T1fKJvi6am8kCgM+agFVQ23j7w/aJ7T1EHUCZdaJ+aSODSYl8dM4RFNTjda0KMHHXqMMvd2+g8lZ0lAfstHywqZtCcHc9ULClVvQmQyXovn2qTktHAcD6BHTAgQIDAQAB",
+ "auth-server-url": "http://localhost:8080/auth",
+ "ssl-required": "external",
+ "resource": "hello-world-authz-service",
+ "credentials": {
+ "secret": "a7672d93-ea27-44a3-baa6-ba3536609067"
+ },
+ "policy-enforcer": {
+ "on-deny-redirect-to" : "/hello-world-authz-service/error.jsp"
+ }
+}
\ No newline at end of file
diff --git a/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/web.xml b/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..1ca06be
--- /dev/null
+++ b/examples/authz/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/examples/authz/photoz-uma/photoz-uma-authz-policy/pom.xml b/examples/authz/photoz-uma/photoz-uma-authz-policy/pom.xml
new file mode 100755
index 0000000..6625a57
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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</groupId>
+ <artifactId>keycloak-authz-photoz-uma-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>photoz-uma-authz-policy</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Keycloak Authz: Examples - Photoz UMA Authz Rule-based Policy</name>
+
+ <description>
+ Photoz UMA Authz Rule-based Policies using JBoss Drools
+ </description>
+
+</project>
diff --git a/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com.photoz.authz.policy.admin/Main.drl b/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com.photoz.authz.policy.admin/Main.drl
new file mode 100644
index 0000000..deb1c84
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com.photoz.authz.policy.resource.owner/Main.drl b/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com.photoz.authz.policy.resource.owner/Main.drl
new file mode 100644
index 0000000..9378b94
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com.photoz.authz.policy.user/Main.drl b/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com.photoz.authz.policy.user/Main.drl
new file mode 100644
index 0000000..9b1677e
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com/photoz/authz/policy/contextual/Main.drl b/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/com/photoz/authz/policy/contextual/Main.drl
new file mode 100644
index 0000000..d187467
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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.authz.context.authc.method", "otp"),
+ $attributes.containsValue("someAttribute", "you_can_access")
+ )
+ then
+ $evaluation.grant();
+end
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/META-INF/kmodule.xml b/examples/authz/photoz-uma/photoz-uma-authz-policy/src/main/resources/META-INF/kmodule.xml
new file mode 100644
index 0000000..84bacd5
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-html5-client/pom.xml b/examples/authz/photoz-uma/photoz-uma-html5-client/pom.xml
new file mode 100755
index 0000000..39bf86f
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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</groupId>
+ <artifactId>keycloak-authz-photoz-uma-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>photoz-uma-html5-client</artifactId>
+ <packaging>war</packaging>
+
+ <name>Keycloak Authz: Photoz UMA HTML5 Client</name>
+ <description>Photoz UMA 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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/index.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/index.html
new file mode 100755
index 0000000..b41383b
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/index.html
@@ -0,0 +1,29 @@
+<!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:8080/auth/js/keycloak.js"></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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/js/app.js b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/js/app.js
new file mode 100755
index 0000000..8913ead
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/js/app.js
@@ -0,0 +1,207 @@
+var module = angular.module('photoz-uma', ['ngRoute', 'ngResource']);
+
+var Identity = {};
+
+angular.element(document).ready(function ($http) {
+ var keycloakAuth = new Keycloak('keycloak.json');
+ Identity.loggedIn = false;
+ keycloakAuth.init({onLoad: 'login-required'}).success(function () {
+ Identity.loggedIn = true;
+ Identity.authz = keycloakAuth;
+ Identity.logout = function () {
+ Identity.loggedIn = false;
+ Identity.claim = {};
+ Identity.authc = null;
+ window.location = this.authz.authServerUrl + "/realms/photoz-uma/protocol/openid-connect/logout?redirect_uri=http://localhost:8080/photoz-uma-html5-client/index.html";
+ Identity.authz = null;
+ };
+ Identity.claim = {};
+ Identity.claim.name = Identity.authz.idTokenParsed.name;
+ Identity.hasRole = function (name) {
+ if (Identity.authz && Identity.authz.hasRealmRole(name)) {
+ return true;
+ }
+ return false;
+ };
+ Identity.isAdmin = function () {
+ return this.hasRole("admin");
+ };
+ Identity.authc = {};
+ Identity.authc.token = Identity.authz.token;
+ module.factory('Identity', function () {
+ return Identity;
+ });
+ angular.bootstrap(document, ["photoz-uma"]);
+ }).error(function () {
+ window.location.reload();
+ });
+});
+module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Album, Identity) {
+ Album.query(function (albums) {
+ $scope.albums = albums;
+ });
+
+ $scope.Identity = Identity;
+
+ $scope.deleteAlbum = function (album) {
+ new Album(album).$delete({id: album.id}, function () {
+ $route.reload();
+ });
+ }
+});
+module.controller('TokenCtrl', function ($scope, Identity) {
+ $scope.showRpt = function () {
+ document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.uma.rpt.rpt), null, ' ');
+ }
+
+ $scope.showAccessToken = function () {
+ document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.authc.token), null, ' ');
+ }
+
+ $scope.requestEntitlements = function () {
+ var request = new XMLHttpRequest();
+
+ request.open("GET", "http://localhost:8080/auth/realms/photoz-uma/authz/entitlement/photoz-uma-restful-api", true);
+ request.setRequestHeader("Authorization", "Bearer " + Identity.authc.token);
+ request.onreadystatechange = function () {
+ if (request.readyState == 4 && request.status == 200) {
+ Identity.uma.rpt = JSON.parse(request.responseText);
+ }
+ }
+
+ request.send(null);
+ }
+});
+module.controller('AlbumCtrl', function ($scope, $http, $routeParams, $location, Album) {
+ $scope.album = {};
+ if ($routeParams.id) {
+ $scope.album = Album.get({id: $routeParams.id});
+ }
+ $scope.create = function () {
+ var newAlbum = new Album($scope.album);
+ newAlbum.$save({}, function (data) {
+ $location.path('/');
+ });
+ };
+});
+module.controller('ProfileCtrl', function ($scope, $http, $routeParams, $location, Profile) {
+ $scope.profile = Profile.get();
+});
+module.controller('AdminAlbumCtrl', function ($scope, $http, $route, AdminAlbum, Album) {
+ $scope.albums = {};
+ $http.get('/photoz-uma-restful-api/admin/album').success(function (data) {
+ $scope.albums = data;
+ });
+ $scope.deleteAlbum = function (album) {
+ var newAlbum = new Album(album);
+ newAlbum.$delete({id: album.id}, function () {
+ $route.reload();
+ });
+ }
+});
+module.factory('Album', ['$resource', function ($resource) {
+ return $resource('http://localhost:8080/photoz-uma-restful-api/album/:id');
+}]);
+module.factory('Profile', ['$resource', function ($resource) {
+ return $resource('http://localhost:8080/photoz-uma-restful-api/profile');
+}]);
+module.factory('AdminAlbum', ['$resource', function ($resource) {
+ return $resource('http://localhost:8080/photoz-uma-restful-api/admin/album/:id');
+}]);
+module.factory('authInterceptor', function ($q, $injector, $timeout, Identity) {
+ return {
+ request: function (request) {
+ document.getElementById("output").innerHTML = '';
+ if (Identity.uma && Identity.uma.rpt && request.url.indexOf('/authorize') == -1) {
+ retries = 0;
+ request.headers.Authorization = 'Bearer ' + Identity.uma.rpt.rpt;
+ } else {
+ request.headers.Authorization = 'Bearer ' + Identity.authc.token;
+ }
+ return request;
+ },
+ responseError: function (rejection) {
+ if (rejection.status == 403 || rejection.status == 401) {
+ var retry = (!rejection.config.retry || rejection.config.retry < 1);
+
+ if (!retry) {
+ document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
+ return $q.reject(rejection);
+ }
+
+ if (rejection.config.url.indexOf('/authorize') == -1 && retry) {
+ if (rejection.status == 401) {
+ console.log("Here");
+ var authenticateHeader = rejection.headers('WWW-Authenticate');
+
+ if (authenticateHeader.startsWith('UMA')) {
+ var params = authenticateHeader.split(',');
+
+ for (i = 0; i < params.length; i++) {
+ var param = params[i].split('=');
+
+ if (param[0] == 'ticket') {
+ var ticket = param[1].substring(1, param[1].length - 1).trim();
+
+ var data = JSON.stringify({
+ ticket: ticket,
+ rpt: Identity.uma ? Identity.uma.rpt.rpt : ""
+ });
+
+ var $http = $injector.get("$http");
+
+ var deferred = $q.defer();
+
+ $http.post('http://localhost:8080/auth/realms/photoz-uma/authz/authorize', data, {headers: {"Authorization": "Bearer " + Identity.authc.token}})
+ .then(function (authzResponse) {
+ if (authzResponse.data) {
+ Identity.uma = {};
+ Identity.uma.rpt = authzResponse.data;
+ }
+ deferred.resolve(rejection);
+ }, function (authzResponse) {
+ document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
+ });
+
+ var promise = deferred.promise;
+
+ return promise.then(function (res) {
+ if (!res.config.retry) {
+ res.config.retry = 1;
+ } else {
+ res.config.retry++;
+ }
+ return $http(res.config).then(function (response) {
+ return response;
+ });
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return $q.reject(rejection);
+ }
+ };
+});
+module.config(function ($httpProvider, $routeProvider) {
+ $httpProvider.interceptors.push('authInterceptor');
+ $routeProvider.when('/', {
+ templateUrl: 'partials/home.html',
+ controller: 'GlobalCtrl'
+ }).when('/album/create', {
+ templateUrl: 'partials/album/create.html',
+ controller: 'AlbumCtrl',
+ }).when('/album/:id', {
+ templateUrl: 'partials/album/detail.html',
+ controller: 'AlbumCtrl',
+ }).when('/admin/album', {
+ templateUrl: 'partials/admin/albums.html',
+ controller: 'AdminAlbumCtrl',
+ }).when('/profile', {
+ templateUrl: 'partials/profile.html',
+ controller: 'ProfileCtrl',
+ });
+});
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/keycloak.json b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/keycloak.json
new file mode 100644
index 0000000..3e73241
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/keycloak.json
@@ -0,0 +1,12 @@
+{
+ "realm": "photoz-uma",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8080/auth",
+ "ssl-required" : "external",
+ "resource" : "photoz-uma-html5-client",
+ "public-client" : true,
+ "use-resource-role-mappings": "false",
+ "scope" : {
+ "realm" : [ "user" ]
+ }
+}
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular.min.js b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular.min.js
new file mode 100644
index 0000000..569a9a2
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular-resource.min.js b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular-resource.min.js
new file mode 100644
index 0000000..3f196c3
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular-resource.min.js
@@ -0,0 +1,13 @@
+/*
+ AngularJS v1.3.0-beta.5
+ (c) 2010-2014 Google, Inc. http://angularjs.org
+ License: MIT
+*/
+(function(H,a,A){'use strict';function D(p,g){g=g||{};a.forEach(g,function(a,c){delete g[c]});for(var c in p)!p.hasOwnProperty(c)||"$"===c.charAt(0)&&"$"===c.charAt(1)||(g[c]=p[c]);return g}var v=a.$$minErr("$resource"),C=/^(\.[a-zA-Z_$][0-9a-zA-Z_$]*)+$/;a.module("ngResource",["ng"]).factory("$resource",["$http","$q",function(p,g){function c(a,c){this.template=a;this.defaults=c||{};this.urlParams={}}function t(n,w,l){function r(h,d){var e={};d=x({},w,d);s(d,function(b,d){u(b)&&(b=b());var k;if(b&&
+b.charAt&&"@"==b.charAt(0)){k=h;var a=b.substr(1);if(null==a||""===a||"hasOwnProperty"===a||!C.test("."+a))throw v("badmember",a);for(var a=a.split("."),f=0,c=a.length;f<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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular-route.min.js b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/angular/angular-route.min.js
new file mode 100644
index 0000000..9e161e2
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/jwt-decode.min.js b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/jwt-decode.min.js
new file mode 100644
index 0000000..f56f967
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/lib/jwt-decode.min.js
@@ -0,0 +1 @@
+!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);throw new Error("Cannot find module '"+g+"'")}var j=c[g]={exports:{}};b[g][0].call(j.exports,function(a){var c=b[g][1][a];return e(c?c:a)},j,j.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/admin/albums.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/admin/albums.html
new file mode 100644
index 0000000..bb381b9
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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="#" ng-click="deleteAlbum(p)">X</a>]
+ </li>
+ </ul>
+ </td>
+ </tr>
+ </tbody>
+</table>
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/create.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/create.html
new file mode 100644
index 0000000..556693c
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/create.html
@@ -0,0 +1,7 @@
+<h1>Create an Album</h1>
+
+<form>
+ Name: <input type="text" ng-model="album.name"/>
+
+ <button ng-click="create()">Save</button>
+</form>
diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/detail.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/album/detail.html
new file mode 100644
index 0000000..cf32df1
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/home.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/home.html
new file mode 100644
index 0000000..dc10c92
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/home.html
@@ -0,0 +1,22 @@
+<h2><span>Welcome To Photoz, {{Identity.claim.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>
+<hr/>
+<br/>
+<div data-ng-show="!Identity.isAdmin()">
+<a href="#/album/create">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>
+<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="#" ng-click="deleteAlbum(p)">X</a>]</td>
+ </tr>
+ </tbody>
+</table>
+</div>
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/profile.html b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/partials/profile.html
new file mode 100644
index 0000000..c6f6750
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/WEB-INF/web.xml b/examples/authz/photoz-uma/photoz-uma-html5-client/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..98e4067
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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-uma-html5-client</module-name>
+
+</web-app>
examples/authz/photoz-uma/photoz-uma-realm.json 111(+111 -0)
diff --git a/examples/authz/photoz-uma/photoz-uma-realm.json b/examples/authz/photoz-uma/photoz-uma-realm.json
new file mode 100644
index 0000000..0230b61
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-realm.json
@@ -0,0 +1,111 @@
+{
+ "realm": "photoz-uma",
+ "enabled": true,
+ "sslRequired": "external",
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [
+ "password"
+ ],
+ "users": [
+ {
+ "username": "alice",
+ "enabled": true,
+ "email": "alice@keycloak.org",
+ "firstName": "Alice",
+ "lastName": "In Chains",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "alice"
+ }
+ ],
+ "realmRoles": [
+ "user", "uma_authorization"
+ ]
+ },
+ {
+ "username": "jdoe",
+ "enabled": true,
+ "email": "jdoe@keycloak.org",
+ "firstName": "John",
+ "lastName": "Doe",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "jdoe"
+ }
+ ],
+ "realmRoles": [
+ "user", "uma_authorization"
+ ]
+ },
+ {
+ "username": "admin",
+ "enabled": true,
+ "email": "admin@admin.com",
+ "firstName": "Admin",
+ "lastName": "Istrator",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "admin"
+ }
+ ],
+ "realmRoles": [
+ "user", "admin", "uma_authorization"
+ ],
+ "clientRoles": {
+ "realm-management": [
+ "realm-admin"
+ ]
+ }
+ },
+ {
+ "username": "service-account-photoz-uma-restful-api",
+ "enabled": true,
+ "email": "service-account-photoz-uma-restful-api@placeholder.org",
+ "serviceAccountClientId": "photoz-uma-restful-api",
+ "clientRoles": {
+ "photoz-uma-restful-api" : ["uma_protection"]
+ }
+ }
+ ],
+ "roles": {
+ "realm": [
+ {
+ "name": "user",
+ "description": "User privileges"
+ },
+ {
+ "name": "admin",
+ "description": "Administrator privileges"
+ }
+ ]
+ },
+ "clients": [
+ {
+ "clientId": "photoz-uma-html5-client",
+ "enabled": true,
+ "adminUrl": "/photoz-uma-html5-client",
+ "baseUrl": "/photoz-uma-html5-client",
+ "publicClient": true,
+ "redirectUris": [
+ "/photoz-uma-html5-client/*"
+ ],
+ "webOrigins": [
+ ""
+ ]
+ },
+ {
+ "clientId": "photoz-uma-restful-api",
+ "enabled": true,
+ "baseUrl": "/photoz-uma-restful-api",
+ "authorizationServicesEnabled" : true,
+ "redirectUris": [
+ "/photoz-uma-restful-api/*"
+ ],
+ "secret": "secret"
+ }
+ ]
+}
diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/pom.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/pom.xml
new file mode 100755
index 0000000..0f04d95
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/pom.xml
@@ -0,0 +1,68 @@
+<?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</groupId>
+ <artifactId>keycloak-authz-photoz-uma-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>photoz-uma-restful-api</artifactId>
+ <packaging>war</packaging>
+
+ <name>Keycloak Authz: Photoz UMA RESTful API</name>
+ <description>Photoz UMA 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>
+ </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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
new file mode 100644
index 0000000..b349e02
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/admin/AdminAlbumService.java
@@ -0,0 +1,62 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.example.photoz.admin;
+
+import org.keycloak.example.photoz.entity.Album;
+
+import javax.ejb.Stateless;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @author <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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
new file mode 100644
index 0000000..be2f1eb
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/AlbumService.java
@@ -0,0 +1,132 @@
+package org.keycloak.example.photoz.album;
+
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.representation.ResourceRepresentation;
+import org.keycloak.authorization.client.representation.ScopeRepresentation;
+import org.keycloak.authorization.client.resource.ProtectionResource;
+import org.keycloak.example.photoz.ErrorResponse;
+import org.keycloak.example.photoz.entity.Album;
+
+import javax.ejb.Stateless;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.persistence.Query;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.security.Principal;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Path("/album")
+@Stateless
+public class AlbumService {
+
+ public static final String SCOPE_ALBUM_VIEW = "urn:photoz.com:scopes:album:view";
+ public static final String SCOPE_ALBUM_CREATE = "urn:photoz.com:scopes:album:create";
+ public static final String SCOPE_ALBUM_DELETE = "urn:photoz.com:scopes:album:delete";
+
+ @PersistenceContext
+ private EntityManager entityManager;
+
+ @POST
+ @Consumes("application/json")
+ public Response create(@Context HttpServletRequest request, Album newAlbum) {
+ Principal userPrincipal = request.getUserPrincipal();
+
+ newAlbum.setUserId(userPrincipal.getName());
+
+ Query queryDuplicatedAlbum = this.entityManager.createQuery("from Album where name = :name and userId = :userId");
+
+ queryDuplicatedAlbum.setParameter("name", newAlbum.getName());
+ queryDuplicatedAlbum.setParameter("userId", userPrincipal.getName());
+
+ if (!queryDuplicatedAlbum.getResultList().isEmpty()) {
+ throw new ErrorResponse("Name [" + newAlbum.getName() + "] already taken. Choose another one.", Status.CONFLICT);
+ }
+
+ this.entityManager.persist(newAlbum);
+
+ createProtectedResource(newAlbum);
+
+ return Response.ok(newAlbum).build();
+ }
+
+ @Path("{id}")
+ @DELETE
+ public Response delete(@PathParam("id") String id) {
+ Album album = this.entityManager.find(Album.class, Long.valueOf(id));
+
+ try {
+ deleteProtectedResource(album);
+ this.entityManager.remove(album);
+ } catch (Exception e) {
+ throw new RuntimeException("Could not delete album.", e);
+ }
+
+ return Response.ok().build();
+ }
+
+ @GET
+ @Produces("application/json")
+ public Response findAll(@Context HttpServletRequest request) {
+ return Response.ok(this.entityManager.createQuery("from Album where userId = '" + request.getUserPrincipal().getName() + "'").getResultList()).build();
+ }
+
+ @GET
+ @Path("{id}")
+ @Produces("application/json")
+ public Response findById(@PathParam("id") String id) {
+ List result = this.entityManager.createQuery("from Album where id = " + id).getResultList();
+
+ if (result.isEmpty()) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(result.get(0)).build();
+ }
+
+ private void createProtectedResource(Album album) {
+ try {
+ HashSet<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());
+
+ AuthzClient.create().protection().resource().create(albumResource);
+ } catch (Exception e) {
+ throw new RuntimeException("Could not register protected resource.", e);
+ }
+ }
+
+ private void deleteProtectedResource(Album album) {
+ String uri = "/album/" + album.getId();
+
+ try {
+ ProtectionResource protection = AuthzClient.create().protection();
+ Set<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);
+ }
+ }
+}
diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
new file mode 100644
index 0000000..be638b6
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/album/ProfileService.java
@@ -0,0 +1,70 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.example.photoz.album;
+
+import javax.ejb.Stateless;
+import javax.persistence.EntityManager;
+import javax.persistence.PersistenceContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+import java.security.Principal;
+import java.util.List;
+
+/**
+ * @author <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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
new file mode 100644
index 0000000..978bdea
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Album.java
@@ -0,0 +1,79 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.example.photoz.entity;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.OneToMany;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java
new file mode 100644
index 0000000..08b7495
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/entity/Photo.java
@@ -0,0 +1,81 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 Red Hat, Inc. and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.example.photoz.entity;
+
+import javax.persistence.Basic;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.Lob;
+import javax.persistence.ManyToOne;
+
+/**
+ * @author <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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java
new file mode 100644
index 0000000..51755d8
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/ErrorResponse.java
@@ -0,0 +1,32 @@
+package org.keycloak.example.photoz;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java
new file mode 100644
index 0000000..5b8377c
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/java/org/keycloak/example/photoz/PhotozApplication.java
@@ -0,0 +1,12 @@
+package org.keycloak.example.photoz;
+
+import javax.ws.rs.ApplicationPath;
+import javax.ws.rs.core.Application;
+
+/**
+ * Basic auth app.
+ */
+@ApplicationPath("/")
+public class PhotozApplication extends Application {
+
+}
diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/keycloak.json b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/keycloak.json
new file mode 100644
index 0000000..528b178
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/keycloak.json
@@ -0,0 +1,8 @@
+{
+ "realm": "photoz-uma",
+ "auth-server-url": "http://localhost:8080/auth",
+ "resource": "photoz-uma-restful-api",
+ "credentials": {
+ "secret": "secret"
+ }
+}
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/beans.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/beans.xml
new file mode 100644
index 0000000..957dc8a
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/persistence.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/resources/META-INF/persistence.xml
new file mode 100644
index 0000000..9323182
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/index.html b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/index.html
new file mode 100644
index 0000000..8318c86
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/index.html
@@ -0,0 +1 @@
+Test
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/META-INF/jboss-deployment-structure.xml
new file mode 100644
index 0000000..4b23be6
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/keycloak.json
new file mode 100644
index 0000000..8b21c95
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/keycloak.json
@@ -0,0 +1,50 @@
+{
+ "realm": "photoz-uma",
+ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url": "http://localhost:8080/auth",
+ "ssl-required": "external",
+ "resource": "photoz-uma-restful-api",
+ "bearer-only" : true,
+ "credentials": {
+ "secret": "secret"
+ },
+ "policy-enforcer": {
+ "user-managed-access" : {},
+ "paths": [
+ {
+ "path" : "/album/*",
+ "methods" : [
+ {
+ "method": "GET",
+ "scopes" : ["urn:photoz.com:scopes:album:view"]
+ },
+ {
+ "method": "POST",
+ "scopes" : ["urn:photoz.com:scopes:album:create"]
+ }
+ ]
+ },
+ {
+ "name" : "Album Resource",
+ "path" : "/album/{id}",
+ "methods" : [
+ {
+ "method": "DELETE",
+ "scopes" : ["urn:photoz.com:scopes:album:delete"]
+ },
+ {
+ "method": "GET",
+ "scopes" : ["urn:photoz.com:scopes:album:view"]
+ }
+ ]
+ },
+ {
+ "path" : "/profile"
+ },
+ {
+ "name" : "Admin Resources",
+ "path" : "/admin/*"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/photoz-ds.xml
new file mode 100644
index 0000000..247448f
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/web.xml b/examples/authz/photoz-uma/photoz-uma-restful-api/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..92fd000
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-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-uma-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-uma</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/examples/authz/photoz-uma/photoz-uma-restful-api-authz-service.json b/examples/authz/photoz-uma/photoz-uma-restful-api-authz-service.json
new file mode 100644
index 0000000..60a4cd5
--- /dev/null
+++ b/examples/authz/photoz-uma/photoz-uma-restful-api-authz-service.json
@@ -0,0 +1,183 @@
+{
+ "allowRemoteResourceManagement": true,
+ "policyEnforcementMode": "ENFORCING",
+ "resources": [
+ {
+ "name": "User Profile Resource",
+ "uri": "/profile",
+ "type": "http://photoz.com/profile",
+ "scopes": [
+ {
+ "name": "urn:photoz.com:scopes:profile:view"
+ }
+ ]
+ },
+ {
+ "name": "Album Resource",
+ "uri": "/album/*",
+ "type": "http://photoz.com/album",
+ "scopes": [
+ {
+ "name": "urn:photoz.com:scopes:album:view"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:create"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:delete"
+ }
+ ]
+ },
+ {
+ "name": "Admin Resources",
+ "uri": "/admin/*",
+ "type": "http://photoz.com/admin",
+ "scopes": [
+ {
+ "name": "urn:photoz.com:scopes:album:admin:manage"
+ }
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "Only Owner Policy",
+ "description": "Defines that only the resource owner is allowed to do something",
+ "type": "drools",
+ "config": {
+ "mavenArtifactVersion": "2.0.0.CR1-SNAPSHOT",
+ "mavenArtifactId": "photoz-uma-authz-policy",
+ "sessionName": "MainOwnerSession",
+ "mavenArtifactGroupId": "org.keycloak",
+ "moduleName": "PhotozAuthzOwnerPolicy",
+ "scannerPeriod": "1",
+ "scannerPeriodUnit": "Hours"
+ }
+ },
+ {
+ "name": "Any Admin Policy",
+ "description": "Defines that adminsitrators can do something",
+ "type": "role",
+ "config": {
+ "roles": "[\"admin\"]"
+ }
+ },
+ {
+ "name": "Any User Policy",
+ "description": "Defines that any user can do something",
+ "type": "role",
+ "config": {
+ "roles": "[\"user\"]"
+ }
+ },
+ {
+ "name": "Only From a Specific Client Address",
+ "description": "Defines that only clients from a specific address can do something",
+ "type": "js",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "code": "var contextAttributes = $evaluation.getContext().getAttributes();\n\nif (contextAttributes.containsValue('kc.authz.context.client.network.ip_address', '127.0.0.1')) {\n $evaluation.grant();\n}"
+ }
+ },
+ {
+ "name": "Administration Policy",
+ "description": "Defines that only administrators from a specific network address can do something.",
+ "type": "aggregate",
+ "config": {
+ "applyPolicies": "[\"Any Admin Policy\",\"Only From a Specific Client Address\"]"
+ }
+ },
+ {
+ "name": "Only Owner and Administrators Policy",
+ "description": "Defines that only the resource owner and administrators can do something",
+ "type": "aggregate",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "applyPolicies": "[\"Administration Policy\",\"Only Owner Policy\"]"
+ }
+ },
+ {
+ "name": "Only From @keycloak.org or Admin",
+ "description": "Defines that only users from @keycloak.org",
+ "type": "js",
+ "config": {
+ "applyPolicies": "[]",
+ "code": "var context = $evaluation.getContext();\nvar identity = context.getIdentity();\nvar attributes = identity.getAttributes();\nvar email = attributes.getValue('email').asString(0);\n\nif (identity.hasRole('admin') || email.endsWith('@keycloak.org')) {\n $evaluation.grant();\n}"
+ }
+ },
+ {
+ "name": "Only in the Period",
+ "description": "Access granted only during the morning",
+ "type": "time",
+ "config": {
+ "noa": "2016-01-03 23:59:59",
+ "expirationUnit": "Minutes",
+ "nbf": "2016-01-01 00:00:00",
+ "expirationTime": "1"
+ }
+ },
+ {
+ "name": "Album Resource Permission",
+ "description": "General policies that apply to all album resources.",
+ "type": "resource",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "defaultResourceType": "http://photoz.com/album",
+ "default": "true",
+ "applyPolicies": "[\"Any User Policy\",\"Administration Policy\"]"
+ }
+ },
+ {
+ "name": "Admin Resource Permission",
+ "description": "General policy for any administrative resource.",
+ "type": "resource",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "defaultResourceType": "http://photoz.com/admin",
+ "default": "true",
+ "applyPolicies": "[\"Administration Policy\"]"
+ }
+ },
+ {
+ "name": "View User Permission",
+ "description": "Defines who is allowed to view an user profile",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "applyPolicies": "[\"Only From @keycloak.org or Admin\"]",
+ "scopes": "[\"urn:photoz.com:scopes:profile:view\"]"
+ }
+ },
+ {
+ "name": "Delete Album Policy",
+ "description": "A policy that only allows the owner to delete his albums.",
+ "type": "scope",
+ "logic": "POSITIVE",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "applyPolicies": "[\"Only Owner and Administrators Policy\"]",
+ "scopes": "[\"urn:photoz.com:scopes:album:delete\"]"
+ }
+ }
+ ],
+ "scopes": [
+ {
+ "name": "urn:photoz.com:scopes:profile:view"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:view"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:create"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:delete"
+ },
+ {
+ "name": "urn:photoz.com:scopes:album:admin:manage"
+ }
+ ]
+}
\ No newline at end of file
examples/authz/photoz-uma/pom.xml 24(+24 -0)
diff --git a/examples/authz/photoz-uma/pom.xml b/examples/authz/photoz-uma/pom.xml
new file mode 100755
index 0000000..f0482e4
--- /dev/null
+++ b/examples/authz/photoz-uma/pom.xml
@@ -0,0 +1,24 @@
+<?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</groupId>
+ <artifactId>keycloak-authz-example-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-photoz-uma-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <name>Keycloak Authz: PhotoZ UMA Example Application Parent</name>
+ <description>PhotoZ Example Application</description>
+
+ <modules>
+ <module>photoz-uma-restful-api</module>
+ <module>photoz-uma-html5-client</module>
+ <module>photoz-uma-authz-policy</module>
+ </modules>
+</project>
examples/authz/photoz-uma/README.md 99(+99 -0)
diff --git a/examples/authz/photoz-uma/README.md b/examples/authz/photoz-uma/README.md
new file mode 100644
index 0000000..61243ee
--- /dev/null
+++ b/examples/authz/photoz-uma/README.md
@@ -0,0 +1,99 @@
+# About the Example Application
+
+This is a simple application based on HTML5+AngularJS+JAX-RS that will introduce you to some of the main concepts around Keycloak Authorization Services.
+
+Basically, it is a project containing three modules:
+
+* **photoz-uma-restful-api**, with a simple RESTFul API based on JAX-RS and acting as a regular **client application**.
+* **photoz-uma-html5-client**, with a HTML5+AngularJS client that will consume the RESTful API and acting as a **resource server**.
+* **photoz-uma-authz-policy**, with a simple project with some rule-based policies using JBoss Drools.
+
+For this application, users can be regular users or administrators. Regular users can create/view/delete their albums
+and administrators can view the albums for all users.
+
+In Keycloak, albums are resources that must be protected based on a set of policies that defines who and how can access them.
+Beside that, resources belong to a specific resource server, in this case to the *photoz-uma-restful-api*.
+
+The resources are also associated with a set of scopes that define a specific access context. In this case, albums have three main scopes:
+
+* urn:photoz.com:scopes:album:create
+* urn:photoz.com:scopes:album:view
+* urn:photoz.com:scopes:album:delete
+
+The authorization requirements for this example application are based on the following assumptions:
+
+* By default, any regular user can perform any operation on his resources.
+
+ * For instance, Alice can create, view and delete her albums.
+
+* Only the owner and administrators can delete albums. Here we are considering policies based on the *urn:photoz.com:scopes:album:delete*
+
+ * For instance, only Alice can delete her album.
+
+* Only administrators can access the Administration API (which basically provides ways to query albums for all users)
+
+That said, this application will show you how to use the Keycloak to define policies using:
+
+* Role-based Access Control
+* Attribute-based Access Control
+* Rule-based policies using JBoss Drools
+* Rule-based policies using JavaScript
+
+Beside that, this example demonstrates how to create resources dynamically and how to protected them using the *Protection API* and the *Authorization Client API*. Here you'll see
+how to create a resource whose owner is the authenticated user.
+
+It also provides some background on how you can actually protect your JAX-RS endpoints using a *policy enforcer*.
+
+## Create the Example Realm and a Resource Server
+
+Considering that your AuthZ Server is up and running, log in to the Keycloak Administration Console.
+
+Now, create a new realm based on the following configuration file:
+
+ examples/authz/photoz/photoz-uma-realm.json
+
+That will import a pre-configured realm with everything you need to run this example. For more details about how to import a realm
+into Keycloak, check the Keycloak's reference documentation.
+
+After importing that file, you'll have a new realm called ``photoz``.
+
+Back to the command-line, build the example application. This step is necessary given that we're using policies based on
+JBoss Drools, which require ``photoz-uma-authz-policy`` artifact installed into your local maven repository.
+
+ cd examples/authz/photoz
+ mvn clean install
+
+Now, let's import another configuration using the Administration Console in order to configure the ``photoz-uma-restful-api`` as a resource server with all resources, scopes, permissions and policies.
+
+Click on ``Authorization`` on the left side menu. Click on the ``Create`` button on the top of the resource server table. This will
+open the page that allows you to create a new resource server.
+
+Click on the ``Select file`` button, which means you want to import a resource server configuration. Now select the file that is located at:
+
+ examples/authz/photoz/photoz-uma-restful-api/photoz-uma-restful-api-authz-config.json
+
+Now click ``Upload`` and a new resource server will be created based on the ``photoz-uma-restful-api`` client application.
+
+## Deploy and Run the Example Applications
+
+To deploy the example applications, follow these steps:
+
+ cd examples/authz/photoz/photoz-uma-html5-client
+ mvn wildfly:deploy
+
+And then:
+
+ cd examples/authz/photoz/photoz-uma-restful-api
+ mvn wildfly:deploy
+
+Now, try to access the client application using the following URL:
+
+ http://localhost:8080/photoz-uma-html5-client
+
+If everything is correct, you will be redirect to Keycloak login page. You can login to the application with the following credentials:
+
+* username: jdoe / password: jdoe
+* username: alice / password: alice
+* username: admin / password: admin
+
+
examples/authz/pom.xml 30(+30 -0)
diff --git a/examples/authz/pom.xml b/examples/authz/pom.xml
new file mode 100755
index 0000000..318d01e
--- /dev/null
+++ b/examples/authz/pom.xml
@@ -0,0 +1,30 @@
+<?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>
+ <artifactId>keycloak-examples-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>keycloak-authz-example-parent</artifactId>
+ <packaging>pom</packaging>
+
+ <name>Keycloak Authz: Examples Parent</name>
+ <description/>
+
+ <properties>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ </properties>
+
+ <modules>
+ <module>photoz-uma</module>
+ <module>servlet-authz</module>
+ <module>hello-world</module>
+ <module>hello-world-authz-service</module>
+ </modules>
+</project>
examples/authz/servlet-authz/pom.xml 54(+54 -0)
diff --git a/examples/authz/servlet-authz/pom.xml b/examples/authz/servlet-authz/pom.xml
new file mode 100755
index 0000000..cfe82bc
--- /dev/null
+++ b/examples/authz/servlet-authz/pom.xml
@@ -0,0 +1,54 @@
+<?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</groupId>
+ <artifactId>keycloak-authz-example-parent</artifactId>
+ <version>2.0.0.CR1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>servlet-authz</artifactId>
+ <packaging>war</packaging>
+
+ <name>Keycloak Authz: Examples - Servlet Authorization</name>
+ <description>Servlet Authorization</description>
+
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-client</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</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>
examples/authz/servlet-authz/README.md 50(+50 -0)
diff --git a/examples/authz/servlet-authz/README.md b/examples/authz/servlet-authz/README.md
new file mode 100644
index 0000000..df52870
--- /dev/null
+++ b/examples/authz/servlet-authz/README.md
@@ -0,0 +1,50 @@
+# About the Example Application
+
+This is a simple Servlet-based application that will introduce you to some of the main concepts around Keycloak Authorization Services.
+
+For this application, users can be regular users, premium users or administrators, where:
+
+* Regular users have very limited access.
+* Premium users have access to the *premium area*
+* Administrators have access to the *administration area*
+
+In Keycloak, all the paths being protected are resources on the server.
+
+This application will also show you how to create a dynamic menu with the permissions granted to an user.
+
+## Create the Example Realm and a Resource Server
+
+Considering that your AuthZ Server is up and running, log in to the Keycloak Administration Console.
+
+Now, create a new realm based on the following configuration file:
+
+ examples/authz/servlet-authz/servlet-authz-realm.json
+
+That will import a pre-configured realm with everything you need to run this example. For more details about how to import a realm
+into Keycloak, check the Keycloak's reference documentation.
+
+After importing that file, you'll have a new realm called ``servlet-authz``.
+
+Now, let's import another configuration using the Administration Console in order to configure the ``servlet-authz-app`` client application as a resource server with all resources, scopes, permissions and policies.
+
+Click on ``Authorization`` on the left side menu. Click on the ``Create`` button on the top of the resource server table. This will
+open the page that allows you to create a new resource server.
+
+Click on the ``Select file`` button, which means you want to import a resource server configuration. Now select the file that is located at:
+
+ examples/authz/servlet-authz/servlet-authz-app-config.json
+
+Now click ``Upload`` and a new resource server will be created based on the ``servlet-authz-app`` client application.
+
+## Deploy and Run the Example Applications
+
+To deploy the example applications, follow these steps:
+
+ cd examples/authz/servlet-authz
+ mvn wildfly:deploy
+
+If everything is correct, you will be redirect to Keycloak login page. You can login to the application with the following credentials:
+
+* username: jdoe / password: jdoe (premium user)
+* username: alice / password: alice (regular user)
+* username: admin / password: admin (administrator)
\ No newline at end of file
diff --git a/examples/authz/servlet-authz/servlet-authz-app-config.json b/examples/authz/servlet-authz/servlet-authz-app-config.json
new file mode 100644
index 0000000..d5fb1cb
--- /dev/null
+++ b/examples/authz/servlet-authz/servlet-authz-app-config.json
@@ -0,0 +1,147 @@
+{
+ "allowRemoteResourceManagement": true,
+ "policyEnforcementMode": "ENFORCING",
+ "resources": [
+ {
+ "name": "Admin Resource",
+ "uri": "/protected/admin/*",
+ "type": "http://servlet-authz/protected/admin",
+ "scopes": [
+ {
+ "name": "urn:servlet-authz:protected:admin:access"
+ }
+ ]
+ },
+ {
+ "name": "Protected Resource",
+ "uri": "/*",
+ "type": "http://servlet-authz/protected/resource",
+ "scopes": [
+ {
+ "name": "urn:servlet-authz:protected:resource:access"
+ }
+ ]
+ },
+ {
+ "name": "Premium Resource",
+ "uri": "/protected/premium/*",
+ "type": "urn:servlet-authz:protected:resource",
+ "scopes": [
+ {
+ "name": "urn:servlet-authz:protected:premium:access"
+ }
+ ]
+ },
+ {
+ "name": "Main Page",
+ "type": "urn:servlet-authz:protected:resource",
+ "scopes": [
+ {
+ "name": "urn:servlet-authz:page:main:actionForAdmin"
+ },
+ {
+ "name": "urn:servlet-authz:page:main:actionForUser"
+ },
+ {
+ "name": "urn:servlet-authz:page:main:actionForPremiumUser"
+ }
+ ]
+ }
+ ],
+ "policies": [
+ {
+ "name": "Any Admin Policy",
+ "description": "Defines that adminsitrators can do something",
+ "type": "role",
+ "config": {
+ "roles": "[\"admin\"]"
+ }
+ },
+ {
+ "name": "Any User Policy",
+ "description": "Defines that any user can do something",
+ "type": "role",
+ "config": {
+ "roles": "[\"user\"]"
+ }
+ },
+ {
+ "name": "Only Premium User Policy",
+ "description": "Defines that only premium users can do something",
+ "type": "role",
+ "logic": "POSITIVE",
+ "config": {
+ "roles": "[\"user_premium\"]"
+ }
+ },
+ {
+ "name": "All Users Policy",
+ "description": "Defines that all users can do something",
+ "type": "aggregate",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "applyPolicies": "[\"Any User Policy\",\"Any Admin Policy\",\"Only Premium User Policy\"]"
+ }
+ },
+ {
+ "name": "Premium Resource Permission",
+ "description": "A policy that defines access to premium resources",
+ "type": "resource",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Premium Resource\"]",
+ "applyPolicies": "[\"Only Premium User Policy\"]"
+ }
+ },
+ {
+ "name": "Administrative Resource Permission",
+ "description": "A policy that defines access to administrative resources",
+ "type": "resource",
+ "decisionStrategy": "UNANIMOUS",
+ "config": {
+ "resources": "[\"Admin Resource\"]",
+ "applyPolicies": "[\"Any Admin Policy\"]"
+ }
+ },
+ {
+ "name": "Protected Resource Permission",
+ "description": "A policy that defines access to any protected resource",
+ "type": "resource",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "resources": "[\"Protected Resource\"]",
+ "applyPolicies": "[\"All Users Policy\"]"
+ }
+ },
+ {
+ "name": "Action 1 on Main Page Resource Permission",
+ "description": "A policy that defines access to action 1 on the main page",
+ "type": "scope",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "scopes": "[\"urn:servlet-authz:page:main:actionForAdmin\"]",
+ "applyPolicies": "[\"Any Admin Policy\"]"
+ }
+ },
+ {
+ "name": "Action 2 on Main Page Resource Permission",
+ "description": "A policy that defines access to action 2 on the main page",
+ "type": "scope",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "scopes": "[\"urn:servlet-authz:page:main:actionForUser\"]",
+ "applyPolicies": "[\"Any User Policy\"]"
+ }
+ },
+ {
+ "name": "Action 3 on Main Page Resource Permission",
+ "description": "A policy that defines access to action 3 on the main page",
+ "type": "scope",
+ "decisionStrategy": "AFFIRMATIVE",
+ "config": {
+ "scopes": "[\"urn:servlet-authz:page:main:actionForPremiumUser\"]",
+ "applyPolicies": "[\"Only Premium User Policy\"]"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/authz/servlet-authz/servlet-authz-realm.json b/examples/authz/servlet-authz/servlet-authz-realm.json
new file mode 100644
index 0000000..371e451
--- /dev/null
+++ b/examples/authz/servlet-authz/servlet-authz-realm.json
@@ -0,0 +1,95 @@
+{
+ "realm": "servlet-authz",
+ "enabled": true,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [
+ "password"
+ ],
+ "users": [
+ {
+ "username": "alice",
+ "enabled": true,
+ "credentials": [
+ {
+ "type": "password",
+ "value": "alice"
+ }
+ ],
+ "realmRoles": [
+ "user"
+ ]
+ },
+ {
+ "username": "jdoe",
+ "enabled": true,
+ "credentials": [
+ {
+ "type": "password",
+ "value": "jdoe"
+ }
+ ],
+ "realmRoles": [
+ "user",
+ "user_premium"
+ ]
+ },
+ {
+ "username": "admin",
+ "enabled": true,
+ "credentials": [
+ {
+ "type": "password",
+ "value": "admin"
+ }
+ ],
+ "realmRoles": [
+ "user",
+ "admin"
+ ],
+ "clientRoles": {
+ "realm-management": [
+ "realm-admin"
+ ]
+ }
+ },
+ {
+ "username": "service-account-servlet-authz-app",
+ "enabled": true,
+ "serviceAccountClientId": "servlet-authz-app",
+ "clientRoles": {
+ "servlet-authz-app" : ["uma_protection"]
+ }
+ }
+ ],
+ "roles": {
+ "realm": [
+ {
+ "name": "user",
+ "description": "User privileges"
+ },
+ {
+ "name": "admin",
+ "description": "Administrator privileges"
+ },
+ {
+ "name": "user_premium",
+ "description": "User Premium privileges"
+ }
+ ]
+ },
+ "clients": [
+ {
+ "clientId": "servlet-authz-app",
+ "enabled": true,
+ "baseUrl": "/servlet-authz-app",
+ "adminUrl": "/servlet-authz-app",
+ "bearerOnly": false,
+ "authorizationServicesEnabled": true,
+ "redirectUris": [
+ "/servlet-authz-app/*"
+ ],
+ "secret": "secret"
+ }
+ ]
+}
diff --git a/examples/authz/servlet-authz/src/main/webapp/accessDenied.jsp b/examples/authz/servlet-authz/src/main/webapp/accessDenied.jsp
new file mode 100644
index 0000000..be85c22
--- /dev/null
+++ b/examples/authz/servlet-authz/src/main/webapp/accessDenied.jsp
@@ -0,0 +1,8 @@
+<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
+<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
+<html>
+ <body>
+ <h2 style="color: red">You can not access this resource. Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+ .queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to log in as a different user.</h2>
+ </body>
+</html>
\ No newline at end of file
diff --git a/examples/authz/servlet-authz/src/main/webapp/index.jsp b/examples/authz/servlet-authz/src/main/webapp/index.jsp
new file mode 100755
index 0000000..118f142
--- /dev/null
+++ b/examples/authz/servlet-authz/src/main/webapp/index.jsp
@@ -0,0 +1,38 @@
+<%@page import="org.keycloak.AuthorizationContext" %>
+<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
+<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
+<%@ page import="org.keycloak.KeycloakSecurityContext" %>
+<%@ page import="org.keycloak.representations.authorization.Permission" %>
+
+<%
+ KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
+ AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext();
+%>
+
+<html>
+<body>
+ <h2>Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+ .queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to logout.</h2>
+ <h2>This is a public resource. Try to access one of these <i>protected</i> resources:</h2>
+
+ <p><a href="protected/dynamicMenu.jsp">Dynamic Menu</a></p>
+ <p><a href="protected/premium/onlyPremium.jsp">User Premium</a></p>
+ <p><a href="protected/admin/onlyAdmin.jsp">Administration</a></p>
+
+ <h3>Your permissions are:</h3>
+
+ <ul>
+ <%
+ for (Permission permission : authzContext.getPermissions()) {
+ %>
+ <li>
+ <p>Resource: <%= permission.getResourceSetName() %></p>
+ <p>ID: <%= permission.getResourceSetId() %></p>
+ <p>Scopes: <%= permission.getScopes() %></p>
+ </li>
+ <%
+ }
+ %>
+ </ul>
+</body>
+</html>
diff --git a/examples/authz/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml b/examples/authz/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml
new file mode 100644
index 0000000..515ffa5
--- /dev/null
+++ b/examples/authz/servlet-authz/src/main/webapp/META-INF/jboss-deployment-structure.xml
@@ -0,0 +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.
+ ~
+ -->
+
+<jboss-deployment-structure>
+ <deployment>
+ <dependencies>
+ <module name="org.keycloak.keycloak-authz-client" services="import"/>
+ </dependencies>
+ </deployment>
+</jboss-deployment-structure>
\ No newline at end of file
diff --git a/examples/authz/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp b/examples/authz/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp
new file mode 100644
index 0000000..554b250
--- /dev/null
+++ b/examples/authz/servlet-authz/src/main/webapp/protected/admin/onlyAdmin.jsp
@@ -0,0 +1,8 @@
+<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
+<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
+<html>
+<body>
+ <h2>Only Administrators can access this page. Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+ .queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to logout.</h2></h2>
+</body>
+</html>
\ No newline at end of file
diff --git a/examples/authz/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp b/examples/authz/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp
new file mode 100644
index 0000000..7240a98
--- /dev/null
+++ b/examples/authz/servlet-authz/src/main/webapp/protected/dynamicMenu.jsp
@@ -0,0 +1,50 @@
+<%@page import="org.keycloak.AuthorizationContext" %>
+<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
+<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
+<%@ page import="org.keycloak.KeycloakSecurityContext" %>
+
+<%
+ KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
+ AuthorizationContext authzContext = keycloakSecurityContext.getAuthorizationContext();
+%>
+
+<html>
+<body>
+<h2>Any authenticated user can access this page. Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+ .queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to logout.</h2>
+
+<p>Here is a dynamic menu built from the permissions returned by the server:</p>
+
+<ul>
+ <%
+ if (authzContext.hasResourcePermission("Protected Resource")) {
+ %>
+ <li>
+ Do user thing
+ </li>
+ <%
+ }
+ %>
+
+ <%
+ if (authzContext.hasResourcePermission("Premium Resource")) {
+ %>
+ <li>
+ Do user premium thing
+ </li>
+ <%
+ }
+ %>
+
+ <%
+ if (authzContext.hasPermission("Admin Resource", "urn:servlet-authz:protected:admin:access")) {
+ %>
+ <li>
+ Do administration thing
+ </li>
+ <%
+ }
+ %>
+</ul>
+</body>
+</html>
\ No newline at end of file
diff --git a/examples/authz/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp b/examples/authz/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp
new file mode 100644
index 0000000..f172573
--- /dev/null
+++ b/examples/authz/servlet-authz/src/main/webapp/protected/premium/onlyPremium.jsp
@@ -0,0 +1,9 @@
+<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
+<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
+<html>
+<body>
+<h2>Only for premium users. Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+ .queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to logout.</h2>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json
new file mode 100644
index 0000000..7f37597
--- /dev/null
+++ b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json
@@ -0,0 +1,12 @@
+{
+ "realm": "servlet-authz",
+ "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "auth-server-url" : "http://localhost:8080/auth",
+ "ssl-required" : "external",
+ "resource" : "servlet-authz-app",
+ "public-client" : false,
+ "credentials": {
+ "secret": "secret"
+ },
+ "policy-enforcer": {}
+}
\ No newline at end of file
diff --git a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..14d0615
--- /dev/null
+++ b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,47 @@
+<?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>servlet-authz-app</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>servlet-authz</realm-name>
+ </login-config>
+
+ <security-role>
+ <role-name>admin</role-name>
+ </security-role>
+
+ <security-role>
+ <role-name>user</role-name>
+ </security-role>
+
+ <error-page>
+ <error-code>403</error-code>
+ <location>/accessDenied.jsp</location>
+ </error-page>
+
+</web-app>
examples/pom.xml 1(+1 -0)
diff --git a/examples/pom.xml b/examples/pom.xml
index 3d55a42..622699e 100755
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -66,5 +66,6 @@
<module>themes</module>
<module>saml</module>
<module>ldap</module>
+ <module>authz</module>
</modules>
</project>
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
index ded6c3c..c9311bd 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java
@@ -21,6 +21,7 @@ import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
+import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.transaction.LockingMode;
@@ -164,6 +165,9 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
Configuration counterCacheConfiguration = counterConfigBuilder.build();
cacheManager.defineConfiguration(InfinispanConnectionProvider.VERSION_CACHE_NAME, counterCacheConfiguration);
+
+ cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME,
+ new ConfigurationBuilder().eviction().type(EvictionType.COUNT).size(100).simpleCache(true).build());
}
}
diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
index 8a21def..0fc2bc0 100755
--- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java
@@ -32,6 +32,7 @@ public interface InfinispanConnectionProvider extends Provider {
static final String OFFLINE_SESSION_CACHE_NAME = "offlineSessions";
static final String LOGIN_FAILURE_CACHE_NAME = "loginFailures";
static final String WORK_CACHE_NAME = "work";
+ String AUTHORIZATION_CACHE_NAME = "authorization";
<K, V> Cache<K, V> getCache(String name);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java
new file mode 100644
index 0000000..f1855d3
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedPolicyStore.java
@@ -0,0 +1,407 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan;
+
+import org.infinispan.Cache;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction;
+import org.keycloak.models.authorization.infinispan.entities.CachedPolicy;
+import org.keycloak.models.entities.AbstractIdentifiableEntity;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedPolicyStore implements PolicyStore {
+
+ private static final String POLICY_ID_CACHE_PREFIX = "policy-id-";
+
+ private final Cache<String, List> cache;
+ private final KeycloakSession session;
+ private final CacheTransaction transaction;
+ private StoreFactory storeFactory;
+ private PolicyStore delegate;
+
+ public CachedPolicyStore(KeycloakSession session, CacheTransaction transaction) {
+ this.session = session;
+ this.transaction = transaction;
+ InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
+ this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
+ }
+
+ @Override
+ public Policy create(String name, String type, ResourceServer resourceServer) {
+ Policy policy = getDelegate().create(name, type, getStoreFactory().getResourceServerStore().findById(resourceServer.getId()));
+
+ return createAdapter(new CachedPolicy(policy));
+ }
+
+ @Override
+ public void delete(String id) {
+ getDelegate().delete(id);
+ this.transaction.whenComplete(() -> cache.remove(id));
+ }
+
+ @Override
+ public Policy findById(String id) {
+ String cacheKeyForPolicy = getCacheKeyForPolicy(id);
+ List<CachedPolicy> cached = this.cache.get(cacheKeyForPolicy);
+
+ if (cached == null) {
+ Policy policy = getDelegate().findById(id);
+
+ if (policy != null) {
+ return createAdapter(updatePolicyCache(policy));
+ }
+
+ return null;
+ }
+
+ return createAdapter(cached.get(0));
+ }
+
+ @Override
+ public Policy findByName(String name, String resourceServerId) {
+ return getDelegate().findByName(name, resourceServerId);
+ }
+
+ @Override
+ public List<Policy> findByResourceServer(String resourceServerId) {
+ return getDelegate().findByResourceServer(resourceServerId);
+ }
+
+ @Override
+ public List<Policy> findByResource(String resourceId) {
+ List<Policy> cache = new ArrayList<>();
+
+ for (Entry entry : this.cache.entrySet()) {
+ String cacheKey = (String) entry.getKey();
+
+ if (cacheKey.startsWith(POLICY_ID_CACHE_PREFIX)) {
+ List<CachedPolicy> value = (List<CachedPolicy>) entry.getValue();
+ CachedPolicy policy = value.get(0);
+
+ if (policy.getResourcesIds().contains(resourceId)) {
+ cache.add(findById(policy.getId()));
+ }
+ }
+ }
+
+ if (cache.isEmpty()) {
+ getDelegate().findByResource(resourceId).forEach(policy -> cache.add(findById(updatePolicyCache(policy).getId())));
+ }
+
+ return cache;
+ }
+
+ @Override
+ public List<Policy> findByResourceType(String resourceType, String resourceServerId) {
+ List<Policy> cache = new ArrayList<>();
+
+ for (Entry entry : this.cache.entrySet()) {
+ String cacheKey = (String) entry.getKey();
+
+ if (cacheKey.startsWith(POLICY_ID_CACHE_PREFIX)) {
+ List<CachedPolicy> value = (List<CachedPolicy>) entry.getValue();
+ CachedPolicy policy = value.get(0);
+
+ if (policy.getResourceServerId().equals(resourceServerId) && policy.getConfig().getOrDefault("defaultResourceType", "").equals(resourceType)) {
+ cache.add(findById(policy.getId()));
+ }
+ }
+ }
+
+ if (cache.isEmpty()) {
+ getDelegate().findByResourceType(resourceType, resourceServerId).forEach(policy -> cache.add(findById(updatePolicyCache(policy).getId())));
+ }
+
+ return cache;
+ }
+
+ @Override
+ public List<Policy> findByScopeIds(List<String> scopeIds, String resourceServerId) {
+ List<Policy> cache = new ArrayList<>();
+
+ for (Entry entry : this.cache.entrySet()) {
+ String cacheKey = (String) entry.getKey();
+
+ if (cacheKey.startsWith(POLICY_ID_CACHE_PREFIX)) {
+ List<CachedPolicy> value = (List<CachedPolicy>) entry.getValue();
+ CachedPolicy policy = value.get(0);
+
+ for (String scopeId : policy.getScopesIds()) {
+ if (scopeIds.contains(scopeId)) {
+ cache.add(findById(policy.getId()));
+ break;
+ }
+ }
+ }
+ }
+
+ if (cache.isEmpty()) {
+ getDelegate().findByScopeIds(scopeIds, resourceServerId).forEach(policy -> cache.add(findById(updatePolicyCache(policy).getId())));
+ }
+
+ return cache;
+ }
+
+ @Override
+ public List<Policy> findByType(String type) {
+ return getDelegate().findByType(type);
+ }
+
+ @Override
+ public List<Policy> findDependentPolicies(String id) {
+ return getDelegate().findDependentPolicies(id);
+ }
+
+ private String getCacheKeyForPolicy(String policyId) {
+ return POLICY_ID_CACHE_PREFIX + policyId;
+ }
+
+ private StoreFactory getStoreFactory() {
+ if (this.storeFactory == null) {
+ this.storeFactory = this.session.getProvider(StoreFactory.class);
+ }
+
+ return this.storeFactory;
+ }
+
+ private PolicyStore getDelegate() {
+ if (this.delegate == null) {
+ this.delegate = getStoreFactory().getPolicyStore();
+ }
+
+ return this.delegate;
+ }
+
+ private Policy createAdapter(CachedPolicy cached) {
+ return new Policy() {
+
+ private Policy updated;
+
+ @Override
+ public String getId() {
+ return cached.getId();
+ }
+
+ @Override
+ public String getType() {
+ return cached.getType();
+ }
+
+ @Override
+ public DecisionStrategy getDecisionStrategy() {
+ return cached.getDecisionStrategy();
+ }
+
+ @Override
+ public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
+ getDelegateForUpdate().setDecisionStrategy(decisionStrategy);
+ cached.setDecisionStrategy(decisionStrategy);
+ }
+
+ @Override
+ public Logic getLogic() {
+ return cached.getLogic();
+ }
+
+ @Override
+ public void setLogic(Logic logic) {
+ getDelegateForUpdate().setLogic(logic);
+ cached.setLogic(logic);
+ }
+
+ @Override
+ public Map<String, String> getConfig() {
+ return cached.getConfig();
+ }
+
+ @Override
+ public void setConfig(Map<String, String> config) {
+ getDelegateForUpdate().setConfig(config);
+ cached.setConfig(config);
+ }
+
+ @Override
+ public String getName() {
+ return cached.getName();
+ }
+
+ @Override
+ public void setName(String name) {
+ getDelegateForUpdate().setName(name);
+ cached.setName(name);
+ }
+
+ @Override
+ public String getDescription() {
+ return cached.getDescription();
+ }
+
+ @Override
+ public void setDescription(String description) {
+ getDelegateForUpdate().setDescription(description);
+ cached.setDescription(description);
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ return getStoreFactory().getResourceServerStore().findById(cached.getResourceServerId());
+ }
+
+ @Override
+ public void addScope(Scope scope) {
+ getDelegateForUpdate().addScope(getStoreFactory().getScopeStore().findById(scope.getId()));
+ cached.addScope(scope);
+ }
+
+ @Override
+ public void removeScope(Scope scope) {
+ getDelegateForUpdate().removeScope(getStoreFactory().getScopeStore().findById(scope.getId()));
+ cached.removeScope(scope);
+ }
+
+ @Override
+ public void addAssociatedPolicy(Policy associatedPolicy) {
+ getDelegateForUpdate().addAssociatedPolicy(getStoreFactory().getPolicyStore().findById(associatedPolicy.getId()));
+ cached.addAssociatedPolicy(associatedPolicy);
+ }
+
+ @Override
+ public void removeAssociatedPolicy(Policy associatedPolicy) {
+ getDelegateForUpdate().removeAssociatedPolicy(getStoreFactory().getPolicyStore().findById(associatedPolicy.getId()));
+ cached.removeAssociatedPolicy(associatedPolicy);
+ }
+
+ @Override
+ public void addResource(Resource resource) {
+ getDelegateForUpdate().addResource(getStoreFactory().getResourceStore().findById(resource.getId()));
+ cached.addResource(resource);
+ }
+
+ @Override
+ public void removeResource(Resource resource) {
+ getDelegateForUpdate().removeResource(getStoreFactory().getResourceStore().findById(resource.getId()));
+ cached.removeResource(resource);
+ }
+
+ @Override
+ public Set<Policy> getAssociatedPolicies() {
+ Set<Policy> associatedPolicies = new HashSet<>();
+
+ for (String id : cached.getAssociatedPoliciesIds()) {
+ Policy cached = findById(id);
+
+ if (cached != null) {
+ associatedPolicies.add(cached);
+ }
+ }
+
+ return associatedPolicies;
+ }
+
+ @Override
+ public Set<Resource> getResources() {
+ Set<Resource> resources = new HashSet<>();
+
+ for (String id : cached.getResourcesIds()) {
+ Resource cached = getStoreFactory().getResourceStore().findById(id);
+
+ if (cached != null) {
+ resources.add(cached);
+ }
+ }
+
+ return resources;
+ }
+
+ @Override
+ public Set<Scope> getScopes() {
+ Set<Scope> scopes = new HashSet<>();
+
+ for (String id : cached.getScopesIds()) {
+ Scope cached = getStoreFactory().getScopeStore().findById(id);
+
+ if (cached != null) {
+ scopes.add(cached);
+ }
+ }
+
+ return scopes;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+
+ if (getId() == null) return false;
+
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Policy that = (Policy) o;
+
+ if (!getId().equals(that.getId())) return false;
+
+ return true;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return getId()!=null ? getId().hashCode() : super.hashCode();
+ }
+
+ private Policy getDelegateForUpdate() {
+ if (this.updated == null) {
+ this.updated = getDelegate().findById(getId());
+ if (this.updated == null) throw new IllegalStateException("Not found in database");
+ transaction.whenComplete(() -> cache.evict(getCacheKeyForPolicy(getId())));
+ }
+
+ return this.updated;
+ }
+ };
+ }
+
+ private CachedPolicy updatePolicyCache(Policy policy) {
+ CachedPolicy cached = new CachedPolicy(policy);
+ List<Policy> cache = new ArrayList<>();
+
+ cache.add(cached);
+
+ this.cache.put(getCacheKeyForPolicy(policy.getId()), cache);
+
+ return cached;
+ }
+
+}
\ No newline at end of file
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java
new file mode 100644
index 0000000..5779ae1
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceServerStore.java
@@ -0,0 +1,187 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan;
+
+import org.infinispan.Cache;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction;
+import org.keycloak.models.authorization.infinispan.entities.CachedResourceServer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedResourceServerStore implements ResourceServerStore {
+
+ private static final String RS_ID_CACHE_PREFIX = "rs-id-";
+
+ private final KeycloakSession session;
+ private final CacheTransaction transaction;
+ private StoreFactory storeFactory;
+ private ResourceServerStore delegate;
+ private final Cache<String, List> cache;
+
+ public CachedResourceServerStore(KeycloakSession session, CacheTransaction transaction) {
+ this.session = session;
+ this.transaction = transaction;
+ InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
+ this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
+ }
+
+ @Override
+ public ResourceServer create(String clientId) {
+ ResourceServer resourceServer = getDelegate().create(clientId);
+
+ return createAdapter(new CachedResourceServer(resourceServer));
+ }
+
+ @Override
+ public void delete(String id) {
+ getDelegate().delete(id);
+ this.transaction.whenComplete(() -> this.cache.remove(getCacheKeyForResourceServer(id)));
+ }
+
+ @Override
+ public ResourceServer findById(String id) {
+ String cacheKeyForResourceServer = getCacheKeyForResourceServer(id);
+ List<ResourceServer> cached = this.cache.get(cacheKeyForResourceServer);
+
+ if (cached == null) {
+ ResourceServer resourceServer = getDelegate().findById(id);
+
+ if (resourceServer != null) {
+ return createAdapter(updateResourceServerCache(resourceServer));
+ }
+
+ return null;
+ }
+
+ return createAdapter(cached.get(0));
+ }
+
+ @Override
+ public ResourceServer findByClient(String id) {
+ for (Map.Entry entry : this.cache.entrySet()) {
+ String cacheKey = (String) entry.getKey();
+
+ if (cacheKey.startsWith(RS_ID_CACHE_PREFIX)) {
+ List<ResourceServer> cache = (List<ResourceServer>) entry.getValue();
+ ResourceServer resourceServer = cache.get(0);
+
+ if (resourceServer.getClientId().equals(id)) {
+ return findById(resourceServer.getId());
+ }
+ }
+ }
+
+ ResourceServer resourceServer = getDelegate().findByClient(id);
+
+ if (resourceServer != null) {
+ return findById(updateResourceServerCache(resourceServer).getId());
+ }
+
+ return null;
+ }
+
+ private String getCacheKeyForResourceServer(String id) {
+ return RS_ID_CACHE_PREFIX + id;
+ }
+
+ private ResourceServerStore getDelegate() {
+ if (this.delegate == null) {
+ this.delegate = getStoreFactory().getResourceServerStore();
+ }
+
+ return this.delegate;
+ }
+
+ private StoreFactory getStoreFactory() {
+ if (this.storeFactory == null) {
+ this.storeFactory = session.getProvider(StoreFactory.class);
+ }
+
+ return this.storeFactory;
+ }
+ private ResourceServer createAdapter(ResourceServer cached) {
+ return new ResourceServer() {
+
+ private ResourceServer updated;
+
+ @Override
+ public String getId() {
+ return cached.getId();
+ }
+
+ @Override
+ public String getClientId() {
+ return cached.getClientId();
+ }
+
+ @Override
+ public boolean isAllowRemoteResourceManagement() {
+ return cached.isAllowRemoteResourceManagement();
+ }
+
+ @Override
+ public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) {
+ getDelegateForUpdate().setAllowRemoteResourceManagement(allowRemoteResourceManagement);
+ cached.setAllowRemoteResourceManagement(allowRemoteResourceManagement);
+ }
+
+ @Override
+ public PolicyEnforcementMode getPolicyEnforcementMode() {
+ return cached.getPolicyEnforcementMode();
+ }
+
+ @Override
+ public void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode) {
+ getDelegateForUpdate().setPolicyEnforcementMode(enforcementMode);
+ cached.setPolicyEnforcementMode(enforcementMode);
+ }
+
+ private ResourceServer getDelegateForUpdate() {
+ if (this.updated == null) {
+ this.updated = getDelegate().findById(getId());
+ if (this.updated == null) throw new IllegalStateException("Not found in database");
+ transaction.whenComplete(() -> cache.evict(getCacheKeyForResourceServer(getId())));
+ }
+
+ return this.updated;
+ }
+ };
+ }
+
+ private CachedResourceServer updateResourceServerCache(ResourceServer resourceServer) {
+ CachedResourceServer cached = new CachedResourceServer(resourceServer);
+ List<ResourceServer> cache = new ArrayList<>();
+
+ cache.add(cached);
+
+ this.cache.put(getCacheKeyForResourceServer(resourceServer.getId()), cache);
+
+ return cached;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java
new file mode 100644
index 0000000..9e1ea14
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedResourceStore.java
@@ -0,0 +1,299 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan;
+
+import org.infinispan.Cache;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction;
+import org.keycloak.models.authorization.infinispan.entities.CachedResource;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedResourceStore implements ResourceStore {
+
+ private static final String RESOURCE_ID_CACHE_PREFIX = "rsc-id-";
+
+ private final KeycloakSession session;
+ private final CacheTransaction transaction;
+ private StoreFactory storeFactory;
+ private ResourceStore delegate;
+ private final Cache<String, List> cache;
+
+ public CachedResourceStore(KeycloakSession session, CacheTransaction transaction) {
+ this.session = session;
+ InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
+ this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
+ this.transaction = transaction;
+ }
+
+ @Override
+ public Resource create(String name, ResourceServer resourceServer, String owner) {
+ Resource resource = getDelegate().create(name, getStoreFactory().getResourceServerStore().findById(resourceServer.getId()), owner);
+
+ return createAdapter(new CachedResource(resource));
+ }
+
+ @Override
+ public void delete(String id) {
+ this.cache.evict(getCacheKeyForResource(id));
+ getDelegate().delete(id);
+ }
+
+ @Override
+ public Resource findById(String id) {
+ String cacheKeyForResource = getCacheKeyForResource(id);
+ List<CachedResource> cached = this.cache.get(cacheKeyForResource);
+
+ if (cached == null) {
+ Resource resource = getDelegate().findById(id);
+
+ if (resource != null) {
+ return createAdapter(updateResourceCache(resource));
+ }
+
+ return null;
+ }
+
+ return createAdapter(cached.get(0));
+ }
+
+ @Override
+ public List<Resource> findByOwner(String ownerId) {
+ List<Resource> cache = new ArrayList<>();
+
+ for (Entry entry : this.cache.entrySet()) {
+ String cacheKey = (String) entry.getKey();
+
+ if (cacheKey.startsWith(RESOURCE_ID_CACHE_PREFIX)) {
+ List<Resource> value = (List<Resource>) entry.getValue();
+ Resource resource = value.get(0);
+
+ if (resource.getOwner().equals(ownerId)) {
+ cache.add(findById(resource.getId()));
+ }
+ }
+ }
+
+ if (cache.isEmpty()) {
+ getDelegate().findByOwner(ownerId).forEach(resource -> cache.add(findById(updateResourceCache(resource).getId())));
+ }
+
+ return cache;
+ }
+
+ @Override
+ public List<Resource> findByResourceServer(String resourceServerId) {
+ return getDelegate().findByResourceServer(resourceServerId);
+ }
+
+ @Override
+ public List<Resource> findByScope(String... id) {
+ return getDelegate().findByScope(id);
+ }
+
+ @Override
+ public Resource findByName(String name, String resourceServerId) {
+ for (Entry entry : this.cache.entrySet()) {
+ String cacheKey = (String) entry.getKey();
+
+ if (cacheKey.startsWith(RESOURCE_ID_CACHE_PREFIX)) {
+ List<CachedResource> value = (List<CachedResource>) entry.getValue();
+ CachedResource resource = value.get(0);
+
+ if (resource.getResourceServerId().equals(resourceServerId) && resource.getName().equals(name)) {
+ return findById(resource.getId());
+ }
+ }
+ }
+
+ Resource resource = getDelegate().findByName(name, resourceServerId);
+
+ if (resource != null) {
+ return findById(updateResourceCache(resource).getId());
+ }
+
+ return null;
+ }
+
+ @Override
+ public List<Resource> findByType(String type) {
+ List<Resource> cache = new ArrayList<>();
+
+ for (Entry entry : this.cache.entrySet()) {
+ String cacheKey = (String) entry.getKey();
+
+ if (cacheKey.startsWith(RESOURCE_ID_CACHE_PREFIX)) {
+ List<Resource> value = (List<Resource>) entry.getValue();
+ Resource resource = value.get(0);
+
+ if (resource.getType().equals(type)) {
+ cache.add(findById(resource.getId()));
+ }
+ }
+ }
+
+ if (cache.isEmpty()) {
+ getDelegate().findByType(type).forEach(resource -> cache.add(findById(updateResourceCache(resource).getId())));
+ }
+
+ return cache;
+ }
+
+ private String getCacheKeyForResource(String id) {
+ return RESOURCE_ID_CACHE_PREFIX + id;
+ }
+
+ private ResourceStore getDelegate() {
+ if (this.delegate == null) {
+ this.delegate = getStoreFactory().getResourceStore();
+ }
+
+ return this.delegate;
+ }
+
+ private StoreFactory getStoreFactory() {
+ if (this.storeFactory == null) {
+ this.storeFactory = session.getProvider(StoreFactory.class);
+ }
+
+ return this.storeFactory;
+ }
+
+ private Resource createAdapter(CachedResource cached) {
+ return new Resource() {
+
+ private List<Scope> scopes;
+ private Resource updated;
+
+ @Override
+ public String getId() {
+ return cached.getId();
+ }
+
+ @Override
+ public String getName() {
+ return cached.getName();
+ }
+
+ @Override
+ public void setName(String name) {
+ getDelegateForUpdate().setName(name);
+ cached.setName(name);
+ }
+
+ @Override
+ public String getUri() {
+ return cached.getUri();
+ }
+
+ @Override
+ public void setUri(String uri) {
+ getDelegateForUpdate().setUri(uri);
+ cached.setUri(uri);
+ }
+
+ @Override
+ public String getType() {
+ return cached.getType();
+ }
+
+ @Override
+ public void setType(String type) {
+ getDelegateForUpdate().setType(type);
+ cached.setType(type);
+ }
+
+ @Override
+ public List<Scope> getScopes() {
+ List<Scope> scopes = new ArrayList<>();
+
+ for (String id : cached.getScopesIds()) {
+ Scope cached = getStoreFactory().getScopeStore().findById(id);
+
+ if (cached != null) {
+ scopes.add(cached);
+ }
+ }
+
+ return scopes;
+ }
+
+ @Override
+ public String getIconUri() {
+ return cached.getIconUri();
+ }
+
+ @Override
+ public void setIconUri(String iconUri) {
+ getDelegateForUpdate().setIconUri(iconUri);
+ cached.setIconUri(iconUri);
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ return getStoreFactory().getResourceServerStore().findById(cached.getResourceServerId());
+ }
+
+ @Override
+ public String getOwner() {
+ return cached.getOwner();
+ }
+
+ @Override
+ public void updateScopes(Set<Scope> scopes) {
+ getDelegateForUpdate().updateScopes(scopes.stream().map(scope -> getStoreFactory().getScopeStore().findById(scope.getId())).collect(Collectors.toSet()));
+ cached.updateScopes(scopes);
+ }
+
+ private Resource getDelegateForUpdate() {
+ if (this.updated == null) {
+ this.updated = getDelegate().findById(getId());
+ if (this.updated == null) throw new IllegalStateException("Not found in database");
+ transaction.whenComplete(() -> cache.evict(getCacheKeyForResource(getId())));
+ }
+
+ return this.updated;
+ }
+ };
+ }
+
+ private CachedResource updateResourceCache(Resource resource) {
+ CachedResource cached = new CachedResource(resource);
+ List cache = new ArrayList<>();
+
+ cache.add(cached);
+
+ this.cache.put(getCacheKeyForResource(resource.getId()), cache);
+
+ return cached;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java
new file mode 100644
index 0000000..5912645
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/CachedScopeStore.java
@@ -0,0 +1,195 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan;
+
+import org.infinispan.Cache;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.authorization.infinispan.InfinispanStoreFactoryProvider.CacheTransaction;
+import org.keycloak.models.authorization.infinispan.entities.CachedScope;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedScopeStore implements ScopeStore {
+
+ private static final String SCOPE_ID_CACHE_PREFIX = "scp-id-";
+
+ private final Cache<String, List> cache;
+ private final KeycloakSession session;
+ private final CacheTransaction transaction;
+ private ScopeStore delegate;
+ private StoreFactory storeFactory;
+
+ public CachedScopeStore(KeycloakSession session, CacheTransaction transaction) {
+ this.session = session;
+ this.transaction = transaction;
+ InfinispanConnectionProvider provider = session.getProvider(InfinispanConnectionProvider.class);
+ this.cache = provider.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME);
+ }
+
+ @Override
+ public Scope create(String name, ResourceServer resourceServer) {
+ Scope scope = getDelegate().create(name, getStoreFactory().getResourceServerStore().findById(resourceServer.getId()));
+
+ return createAdapter(new CachedScope(scope));
+ }
+
+ @Override
+ public void delete(String id) {
+ getDelegate().delete(id);
+ this.transaction.whenComplete(() -> cache.remove(getCacheKeyForScope(id)));
+ }
+
+ @Override
+ public Scope findById(String id) {
+ String cacheKeyForScope = getCacheKeyForScope(id);
+ List<CachedScope> cached = this.cache.get(cacheKeyForScope);
+
+ if (cached == null) {
+ Scope scope = getDelegate().findById(id);
+
+ if (scope != null) {
+ return createAdapter(updateScopeCache(scope));
+ }
+
+ return null;
+ }
+
+ return createAdapter(cached.get(0));
+ }
+
+ @Override
+ public Scope findByName(String name, String resourceServerId) {
+ for (Entry entry : this.cache.entrySet()) {
+ String cacheKey = (String) entry.getKey();
+
+ if (cacheKey.startsWith(SCOPE_ID_CACHE_PREFIX)) {
+ List<CachedScope> cache = (List<CachedScope>) entry.getValue();
+ CachedScope scope = cache.get(0);
+
+ if (scope.getResourceServerId().equals(resourceServerId) && scope.getName().equals(name)) {
+ return findById(scope.getId());
+ }
+ }
+ }
+
+ Scope scope = getDelegate().findByName(name, resourceServerId);
+
+ if (scope != null) {
+ return findById(updateScopeCache(scope).getId());
+ }
+
+ return null;
+ }
+
+ @Override
+ public List<Scope> findByResourceServer(String id) {
+ return getDelegate().findByResourceServer(id);
+ }
+
+ private String getCacheKeyForScope(String id) {
+ return SCOPE_ID_CACHE_PREFIX + id;
+ }
+
+ private ScopeStore getDelegate() {
+ if (this.delegate == null) {
+ this.delegate = getStoreFactory().getScopeStore();
+ }
+
+ return this.delegate;
+ }
+
+ private StoreFactory getStoreFactory() {
+ if (this.storeFactory == null) {
+ this.storeFactory = session.getProvider(StoreFactory.class);
+ }
+
+ return this.storeFactory;
+ }
+
+ private Scope createAdapter(CachedScope cached) {
+ return new Scope() {
+
+ private Scope updated;
+
+ @Override
+ public String getId() {
+ return cached.getId();
+ }
+
+ @Override
+ public String getName() {
+ return cached.getName();
+ }
+
+ @Override
+ public void setName(String name) {
+ getDelegateForUpdate().setName(name);
+ cached.setName(name);
+ }
+
+ @Override
+ public String getIconUri() {
+ return cached.getIconUri();
+ }
+
+ @Override
+ public void setIconUri(String iconUri) {
+ getDelegateForUpdate().setIconUri(iconUri);
+ cached.setIconUri(iconUri);
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ return getStoreFactory().getResourceServerStore().findById(cached.getResourceServerId());
+ }
+
+ private Scope getDelegateForUpdate() {
+ if (this.updated == null) {
+ this.updated = getDelegate().findById(getId());
+ if (this.updated == null) throw new IllegalStateException("Not found in database");
+ transaction.whenComplete(() -> cache.evict(getCacheKeyForScope(getId())));
+ }
+
+ return this.updated;
+ }
+ };
+ }
+
+ private CachedScope updateScopeCache(Scope scope) {
+ CachedScope cached = new CachedScope(scope);
+
+ List cache = new ArrayList();
+
+ cache.add(cached);
+
+ this.transaction.whenComplete(() -> this.cache.put(getCacheKeyForScope(scope.getId()), cache));
+
+ return cached;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedPolicy.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedPolicy.java
new file mode 100644
index 0000000..6c6230b
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedPolicy.java
@@ -0,0 +1,213 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan.entities;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.models.entities.AbstractIdentifiableEntity;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedPolicy implements Policy {
+
+ private String id;
+ private String type;
+ private DecisionStrategy decisionStrategy;
+ private Logic logic;
+ private Map<String, String> config;
+ private String name;
+ private String description;
+ private String resourceServerId;
+ private Set<String> associatedPoliciesIds;
+ private Set<String> resourcesIds;
+ private Set<String> scopesIds;
+
+ public CachedPolicy(Policy policy) {
+ this.id = policy.getId();
+ this.type = policy.getType();
+ this.decisionStrategy = policy.getDecisionStrategy();
+ this.logic = policy.getLogic();
+ this.config = new HashMap(policy.getConfig());
+ this.name = policy.getName();
+ this.description = policy.getDescription();
+ this.resourceServerId = policy.getResourceServer().getId();
+ this.associatedPoliciesIds = policy.getAssociatedPolicies().stream().map(Policy::getId).collect(Collectors.toSet());
+ this.resourcesIds = policy.getResources().stream().map(Resource::getId).collect(Collectors.toSet());
+ this.scopesIds = policy.getScopes().stream().map(Scope::getId).collect(Collectors.toSet());
+ }
+
+ public CachedPolicy(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getId() {
+ return this.id;
+ }
+
+ @Override
+ public String getType() {
+ return this.type;
+ }
+
+ @Override
+ public DecisionStrategy getDecisionStrategy() {
+ return this.decisionStrategy;
+ }
+
+ @Override
+ public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
+ this.decisionStrategy = decisionStrategy;
+ }
+
+ @Override
+ public Logic getLogic() {
+ return this.logic;
+ }
+
+ @Override
+ public void setLogic(Logic logic) {
+ this.logic = logic;
+ }
+
+ @Override
+ public Map<String, String> getConfig() {
+ return this.config;
+ }
+
+ @Override
+ public void setConfig(Map<String, String> config) {
+ this.config = config;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getDescription() {
+ return this.description;
+ }
+
+ @Override
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ throw new RuntimeException("Not implemented");
+ }
+
+ @Override
+ public void addScope(Scope scope) {
+ this.scopesIds.add(scope.getId());
+ }
+
+ @Override
+ public void removeScope(Scope scope) {
+ this.scopesIds.remove(scope.getId());
+ }
+
+ @Override
+ public void addAssociatedPolicy(Policy associatedPolicy) {
+ this.associatedPoliciesIds.add(associatedPolicy.getId());
+ }
+
+ @Override
+ public void removeAssociatedPolicy(Policy associatedPolicy) {
+ this.associatedPoliciesIds.remove(associatedPolicy.getId());
+ }
+
+ @Override
+ public void addResource(Resource resource) {
+ this.resourcesIds.add(resource.getId());
+ }
+
+ @Override
+ public void removeResource(Resource resource) {
+ this.resourcesIds.add(resource.getId());
+ }
+
+ @Override
+ public Set<Policy> getAssociatedPolicies() {
+ throw new RuntimeException("Not implemented");
+ }
+
+ @Override
+ public Set<Resource> getResources() {
+ throw new RuntimeException("Not implemented");
+ }
+
+ @Override
+ public Set<Scope> getScopes() {
+ throw new RuntimeException("Not implemented");
+ }
+
+ public Set<String> getAssociatedPoliciesIds() {
+ return this.associatedPoliciesIds;
+ }
+
+ public Set<String> getResourcesIds() {
+ return this.resourcesIds;
+ }
+
+ public Set<String> getScopesIds() {
+ return this.scopesIds;
+ }
+
+ public String getResourceServerId() {
+ return this.resourceServerId;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+
+ if (this.id == null) return false;
+
+ if (o == null || getClass() != o.getClass()) return false;
+
+ AbstractIdentifiableEntity that = (AbstractIdentifiableEntity) o;
+
+ if (!getId().equals(that.getId())) return false;
+
+ return true;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return id!=null ? id.hashCode() : super.hashCode();
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResource.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResource.java
new file mode 100644
index 0000000..8af333b
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResource.java
@@ -0,0 +1,131 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan.entities;
+
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedResource implements Resource {
+
+ private final String id;
+ private String resourceServerId;
+ private String iconUri;
+ private String owner;
+ private String type;
+ private String name;
+ private String uri;
+ private Set<String> scopesIds;
+
+ public CachedResource(Resource resource) {
+ this.id = resource.getId();
+ this.name = resource.getName();
+ this.uri = resource.getUri();
+ this.type = resource.getType();
+ this.owner = resource.getOwner();
+ this.iconUri = resource.getIconUri();
+ this.resourceServerId = resource.getResourceServer().getId();
+ this.scopesIds = resource.getScopes().stream().map(Scope::getId).collect(Collectors.toSet());
+ }
+
+ public CachedResource(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getId() {
+ return this.id;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getUri() {
+ return this.uri;
+ }
+
+ @Override
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ @Override
+ public String getType() {
+ return this.type;
+ }
+
+ @Override
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public List<Scope> getScopes() {
+ throw new RuntimeException("Not implemented");
+ }
+
+ @Override
+ public String getIconUri() {
+ return this.iconUri;
+ }
+
+ @Override
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ throw new RuntimeException("Not implemented");
+ }
+
+ @Override
+ public String getOwner() {
+ return this.owner;
+ }
+
+ @Override
+ public void updateScopes(Set<Scope> scopes) {
+ this.scopesIds.clear();
+ this.scopesIds.addAll(scopes.stream().map(Scope::getId).collect(Collectors.toSet()));
+ }
+
+ public String getResourceServerId() {
+ return this.resourceServerId;
+ }
+
+ public Set<String> getScopesIds() {
+ return this.scopesIds;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResourceServer.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResourceServer.java
new file mode 100644
index 0000000..fe59510
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedResourceServer.java
@@ -0,0 +1,73 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan.entities;
+
+import org.keycloak.authorization.model.ResourceServer;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedResourceServer implements ResourceServer {
+
+ private final String id;
+ private String clientId;
+ private boolean allowRemoteResourceManagement;
+ private PolicyEnforcementMode policyEnforcementMode;
+
+ public CachedResourceServer(ResourceServer resourceServer) {
+ this.id = resourceServer.getId();
+ this.clientId = resourceServer.getClientId();
+ this.allowRemoteResourceManagement = resourceServer.isAllowRemoteResourceManagement();
+ this.policyEnforcementMode = resourceServer.getPolicyEnforcementMode();
+ }
+
+ public CachedResourceServer(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getId() {
+ return this.id;
+ }
+
+ @Override
+ public String getClientId() {
+ return this.clientId;
+ }
+
+ @Override
+ public boolean isAllowRemoteResourceManagement() {
+ return this.allowRemoteResourceManagement;
+ }
+
+ @Override
+ public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) {
+ this.allowRemoteResourceManagement = allowRemoteResourceManagement;
+ }
+
+ @Override
+ public PolicyEnforcementMode getPolicyEnforcementMode() {
+ return this.policyEnforcementMode;
+ }
+
+ @Override
+ public void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode) {
+ this.policyEnforcementMode = enforcementMode;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedScope.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedScope.java
new file mode 100644
index 0000000..1cba6a2
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/entities/CachedScope.java
@@ -0,0 +1,78 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan.entities;
+
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedScope implements Scope {
+
+ private final String id;
+ private String resourceServerId;
+ private String name;
+ private String iconUri;
+
+ public CachedScope(Scope scope) {
+ this.id = scope.getId();
+ this.name = scope.getName();
+ this.iconUri = scope.getIconUri();
+ this.resourceServerId = scope.getResourceServer().getId();
+ }
+
+ public CachedScope(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getId() {
+ return this.id;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getIconUri() {
+ return this.iconUri;
+ }
+
+ @Override
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ throw new RuntimeException("Not implemented");
+ }
+
+ public String getResourceServerId() {
+ return this.resourceServerId;
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java
new file mode 100644
index 0000000..df9f262
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreFactoryProvider.java
@@ -0,0 +1,109 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan;
+
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class InfinispanStoreFactoryProvider implements CachedStoreFactoryProvider {
+
+ private final KeycloakSession session;
+ private final CacheTransaction transaction;
+
+ InfinispanStoreFactoryProvider(KeycloakSession delegate) {
+ this.session = delegate;
+ this.transaction = new CacheTransaction();
+ this.session.getTransaction().enlistAfterCompletion(transaction);
+ }
+
+ @Override
+ public ResourceStore getResourceStore() {
+ return new CachedResourceStore(this.session, this.transaction);
+ }
+
+ @Override
+ public ResourceServerStore getResourceServerStore() {
+ return new CachedResourceServerStore(this.session, this.transaction);
+ }
+
+ @Override
+ public ScopeStore getScopeStore() {
+ return new CachedScopeStore(this.session, this.transaction);
+ }
+
+ @Override
+ public PolicyStore getPolicyStore() {
+ return new CachedPolicyStore(this.session, this.transaction);
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ static class CacheTransaction implements KeycloakTransaction {
+
+ private List<Runnable> completeTasks = new ArrayList<>();
+
+ @Override
+ public void begin() {
+
+ }
+
+ @Override
+ public void commit() {
+ this.completeTasks.forEach(task -> task.run());
+ }
+
+ @Override
+ public void rollback() {
+ this.completeTasks.forEach(task -> task.run());
+ }
+
+ @Override
+ public void setRollbackOnly() {
+
+ }
+
+ @Override
+ public boolean getRollbackOnly() {
+ return false;
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ protected void whenComplete(Runnable task) {
+ this.completeTasks.add(task);
+ }
+ }
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreProviderFactory.java
new file mode 100644
index 0000000..8015fc2
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/authorization/infinispan/InfinispanStoreProviderFactory.java
@@ -0,0 +1,56 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.authorization.infinispan;
+
+import org.keycloak.Config;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
+import org.keycloak.models.cache.authorization.CachedStoreProviderFactory;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class InfinispanStoreProviderFactory implements CachedStoreProviderFactory {
+ @Override
+ public CachedStoreFactoryProvider create(KeycloakSession session) {
+ return new InfinispanStoreFactoryProvider(session);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "infinispan-authz-store-factory";
+ }
+}
diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.authorization.CachedStoreProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.authorization.CachedStoreProviderFactory
new file mode 100644
index 0000000..0a46bb3
--- /dev/null
+++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.models.cache.authorization.CachedStoreProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.models.authorization.infinispan.InfinispanStoreProviderFactory
\ No newline at end of file
model/jpa/pom.xml 5(+5 -0)
diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml
index c1176f3..47035c5 100755
--- a/model/jpa/pom.xml
+++ b/model/jpa/pom.xml
@@ -30,6 +30,11 @@
<name>Keycloak Model JPA</name>
<description/>
+ <properties>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ </properties>
+
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java
new file mode 100644
index 0000000..ddaf637
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/PolicyEntity.java
@@ -0,0 +1,251 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.jpa.entities;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.models.entities.AbstractIdentifiableEntity;
+
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.CollectionTable;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.persistence.ManyToOne;
+import javax.persistence.MapKeyColumn;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@Entity
+@Table(name = "RESOURCE_SERVER_POLICY", uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"NAME", "RESOURCE_SERVER_ID"})
+})
+public class PolicyEntity implements Policy {
+
+ @Id
+ @Column(name="ID", length = 36)
+ @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
+ private String id;
+
+ @Column(name = "NAME")
+ private String name;
+
+ @Column(name = "DESCRIPTION")
+ private String description;
+
+ @Column(name = "TYPE")
+ private String type;
+
+ @Column(name = "DECISION_STRATEGY")
+ private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS;
+
+ @Column(name = "LOGIC")
+ private Logic logic = Logic.POSITIVE;
+
+ @ElementCollection
+ @MapKeyColumn(name="NAME")
+ @Column(name="VALUE", columnDefinition = "TEXT")
+ @CollectionTable(name="POLICY_CONFIG", joinColumns={ @JoinColumn(name="POLICY_ID") })
+ private Map<String, String> config = new HashMap();
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "RESOURCE_SERVER_ID")
+ private ResourceServerEntity resourceServer;
+
+ @ManyToMany(fetch = FetchType.LAZY, cascade = {})
+ @JoinTable(name = "ASSOCIATED_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "ASSOCIATED_POLICY_ID"))
+ private Set<PolicyEntity> associatedPolicies = new HashSet<>();
+
+ @ManyToMany(fetch = FetchType.LAZY, cascade = {})
+ @JoinTable(name = "RESOURCE_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "RESOURCE_ID"))
+ private Set<ResourceEntity> resources = new HashSet<>();
+
+ @ManyToMany(fetch = FetchType.EAGER, cascade = {})
+ @JoinTable(name = "SCOPE_POLICY", joinColumns = @JoinColumn(name = "POLICY_ID"), inverseJoinColumns = @JoinColumn(name = "SCOPE_ID"))
+ private Set<ScopeEntity> scopes = new HashSet<>();
+
+ @Override
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getType() {
+ return this.type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public DecisionStrategy getDecisionStrategy() {
+ return this.decisionStrategy;
+ }
+
+ @Override
+ public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
+ this.decisionStrategy = decisionStrategy;
+ }
+
+ @Override
+ public Logic getLogic() {
+ return this.logic;
+ }
+
+ @Override
+ public void setLogic(Logic logic) {
+ this.logic = logic;
+ }
+
+ @Override
+ public Map<String, String> getConfig() {
+ return this.config;
+ }
+
+ @Override
+ public void setConfig(Map<String, String> config) {
+ this.config = config;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getDescription() {
+ return this.description;
+ }
+
+ @Override
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ @Override
+ public ResourceServerEntity getResourceServer() {
+ return this.resourceServer;
+ }
+
+ public void setResourceServer(ResourceServerEntity resourceServer) {
+ this.resourceServer = resourceServer;
+ }
+
+ @Override
+ public <P extends Policy> Set<P> getAssociatedPolicies() {
+ return (Set<P>) this.associatedPolicies;
+ }
+
+ public void setAssociatedPolicies(Set<PolicyEntity> associatedPolicies) {
+ this.associatedPolicies = associatedPolicies;
+ }
+
+ @Override
+ public Set<ResourceEntity> getResources() {
+ return this.resources;
+ }
+
+ public void setResources(Set<ResourceEntity> resources) {
+ this.resources = resources;
+ }
+
+ @Override
+ public Set<ScopeEntity> getScopes() {
+ return this.scopes;
+ }
+
+ public void setScopes(Set<ScopeEntity> scopes) {
+ this.scopes = scopes;
+ }
+
+ @Override
+ public void addScope(Scope scope) {
+ getScopes().add((ScopeEntity) scope);
+ }
+
+ @Override
+ public void removeScope(Scope scope) {
+ getScopes().remove(scope);
+ }
+
+ @Override
+ public void addAssociatedPolicy(Policy associatedPolicy) {
+ getAssociatedPolicies().add(associatedPolicy);
+ }
+
+ @Override
+ public void removeAssociatedPolicy(Policy associatedPolicy) {
+ getAssociatedPolicies().remove(associatedPolicy);
+ }
+
+ @Override
+ public void addResource(Resource resource) {
+ getResources().add((ResourceEntity) resource);
+ }
+
+ @Override
+ public void removeResource(Resource resource) {
+ getResources().remove(resource);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+
+ if (this.id == null) return false;
+
+ if (o == null || getClass() != o.getClass()) return false;
+
+ AbstractIdentifiableEntity that = (AbstractIdentifiableEntity) o;
+
+ if (!getId().equals(that.getId())) return false;
+
+ return true;
+
+ }
+
+ @Override
+ public int hashCode() {
+ return id!=null ? id.hashCode() : super.hashCode();
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
new file mode 100644
index 0000000..7cb1a6f
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
@@ -0,0 +1,190 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.jpa.entities;
+
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.Scope;
+
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@Entity
+@Table(name = "RESOURCE_SERVER_RESOURCE", uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"NAME", "RESOURCE_SERVER_ID", "OWNER"})
+})
+public class ResourceEntity implements Resource {
+
+ @Id
+ @Column(name="ID", length = 36)
+ @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
+ private String id;
+
+ @Column(name = "NAME")
+ private String name;
+
+ @Column(name = "URI")
+ private String uri;
+
+ @Column(name = "TYPE")
+ private String type;
+
+ @Column(name = "ICON_URI")
+ private String iconUri;
+
+ @Column(name = "OWNER")
+ private String owner;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "RESOURCE_SERVER_ID")
+ private ResourceServerEntity resourceServer;
+
+ @ManyToMany(fetch = FetchType.LAZY, cascade = {})
+ @JoinTable(name = "RESOURCE_SCOPE", joinColumns = @JoinColumn(name = "RESOURCE_ID"), inverseJoinColumns = @JoinColumn(name = "SCOPE_ID"))
+ private List<ScopeEntity> scopes = new ArrayList<>();
+
+ @ManyToMany(fetch = FetchType.LAZY, cascade = {})
+ @JoinTable(name = "RESOURCE_POLICY", joinColumns = @JoinColumn(name = "RESOURCE_ID"), inverseJoinColumns = @JoinColumn(name = "POLICY_ID"))
+ private List<PolicyEntity> policies = new ArrayList<>();
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getUri() {
+ return uri;
+ }
+
+ @Override
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ @Override
+ public String getType() {
+ return type;
+ }
+
+ @Override
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ @Override
+ public List<ScopeEntity> getScopes() {
+ return this.scopes;
+ }
+
+ @Override
+ public String getIconUri() {
+ return iconUri;
+ }
+
+ @Override
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ @Override
+ public ResourceServerEntity getResourceServer() {
+ return resourceServer;
+ }
+
+ public void setResourceServer(ResourceServerEntity resourceServer) {
+ this.resourceServer = resourceServer;
+ }
+
+ public String getOwner() {
+ return this.owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public List<PolicyEntity> getPolicies() {
+ return this.policies;
+ }
+
+ public void updateScopes(Set<Scope> toUpdate) {
+ for (Scope scope : toUpdate) {
+ boolean hasScope = false;
+
+ for (Scope existingScope : this.scopes) {
+ if (existingScope.equals(scope)) {
+ hasScope = true;
+ }
+ }
+
+ if (!hasScope) {
+ this.scopes.add((ScopeEntity) scope);
+ }
+ }
+
+ for (Scope scopeModel : new HashSet<Scope>(this.scopes)) {
+ boolean hasScope = false;
+
+ for (Scope scope : toUpdate) {
+ if (scopeModel.equals(scope)) {
+ hasScope = true;
+ }
+ }
+
+ if (!hasScope) {
+ this.scopes.remove(scopeModel);
+ }
+ }
+ }
+
+ public void setPolicies(List<PolicyEntity> policies) {
+ this.policies = policies;
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceServerEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceServerEntity.java
new file mode 100644
index 0000000..b74b231
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceServerEntity.java
@@ -0,0 +1,113 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.jpa.entities;
+
+import org.keycloak.authorization.model.ResourceServer;
+
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.OneToMany;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@Entity
+@Table(name = "RESOURCE_SERVER", uniqueConstraints = {@UniqueConstraint(columnNames = "CLIENT_ID")})
+public class ResourceServerEntity implements ResourceServer {
+
+ @Id
+ @Column(name="ID", length = 36)
+ @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
+ private String id;
+
+ @Column(name = "CLIENT_ID")
+ private String clientId;
+
+ @Column(name = "ALLOW_RS_REMOTE_MGMT")
+ private boolean allowRemoteResourceManagement;
+
+ @Column(name = "POLICY_ENFORCE_MODE")
+ private PolicyEnforcementMode policyEnforcementMode = PolicyEnforcementMode.ENFORCING;
+
+ @OneToMany(mappedBy = "resourceServer")
+ private List<ResourceEntity> resources;
+
+ @OneToMany (mappedBy = "resourceServer")
+ private List<ScopeEntity> scopes;
+
+ @Override
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getClientId() {
+ return this.clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ @Override
+ public boolean isAllowRemoteResourceManagement() {
+ return this.allowRemoteResourceManagement;
+ }
+
+ @Override
+ public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) {
+ this.allowRemoteResourceManagement = allowRemoteResourceManagement;
+ }
+
+ @Override
+ public PolicyEnforcementMode getPolicyEnforcementMode() {
+ return this.policyEnforcementMode;
+ }
+
+ @Override
+ public void setPolicyEnforcementMode(PolicyEnforcementMode policyEnforcementMode) {
+ this.policyEnforcementMode = policyEnforcementMode;
+ }
+
+ public List<ResourceEntity> getResources() {
+ return this.resources;
+ }
+
+ public void setResources(final List<ResourceEntity> resources) {
+ this.resources = resources;
+ }
+
+ public List<ScopeEntity> getScopes() {
+ return this.scopes;
+ }
+
+ public void setScopes(final List<ScopeEntity> scopes) {
+ this.scopes = scopes;
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java
new file mode 100644
index 0000000..99f8b41
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ScopeEntity.java
@@ -0,0 +1,126 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.jpa.entities;
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Scope;
+
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@Entity
+@Table(name = "RESOURCE_SERVER_SCOPE", uniqueConstraints = {
+ @UniqueConstraint(columnNames = {"NAME", "RESOURCE_SERVER_ID"})
+})
+public class ScopeEntity implements Scope {
+
+ @Id
+ @Column(name="ID", length = 36)
+ @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
+ private String id;
+
+ @Column(name = "NAME")
+ private String name;
+
+ @Column(name = "ICON_URI")
+ private String iconUri;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "RESOURCE_SERVER_ID")
+ private ResourceServerEntity resourceServer;
+
+ @ManyToMany(fetch = FetchType.LAZY, cascade = {})
+ @JoinTable(name = "SCOPE_POLICY", joinColumns = @JoinColumn(name = "SCOPE_ID"), inverseJoinColumns = @JoinColumn(name = "POLICY_ID"))
+ private List<PolicyEntity> policies = new ArrayList<>();
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getIconUri() {
+ return iconUri;
+ }
+
+ @Override
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ @Override
+ public ResourceServerEntity getResourceServer() {
+ return resourceServer;
+ }
+
+ public List<? extends Policy> getPolicies() {
+ return this.policies;
+ }
+
+ public void setPolicies(List<PolicyEntity> policies) {
+ this.policies = policies;
+ }
+
+ public void setResourceServer(final ResourceServerEntity resourceServer) {
+ this.resourceServer = resourceServer;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ScopeEntity that = (ScopeEntity) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAAuthorizationStoreFactory.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAAuthorizationStoreFactory.java
new file mode 100644
index 0000000..8788884
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAAuthorizationStoreFactory.java
@@ -0,0 +1,56 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.jpa.store;
+
+import org.keycloak.Config;
+import org.keycloak.authorization.store.AuthorizationStoreFactory;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.connections.jpa.JpaConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+
+import javax.persistence.EntityManager;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JPAAuthorizationStoreFactory implements AuthorizationStoreFactory {
+ @Override
+ public StoreFactory create(KeycloakSession session) {
+ return new JPAStoreFactory(getEntityManager(session));
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "jpa";
+ }
+
+ private EntityManager getEntityManager(KeycloakSession session) {
+ return session.getProvider(JpaConnectionProvider.class).getEntityManager();
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java
new file mode 100644
index 0000000..b05f933
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAPolicyStore.java
@@ -0,0 +1,157 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.jpa.store;
+
+import org.keycloak.authorization.jpa.entities.PolicyEntity;
+import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
+import javax.persistence.Query;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JPAPolicyStore implements PolicyStore {
+
+ private final EntityManager entityManager;
+
+ public JPAPolicyStore(EntityManager entityManager) {
+ this.entityManager = entityManager;
+ }
+
+ @Override
+ public Policy create(String name, String type, ResourceServer resourceServer) {
+ PolicyEntity entity = new PolicyEntity();
+
+ entity.setId(KeycloakModelUtils.generateId());
+ entity.setName(name);
+ entity.setType(type);
+ entity.setResourceServer((ResourceServerEntity) resourceServer);
+
+ this.entityManager.persist(entity);
+
+ return entity;
+ }
+
+ public EntityManager getEntityManager() {
+ return this.entityManager;
+ }
+
+ @Override
+ public void delete(String id) {
+ Policy policy = findById(id);
+
+ if (policy != null) {
+ getEntityManager().remove(policy);
+ }
+ }
+
+
+ @Override
+ public Policy findById(String id) {
+ return getEntityManager().find(PolicyEntity.class, id);
+ }
+
+ @Override
+ public Policy findByName(String name, String resourceServerId) {
+ try {
+ Query query = getEntityManager().createQuery("from PolicyEntity where name = :name and resourceServer.id = :serverId");
+
+ query.setParameter("name", name);
+ query.setParameter("serverId", resourceServerId);
+
+ return (Policy) query.getSingleResult();
+ } catch (NoResultException nre) {
+ return null;
+ }
+ }
+
+ @Override
+ public List<Policy> findByResourceServer(final String resourceServerId) {
+ Query query = getEntityManager().createQuery("from PolicyEntity where resourceServer.id = :serverId");
+
+ query.setParameter("serverId", resourceServerId);
+
+ return query.getResultList();
+ }
+
+ @Override
+ public List<Policy> findByResource(final String resourceId) {
+ Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.resources r where r.id = :resourceId");
+
+ query.setParameter("resourceId", resourceId);
+
+ return query.getResultList();
+ }
+
+ @Override
+ public List<Policy> findByResourceType(final String resourceType, String resourceServerId) {
+ List<Policy> policies = new ArrayList<>();
+ Query query = getEntityManager().createQuery("from PolicyEntity where resourceServer.id = :serverId");
+
+ query.setParameter("serverId", resourceServerId);
+
+ List<Policy> models = query.getResultList();
+
+ for (Policy policy : models) {
+ String defaultType = policy.getConfig().get("defaultResourceType");
+
+ if (defaultType != null && defaultType.equals(resourceType) && policy.getResources().isEmpty()) {
+ policies.add(policy);
+ }
+ }
+
+ return policies;
+ }
+
+ @Override
+ public List<Policy> findByScopeIds(List<String> scopeIds, String resourceServerId) {
+ Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.scopes s where p.resourceServer.id = :serverId and s.id in (:scopeIds) and p.resources is empty group by p.id order by p.name");
+
+ query.setParameter("serverId", resourceServerId);
+ query.setParameter("scopeIds", scopeIds);
+
+ return query.getResultList();
+ }
+
+ @Override
+ public List<Policy> findByType(String type) {
+ Query query = getEntityManager().createQuery("select p from PolicyEntity p where p.type = :type");
+
+ query.setParameter("type", type);
+
+ return query.getResultList();
+ }
+
+ @Override
+ public List<Policy> findDependentPolicies(String policyId) {
+ Query query = getEntityManager().createQuery("select p from PolicyEntity p inner join p.associatedPolicies ap where ap.id in (:policyId)");
+
+ query.setParameter("policyId", Arrays.asList(policyId));
+
+ return query.getResultList();
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
new file mode 100644
index 0000000..51d0369
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceServerStore.java
@@ -0,0 +1,75 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.jpa.store;
+
+import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import javax.persistence.EntityManager;
+import javax.persistence.Query;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JPAResourceServerStore implements ResourceServerStore {
+
+ private final EntityManager entityManager;
+
+ public JPAResourceServerStore(EntityManager entityManager) {
+ this.entityManager = entityManager;
+ }
+
+ @Override
+ public ResourceServer create(String clientId) {
+ ResourceServerEntity entity = new ResourceServerEntity();
+
+ entity.setId(KeycloakModelUtils.generateId());
+ entity.setClientId(clientId);
+
+ this.entityManager.persist(entity);
+
+ return entity;
+ }
+
+ @Override
+ public void delete(String id) {
+ this.entityManager.remove(findById(id));
+ }
+
+ @Override
+ public ResourceServer findById(String id) {
+ return entityManager.find(ResourceServerEntity.class, id);
+ }
+
+ @Override
+ public ResourceServer findByClient(final String clientId) {
+ Query query = entityManager.createQuery("from ResourceServerEntity where clientId = :clientId");
+
+ query.setParameter("clientId", clientId);
+ List result = query.getResultList();
+
+ if (result.isEmpty()) {
+ return null;
+ }
+
+ return (ResourceServer) result.get(0);
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
new file mode 100644
index 0000000..802989c
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
@@ -0,0 +1,132 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.jpa.store;
+
+import org.keycloak.authorization.jpa.entities.ResourceEntity;
+import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import javax.persistence.EntityManager;
+import javax.persistence.Query;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JPAResourceStore implements ResourceStore {
+
+ private final EntityManager entityManager;
+
+ public JPAResourceStore(EntityManager entityManager) {
+ this.entityManager = entityManager;
+ }
+
+ @Override
+ public Resource create(String name, ResourceServer resourceServer, String owner) {
+ if (!(resourceServer instanceof ResourceServerEntity)) {
+ throw new RuntimeException("Unexpected type [" + resourceServer.getClass() + "].");
+ }
+
+ ResourceEntity entity = new ResourceEntity();
+
+ entity.setId(KeycloakModelUtils.generateId());
+ entity.setName(name);
+ entity.setResourceServer((ResourceServerEntity) resourceServer);
+ entity.setOwner(owner);
+
+ this.entityManager.persist(entity);
+
+ return entity;
+ }
+
+ @Override
+ public void delete(String id) {
+ Resource resource = findById(id);
+
+ resource.getScopes().clear();
+
+ if (resource != null) {
+ this.entityManager.remove(resource);
+ }
+ }
+
+ @Override
+ public Resource findById(String id) {
+ if (id == null) {
+ return null;
+ }
+
+ return entityManager.find(ResourceEntity.class, id);
+ }
+
+ @Override
+ public List<Resource> findByOwner(String ownerId) {
+ Query query = entityManager.createQuery("from ResourceEntity where owner = :ownerId");
+
+ query.setParameter("ownerId", ownerId);
+
+ return query.getResultList();
+ }
+
+ @Override
+ public List findByResourceServer(String resourceServerId) {
+ Query query = entityManager.createQuery("from ResourceEntity where resourceServer.id = :serverId");
+
+ query.setParameter("serverId", resourceServerId);
+
+ return query.getResultList();
+ }
+
+ @Override
+ public List<Resource> findByScope(String... id) {
+ Query query = entityManager.createQuery("from ResourceEntity r inner join r.scopes s where s.id in (:scopeIds)");
+
+ query.setParameter("scopeIds", Arrays.asList(id));
+
+ return query.getResultList();
+ }
+
+ @Override
+ public Resource findByName(String name, String resourceServerId) {
+ Query query = entityManager.createQuery("from ResourceEntity where resourceServer.id = :serverId and name = :name");
+
+ query.setParameter("serverId", resourceServerId);
+ query.setParameter("name", name);
+
+ List<Resource> result = query.getResultList();
+
+ if (!result.isEmpty()) {
+ return result.get(0);
+ }
+
+ return null;
+ }
+
+ @Override
+ public List<Resource> findByType(String type) {
+ Query query = entityManager.createQuery("from ResourceEntity where type = :type");
+
+ query.setParameter("type", type);
+
+ return query.getResultList();
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java
new file mode 100644
index 0000000..cc9a956
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAScopeStore.java
@@ -0,0 +1,88 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.jpa.store;
+
+import org.keycloak.authorization.jpa.entities.ResourceServerEntity;
+import org.keycloak.authorization.jpa.entities.ScopeEntity;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
+import javax.persistence.Query;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JPAScopeStore implements ScopeStore {
+
+ private final EntityManager entityManager;
+
+ public JPAScopeStore(EntityManager entityManager) {
+ this.entityManager = entityManager;
+ }
+
+ @Override
+ public Scope create(final String name, final ResourceServer resourceServer) {
+ ScopeEntity entity = new ScopeEntity();
+
+ entity.setId(KeycloakModelUtils.generateId());
+ entity.setName(name);
+ entity.setResourceServer((ResourceServerEntity) resourceServer);
+
+ this.entityManager.persist(entity);
+
+ return entity;
+ }
+
+ @Override
+ public void delete(String id) {
+ this.entityManager.remove(findById(id));
+ }
+
+ @Override
+ public Scope findById(String id) {
+ return entityManager.find(ScopeEntity.class, id);
+ }
+
+ @Override
+ public Scope findByName(String name, String resourceServerId) {
+ try {
+ Query query = entityManager.createQuery("select s from ScopeEntity s inner join s.resourceServer rs where rs.id = :resourceServerId and name = :name");
+
+ query.setParameter("name", name);
+ query.setParameter("resourceServerId", resourceServerId);
+
+ return (Scope) query.getSingleResult();
+ } catch (NoResultException nre) {
+ return null;
+ }
+ }
+
+ @Override
+ public List<Scope> findByResourceServer(final String serverId) {
+ Query query = entityManager.createQuery("from ScopeEntity where resourceServer.id = :serverId");
+
+ query.setParameter("serverId", serverId);
+
+ return query.getResultList();
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java
new file mode 100644
index 0000000..5dad6af
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAStoreFactory.java
@@ -0,0 +1,64 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.jpa.store;
+
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.authorization.store.StoreFactory;
+
+import javax.persistence.EntityManager;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class JPAStoreFactory implements StoreFactory {
+
+ private final EntityManager entityManager;
+
+ public JPAStoreFactory(EntityManager entityManager) {
+ this.entityManager = entityManager;
+ }
+
+ @Override
+ public PolicyStore getPolicyStore() {
+ return new JPAPolicyStore(this.entityManager);
+ }
+
+ @Override
+ public ResourceServerStore getResourceServerStore() {
+ return new JPAResourceServerStore(this.entityManager);
+ }
+
+ @Override
+ public ResourceStore getResourceStore() {
+ return new JPAResourceStore(this.entityManager);
+ }
+
+ @Override
+ public ScopeStore getScopeStore() {
+ return new JPAScopeStore(this.entityManager);
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
index e504f2b..5ceab0b 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
@@ -39,11 +39,9 @@ import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
-import java.util.Map;
import java.util.Set;
/**
@@ -127,7 +125,7 @@ public class JpaRealmProvider implements RealmProvider {
return false;
}
em.refresh(realm);
- RealmAdapter adapter = new RealmAdapter(session, em, realm);
+ final RealmAdapter adapter = new RealmAdapter(session, em, realm);
session.users().preRemove(adapter);
realm.getDefaultGroups().clear();
@@ -159,6 +157,19 @@ public class JpaRealmProvider implements RealmProvider {
em.flush();
em.clear();
+
+ session.getKeycloakSessionFactory().publish(new RealmModel.RealmRemovedEvent() {
+ @Override
+ public RealmModel getRealm() {
+ return adapter;
+ }
+
+ @Override
+ public KeycloakSession getKeycloakSession() {
+ return session;
+ }
+ });
+
return true;
}
@@ -268,6 +279,19 @@ public class JpaRealmProvider implements RealmProvider {
int val = em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate();
em.remove(roleEntity);
+
+ session.getKeycloakSessionFactory().publish(new RoleContainerModel.RoleRemovedEvent() {
+ @Override
+ public RoleModel getRole() {
+ return role;
+ }
+
+ @Override
+ public KeycloakSession getKeycloakSession() {
+ return session;
+ }
+ });
+
em.flush();
return true;
@@ -451,7 +475,7 @@ public class JpaRealmProvider implements RealmProvider {
@Override
public boolean removeClient(String id, RealmModel realm) {
- ClientModel client = getClientById(id, realm);
+ final ClientModel client = getClientById(id, realm);
if (client == null) return false;
session.users().preRemove(realm, client);
@@ -460,17 +484,32 @@ public class JpaRealmProvider implements RealmProvider {
client.removeRole(role);
}
-
ClientEntity clientEntity = ((ClientAdapter)client).getEntity();
+
em.createNamedQuery("deleteScopeMappingByClient").setParameter("client", clientEntity).executeUpdate();
em.flush();
+
+ session.getKeycloakSessionFactory().publish(new RealmModel.ClientRemovedEvent() {
+ @Override
+ public ClientModel getClient() {
+ return client;
+ }
+
+ @Override
+ public KeycloakSession getKeycloakSession() {
+ return session;
+ }
+ });
+
em.remove(clientEntity); // i have no idea why, but this needs to come before deleteScopeMapping
+
try {
em.flush();
} catch (RuntimeException e) {
logger.errorv("Unable to delete client entity: {0} from realm {1}", client.getClientId(), realm.getName());
throw e;
}
+
return true;
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 040d6eb..593bae5 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -25,6 +25,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
+import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserFederationProviderModel;
@@ -115,6 +116,17 @@ public class JpaUserProvider implements UserProvider {
UserEntity userEntity = em.find(UserEntity.class, user.getId());
if (userEntity == null) return false;
removeUser(userEntity);
+ session.getKeycloakSessionFactory().publish(new UserModel.UserRemovedEvent() {
+ @Override
+ public UserModel getUser() {
+ return user;
+ }
+
+ @Override
+ public KeycloakSession getKeycloakSession() {
+ return session;
+ }
+ });
return true;
}
@@ -134,6 +146,7 @@ public class JpaUserProvider implements UserProvider {
if (user != null) {
em.remove(user);
}
+
em.flush();
}
diff --git a/model/jpa/src/main/resources/META-INF/jpa-authz-changelog-2.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-authz-changelog-2.0.0.xml
new file mode 100755
index 0000000..6e5e30c
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-authz-changelog-2.0.0.xml
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
+ <!-- Authorization -->
+ <changeSet author="psilva@redhat.com" id="2.0.0">
+ <createTable tableName="RESOURCE_SERVER">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="CLIENT_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="ALLOW_RS_REMOTE_MGMT" type="BOOLEAN" defaultValueBoolean="false">
+ <constraints nullable="false"/>
+ </column>
+ <column name="POLICY_ENFORCE_MODE" type="VARCHAR(15)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_FARS" tableName="RESOURCE_SERVER"/>
+ <addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="RESOURCE_SERVER" constraintName="FK_AOJPHO213XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="CLIENT"/>
+ <addUniqueConstraint columnNames="CLIENT_ID" constraintName="UK_AU8TT6T700S9V50BU18WS5HA6" tableName="RESOURCE_SERVER"/>
+
+ <createTable tableName="RESOURCE_SERVER_RESOURCE">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="NAME" type="VARCHAR(255)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="URI" type="VARCHAR(255)"/>
+ <column name="TYPE" type="VARCHAR(255)"/>
+ <column name="ICON_URI" type="VARCHAR(255)"/>
+ <column name="OWNER" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="RESOURCE_SERVER_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_FARSR" tableName="RESOURCE_SERVER_RESOURCE"/>
+ <addForeignKeyConstraint baseColumnNames="RESOURCE_SERVER_ID" baseTableName="RESOURCE_SERVER_RESOURCE" constraintName="FK_FRSRHO213XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER"/>
+ <addUniqueConstraint columnNames="NAME,OWNER,RESOURCE_SERVER_ID" constraintName="UK_FRSR6T700S9V50BU18WS5HA6" tableName="RESOURCE_SERVER_RESOURCE"/>
+
+ <createTable tableName="RESOURCE_SERVER_SCOPE">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="NAME" type="VARCHAR(255)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="ICON_URI" type="VARCHAR(255)"/>
+ <column name="RESOURCE_SERVER_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_FARSRS" tableName="RESOURCE_SERVER_SCOPE"/>
+ <addForeignKeyConstraint baseColumnNames="RESOURCE_SERVER_ID" baseTableName="RESOURCE_SERVER_SCOPE" constraintName="FK_FRSRSO213XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER"/>
+ <addUniqueConstraint columnNames="NAME,RESOURCE_SERVER_ID" constraintName="UK_FRSRST700S9V50BU18WS5HA6" tableName="RESOURCE_SERVER_SCOPE"/>
+
+ <createTable tableName="RESOURCE_SERVER_POLICY">
+ <column name="ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="NAME" type="VARCHAR(255)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="DESCRIPTION" type="VARCHAR(255)"/>
+ <column name="TYPE" type="VARCHAR(255)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="DECISION_STRATEGY" type="VARCHAR(20)"/>
+ <column name="LOGIC" type="VARCHAR(20)"/>
+ <column name="RESOURCE_SERVER_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_FARSRP" tableName="RESOURCE_SERVER_POLICY"/>
+ <addForeignKeyConstraint baseColumnNames="RESOURCE_SERVER_ID" baseTableName="RESOURCE_SERVER_POLICY" constraintName="FK_FRSRPO213XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER"/>
+ <addUniqueConstraint columnNames="NAME,RESOURCE_SERVER_ID" constraintName="UK_FRSRPT700S9V50BU18WS5HA6" tableName="RESOURCE_SERVER_POLICY"/>
+
+ <createTable tableName="POLICY_CONFIG">
+ <column name="POLICY_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="NAME" type="VARCHAR(255)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="VALUE" type="CLOB"/>
+ </createTable>
+
+ <addPrimaryKey columnNames="POLICY_ID, NAME" constraintName="CONSTRAINT_DPC" tableName="POLICY_CONFIG"/>
+ <addForeignKeyConstraint baseColumnNames="POLICY_ID" baseTableName="POLICY_CONFIG" constraintName="FKDC34197CF864C4E43" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_POLICY"/>
+
+ <createTable tableName="RESOURCE_SCOPE">
+ <column name="RESOURCE_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="SCOPE_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="RESOURCE_ID,SCOPE_ID" constraintName="CONSTRAINT_FARSRSP" tableName="RESOURCE_SCOPE"/>
+ <addForeignKeyConstraint baseColumnNames="RESOURCE_ID" baseTableName="RESOURCE_SCOPE" constraintName="FK_FRSRPOS13XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_RESOURCE"/>
+ <addForeignKeyConstraint baseColumnNames="SCOPE_ID" baseTableName="RESOURCE_SCOPE" constraintName="FK_FRSRPS213XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_SCOPE"/>
+
+ <createTable tableName="RESOURCE_POLICY">
+ <column name="RESOURCE_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="POLICY_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="RESOURCE_ID,POLICY_ID" constraintName="CONSTRAINT_FARSRPP" tableName="RESOURCE_POLICY"/>
+ <addForeignKeyConstraint baseColumnNames="RESOURCE_ID" baseTableName="RESOURCE_POLICY" constraintName="FK_FRSRPOS53XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_RESOURCE"/>
+ <addForeignKeyConstraint baseColumnNames="POLICY_ID" baseTableName="RESOURCE_POLICY" constraintName="FK_FRSRPP213XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_POLICY"/>
+
+ <createTable tableName="SCOPE_POLICY">
+ <column name="SCOPE_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="POLICY_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="SCOPE_ID,POLICY_ID" constraintName="CONSTRAINT_FARSRSPS" tableName="SCOPE_POLICY"/>
+ <addForeignKeyConstraint baseColumnNames="SCOPE_ID" baseTableName="SCOPE_POLICY" constraintName="FK_FRSRPASS3XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_SCOPE"/>
+ <addForeignKeyConstraint baseColumnNames="POLICY_ID" baseTableName="SCOPE_POLICY" constraintName="FK_FRSRASP13XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_POLICY"/>
+
+ <createTable tableName="ASSOCIATED_POLICY">
+ <column name="POLICY_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="ASSOCIATED_POLICY_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="POLICY_ID,ASSOCIATED_POLICY_ID" constraintName="CONSTRAINT_FARSRPAP" tableName="ASSOCIATED_POLICY"/>
+ <addForeignKeyConstraint baseColumnNames="POLICY_ID" baseTableName="ASSOCIATED_POLICY" constraintName="FK_FRSRPAS14XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_POLICY"/>
+ <addForeignKeyConstraint baseColumnNames="ASSOCIATED_POLICY_ID" baseTableName="ASSOCIATED_POLICY" constraintName="FK_FRSR5S213XCX4WNKOG82SSRFY" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_POLICY"/>
+ </changeSet>
+</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-2.0.0.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.0.0.xml
new file mode 100755
index 0000000..72af496
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-2.0.0.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
+ <include file="META-INF/jpa-authz-changelog-2.0.0.xml"/>
+</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
index fd1f0f6..131ce05 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
@@ -32,4 +32,5 @@
<include file="META-INF/jpa-changelog-1.9.0.xml"/>
<include file="META-INF/jpa-changelog-1.9.1.xml"/>
<include file="META-INF/jpa-changelog-1.9.2.xml"/>
+ <include file="META-INF/jpa-changelog-2.0.0.xml"/>
</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml
index 7b6ee37..b90c0fc 100755
--- a/model/jpa/src/main/resources/META-INF/persistence.xml
+++ b/model/jpa/src/main/resources/META-INF/persistence.xml
@@ -58,6 +58,12 @@
<!-- JpaAuditProviders -->
<class>org.keycloak.events.jpa.EventEntity</class>
<class>org.keycloak.events.jpa.AdminEventEntity</class>
+
+ <!-- Authorization -->
+ <class>org.keycloak.authorization.jpa.entities.ResourceServerEntity</class>
+ <class>org.keycloak.authorization.jpa.entities.ResourceEntity</class>
+ <class>org.keycloak.authorization.jpa.entities.ScopeEntity</class>
+ <class>org.keycloak.authorization.jpa.entities.PolicyEntity</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
diff --git a/model/jpa/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory b/model/jpa/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory
new file mode 100644
index 0000000..46463ee
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.jpa.store.JPAAuthorizationStoreFactory
\ No newline at end of file
model/mongo/pom.xml 5(+5 -0)
diff --git a/model/mongo/pom.xml b/model/mongo/pom.xml
index db24473..90e2afa 100755
--- a/model/mongo/pom.xml
+++ b/model/mongo/pom.xml
@@ -31,6 +31,11 @@
<name>Keycloak Model Mongo</name>
<description/>
+ <properties>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ </properties>
+
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java
new file mode 100644
index 0000000..a993c94
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/PolicyAdapter.java
@@ -0,0 +1,168 @@
+package org.keycloak.authorization.mongo.adapter;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.mongo.entities.PolicyEntity;
+import org.keycloak.authorization.mongo.entities.ResourceEntity;
+import org.keycloak.authorization.mongo.entities.ScopeEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.mongo.keycloak.adapters.AbstractMongoAdapter;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static javafx.scene.input.KeyCode.R;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyAdapter extends AbstractMongoAdapter<PolicyEntity> implements Policy {
+
+ private final PolicyEntity entity;
+ private final AuthorizationProvider authorizationProvider;
+
+ public PolicyAdapter(PolicyEntity entity, MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) {
+ super(invocationContext);
+ this.entity = entity;
+ this.authorizationProvider = authorizationProvider;
+ }
+
+ @Override
+ protected PolicyEntity getMongoEntity() {
+ return entity;
+ }
+
+ @Override
+ public String getId() {
+ return getMongoEntity().getId();
+ }
+
+ @Override
+ public String getType() {
+ return getMongoEntity().getType();
+ }
+
+ @Override
+ public DecisionStrategy getDecisionStrategy() {
+ return getMongoEntity().getDecisionStrategy();
+ }
+
+ @Override
+ public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
+ getMongoEntity().setDecisionStrategy(decisionStrategy);
+ updateMongoEntity();
+ }
+
+ @Override
+ public Logic getLogic() {
+ return getMongoEntity().getLogic();
+ }
+
+ @Override
+ public void setLogic(Logic logic) {
+ getMongoEntity().setLogic(logic);
+ updateMongoEntity();
+ }
+
+ @Override
+ public Map<String, String> getConfig() {
+ return getMongoEntity().getConfig();
+ }
+
+ @Override
+ public void setConfig(Map<String, String> config) {
+ getMongoEntity().setConfig(config);
+ updateMongoEntity();
+ }
+
+ @Override
+ public String getName() {
+ return getMongoEntity().getName();
+ }
+
+ @Override
+ public void setName(String name) {
+ getMongoEntity().setName(name);
+ updateMongoEntity();
+ }
+
+ @Override
+ public String getDescription() {
+ return getMongoEntity().getDescription();
+ }
+
+ @Override
+ public void setDescription(String description) {
+ getMongoEntity().setDescription(description);
+ updateMongoEntity();
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ return this.authorizationProvider.getStoreFactory().getResourceServerStore().findById(getMongoEntity().getResourceServerId());
+ }
+
+ @Override
+ public Set<Policy> getAssociatedPolicies() {
+ return getMongoEntity().getAssociatedPolicies().stream()
+ .map((Function<String, Policy>) id -> authorizationProvider.getStoreFactory().getPolicyStore().findById(id))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public Set<Resource> getResources() {
+ return getMongoEntity().getResources().stream()
+ .map((Function<String, Resource>) id -> authorizationProvider.getStoreFactory().getResourceStore().findById(id))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public Set<Scope> getScopes() {
+ return getMongoEntity().getScopes().stream()
+ .map((Function<String, Scope>) id -> authorizationProvider.getStoreFactory().getScopeStore().findById(id))
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public void addScope(Scope scope) {
+ getMongoEntity().addScope(scope.getId());
+ updateMongoEntity();
+ }
+
+ @Override
+ public void removeScope(Scope scope) {
+ getMongoEntity().removeScope(scope.getId());
+ updateMongoEntity();
+ }
+
+ @Override
+ public void addAssociatedPolicy(Policy associatedPolicy) {
+ getMongoEntity().addAssociatedPolicy(associatedPolicy.getId());
+ updateMongoEntity();
+ }
+
+ @Override
+ public void removeAssociatedPolicy(Policy associatedPolicy) {
+ getMongoEntity().removeAssociatedPolicy(associatedPolicy.getId());
+ updateMongoEntity();
+ }
+
+ @Override
+ public void addResource(Resource resource) {
+ getMongoEntity().addResource(resource.getId());
+ updateMongoEntity();
+ }
+
+ @Override
+ public void removeResource(Resource resource) {
+ getMongoEntity().removeResource(resource.getId());
+ updateMongoEntity();
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java
new file mode 100644
index 0000000..7c67f6e
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceAdapter.java
@@ -0,0 +1,106 @@
+package org.keycloak.authorization.mongo.adapter;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.mongo.entities.ResourceEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.mongo.keycloak.adapters.AbstractMongoAdapter;
+
+import java.util.List;
+import java.util.Set;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceAdapter extends AbstractMongoAdapter<ResourceEntity> implements Resource {
+
+ private final ResourceEntity entity;
+ private final AuthorizationProvider authorizationProvider;
+
+ public ResourceAdapter(ResourceEntity entity, MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) {
+ super(invocationContext);
+ this.entity = entity;
+ this.authorizationProvider = authorizationProvider;
+ }
+
+ @Override
+ public String getId() {
+ return getMongoEntity().getId();
+ }
+
+ @Override
+ public String getName() {
+ return getMongoEntity().getName();
+ }
+
+ @Override
+ public void setName(String name) {
+ getMongoEntity().setName(name);
+ updateMongoEntity();
+ }
+
+ @Override
+ public String getUri() {
+ return getMongoEntity().getUri();
+ }
+
+ @Override
+ public void setUri(String uri) {
+ getMongoEntity().setUri(uri);
+ updateMongoEntity();
+ }
+
+ @Override
+ public String getType() {
+ return getMongoEntity().getType();
+ }
+
+ @Override
+ public void setType(String type) {
+ getMongoEntity().setType(type);
+ updateMongoEntity();
+ }
+
+ @Override
+ public List<Scope> getScopes() {
+ return getMongoEntity().getScopes().stream()
+ .map(id -> authorizationProvider.getStoreFactory().getScopeStore().findById(id))
+ .collect(toList());
+ }
+
+ @Override
+ public String getIconUri() {
+ return getMongoEntity().getIconUri();
+ }
+
+ @Override
+ public void setIconUri(String iconUri) {
+ getMongoEntity().setIconUri(iconUri);
+ updateMongoEntity();
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ return this.authorizationProvider.getStoreFactory().getResourceServerStore().findById(getMongoEntity().getResourceServerId());
+ }
+
+ @Override
+ public String getOwner() {
+ return getMongoEntity().getOwner();
+ }
+
+ @Override
+ public void updateScopes(Set<Scope> scopes) {
+ getMongoEntity().updateScopes(scopes);
+ updateMongoEntity();
+ }
+
+ @Override
+ protected ResourceEntity getMongoEntity() {
+ return this.entity;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceServerAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceServerAdapter.java
new file mode 100644
index 0000000..72feedb
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ResourceServerAdapter.java
@@ -0,0 +1,56 @@
+package org.keycloak.authorization.mongo.adapter;
+
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.mongo.entities.ResourceServerEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.mongo.keycloak.adapters.AbstractMongoAdapter;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceServerAdapter extends AbstractMongoAdapter<ResourceServerEntity> implements ResourceServer{
+
+ private final ResourceServerEntity entity;
+
+ public ResourceServerAdapter(ResourceServerEntity entity, MongoStoreInvocationContext invocationContext) {
+ super(invocationContext);
+ this.entity = entity;
+ }
+
+ @Override
+ public String getId() {
+ return getMongoEntity().getId();
+ }
+
+ @Override
+ public String getClientId() {
+ return getMongoEntity().getClientId();
+ }
+
+ @Override
+ public boolean isAllowRemoteResourceManagement() {
+ return getMongoEntity().isAllowRemoteResourceManagement();
+ }
+
+ @Override
+ public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) {
+ getMongoEntity().setAllowRemoteResourceManagement(allowRemoteResourceManagement);
+ updateMongoEntity();
+ }
+
+ @Override
+ public PolicyEnforcementMode getPolicyEnforcementMode() {
+ return getMongoEntity().getPolicyEnforcementMode();
+ }
+
+ @Override
+ public void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode) {
+ getMongoEntity().setPolicyEnforcementMode(enforcementMode);
+ updateMongoEntity();
+ }
+
+ @Override
+ protected ResourceServerEntity getMongoEntity() {
+ return this.entity;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ScopeAdapter.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ScopeAdapter.java
new file mode 100644
index 0000000..72196ca
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/adapter/ScopeAdapter.java
@@ -0,0 +1,60 @@
+package org.keycloak.authorization.mongo.adapter;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.mongo.entities.ScopeEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.mongo.keycloak.adapters.AbstractMongoAdapter;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScopeAdapter extends AbstractMongoAdapter<ScopeEntity> implements Scope {
+
+ private final ScopeEntity entity;
+ private final AuthorizationProvider authorizationProvider;
+
+ public ScopeAdapter(ScopeEntity entity, MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) {
+ super(invocationContext);
+ this.entity = entity;
+ this.authorizationProvider = authorizationProvider;
+ }
+
+ @Override
+ public String getId() {
+ return getMongoEntity().getId();
+ }
+
+ @Override
+ public String getName() {
+ return getMongoEntity().getName();
+ }
+
+ @Override
+ public void setName(String name) {
+ getMongoEntity().setName(name);
+ updateMongoEntity();
+ }
+
+ @Override
+ public String getIconUri() {
+ return getMongoEntity().getIconUri();
+ }
+
+ @Override
+ public void setIconUri(String iconUri) {
+ getMongoEntity().setIconUri(iconUri);
+ updateMongoEntity();
+ }
+
+ @Override
+ public ResourceServer getResourceServer() {
+ return this.authorizationProvider.getStoreFactory().getResourceServerStore().findById(getMongoEntity().getResourceServerId());
+ }
+
+ @Override
+ protected ScopeEntity getMongoEntity() {
+ return this.entity;
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/PolicyEntity.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/PolicyEntity.java
new file mode 100644
index 0000000..9230b88
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/PolicyEntity.java
@@ -0,0 +1,166 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.entities;
+
+import org.keycloak.authorization.model.Policy.DecisionStrategy;
+import org.keycloak.authorization.model.Policy.Logic;
+import org.keycloak.connections.mongo.api.MongoCollection;
+import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.entities.AbstractIdentifiableEntity;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@MongoCollection(collectionName = "policies")
+public class PolicyEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity {
+
+ private String name;
+
+ private String description;
+
+ private String type;
+
+ private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS;
+
+ private Logic logic = Logic.POSITIVE;
+
+ private Map<String, String> config = new HashMap();
+
+ private String resourceServerId;
+
+ private Set<String> associatedPolicies = new HashSet<>();
+
+ private Set<String> resources = new HashSet<>();
+
+ private Set<String> scopes = new HashSet<>();
+
+ public String getType() {
+ return this.type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public DecisionStrategy getDecisionStrategy() {
+ return this.decisionStrategy;
+ }
+
+ public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
+ this.decisionStrategy = decisionStrategy;
+ }
+
+ public Logic getLogic() {
+ return this.logic;
+ }
+
+ public void setLogic(Logic logic) {
+ this.logic = logic;
+ }
+
+ public Map<String, String> getConfig() {
+ return this.config;
+ }
+
+ public void setConfig(Map<String, String> config) {
+ this.config = config;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getResourceServerId() {
+ return this.resourceServerId;
+ }
+
+ public void setResourceServerId(String resourceServerId) {
+ this.resourceServerId = resourceServerId;
+ }
+
+ public Set<String> getAssociatedPolicies() {
+ return this.associatedPolicies;
+ }
+
+ public void setAssociatedPolicies(Set<String> associatedPolicies) {
+ this.associatedPolicies = associatedPolicies;
+ }
+
+ public Set<String> getResources() {
+ return this.resources;
+ }
+
+ public void setResources(Set<String> resources) {
+ this.resources = resources;
+ }
+
+ public Set<String> getScopes() {
+ return this.scopes;
+ }
+
+ public void setScopes(Set<String> scopes) {
+ this.scopes = scopes;
+ }
+
+ public void addScope(String scopeId) {
+ getScopes().add(scopeId);
+ }
+
+ public void removeScope(String scopeId) {
+ getScopes().remove(scopeId);
+ }
+
+ public void addAssociatedPolicy(String policyId) {
+ getAssociatedPolicies().add(policyId);
+ }
+
+ public void removeAssociatedPolicy(String policyId) {
+ getAssociatedPolicies().remove(policyId);
+ }
+
+ public void addResource(String resourceId) {
+ getResources().add(resourceId);
+ }
+
+ public void removeResource(String resourceId) {
+ getResources().remove(resourceId);
+ }
+
+ public void afterRemove(MongoStoreInvocationContext invocationContext) {
+
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceEntity.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceEntity.java
new file mode 100644
index 0000000..b2e15da
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceEntity.java
@@ -0,0 +1,142 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.entities;
+
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.connections.mongo.api.MongoCollection;
+import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.entities.AbstractIdentifiableEntity;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@MongoCollection(collectionName = "resources")
+public class ResourceEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity {
+
+ private String name;
+
+ private String uri;
+
+ private String type;
+
+ private String iconUri;
+
+ private String owner;
+
+ private String resourceServerId;
+
+ private List<String> scopes = new ArrayList<>();
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getUri() {
+ return uri;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public List<String> getScopes() {
+ return this.scopes;
+ }
+
+ public void setScopes(List<String> scopes) {
+ this.scopes = scopes;
+ }
+
+ public String getIconUri() {
+ return iconUri;
+ }
+
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ public String getResourceServerId() {
+ return resourceServerId;
+ }
+
+ public void setResourceServerId(String resourceServerId) {
+ this.resourceServerId = resourceServerId;
+ }
+
+ public String getOwner() {
+ return this.owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+
+ public void updateScopes(Set<Scope> toUpdate) {
+ for (Scope scope : toUpdate) {
+ boolean hasScope = false;
+
+ for (String existingScope : this.scopes) {
+ if (existingScope.equals(scope.getId())) {
+ hasScope = true;
+ }
+ }
+
+ if (!hasScope) {
+ this.scopes.add(scope.getId());
+ }
+ }
+
+ for (String scopeId : new HashSet<String>(this.scopes)) {
+ boolean hasScope = false;
+
+ for (Scope scope : toUpdate) {
+ if (scopeId.equals(scope.getId())) {
+ hasScope = true;
+ }
+ }
+
+ if (!hasScope) {
+ this.scopes.remove(scopeId);
+ }
+ }
+ }
+
+ @Override
+ public void afterRemove(MongoStoreInvocationContext invocationContext) {
+
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceServerEntity.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceServerEntity.java
new file mode 100644
index 0000000..7013e1b
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ResourceServerEntity.java
@@ -0,0 +1,67 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.entities;
+
+import org.keycloak.authorization.model.ResourceServer.PolicyEnforcementMode;
+import org.keycloak.connections.mongo.api.MongoCollection;
+import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.entities.AbstractIdentifiableEntity;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@MongoCollection(collectionName = "resource-servers")
+public class ResourceServerEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity {
+
+ private String clientId;
+
+ private boolean allowRemoteResourceManagement;
+
+ private PolicyEnforcementMode policyEnforcementMode = PolicyEnforcementMode.ENFORCING;
+
+ public String getClientId() {
+ return this.clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public boolean isAllowRemoteResourceManagement() {
+ return this.allowRemoteResourceManagement;
+ }
+
+ public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) {
+ this.allowRemoteResourceManagement = allowRemoteResourceManagement;
+ }
+
+ public PolicyEnforcementMode getPolicyEnforcementMode() {
+ return this.policyEnforcementMode;
+ }
+
+ public void setPolicyEnforcementMode(PolicyEnforcementMode policyEnforcementMode) {
+ this.policyEnforcementMode = policyEnforcementMode;
+ }
+
+ @Override
+ public void afterRemove(MongoStoreInvocationContext invocationContext) {
+
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ScopeEntity.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ScopeEntity.java
new file mode 100644
index 0000000..152127d
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/entities/ScopeEntity.java
@@ -0,0 +1,66 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.entities;
+
+import org.keycloak.connections.mongo.api.MongoCollection;
+import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.entities.AbstractIdentifiableEntity;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@MongoCollection(collectionName = "scopes")
+public class ScopeEntity extends AbstractIdentifiableEntity implements MongoIdentifiableEntity {
+
+ private String name;
+
+ private String iconUri;
+
+ private String resourceServerId;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getIconUri() {
+ return iconUri;
+ }
+
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ public String getResourceServerId() {
+ return resourceServerId;
+ }
+
+ public void setResourceServerId(String resourceServerId) {
+ this.resourceServerId = resourceServerId;
+ }
+
+ @Override
+ public void afterRemove(MongoStoreInvocationContext invocationContext) {
+
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java
new file mode 100644
index 0000000..9a484ad
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoAuthorizationStoreFactory.java
@@ -0,0 +1,53 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.store;
+
+import org.keycloak.Config;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.store.AuthorizationStoreFactory;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.connections.mongo.MongoConnectionProvider;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class MongoAuthorizationStoreFactory implements AuthorizationStoreFactory {
+ @Override
+ public StoreFactory create(KeycloakSession session) {
+ MongoConnectionProvider connection = session.getProvider(MongoConnectionProvider.class);
+ AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class);
+ return new MongoStoreFactory(connection.getInvocationContext(), provider);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "mongo";
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java
new file mode 100644
index 0000000..6f0ba5d
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoPolicyStore.java
@@ -0,0 +1,171 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.store;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.mongo.adapter.PolicyAdapter;
+import org.keycloak.authorization.mongo.entities.PolicyEntity;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.connections.mongo.api.MongoStore;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import java.util.List;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class MongoPolicyStore implements PolicyStore {
+
+ private final MongoStoreInvocationContext invocationContext;
+ private final AuthorizationProvider authorizationProvider;
+
+ public MongoPolicyStore(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) {
+ this.invocationContext = invocationContext;
+ this.authorizationProvider = authorizationProvider;
+ }
+
+ @Override
+ public Policy create(String name, String type, ResourceServer resourceServer) {
+ PolicyEntity entity = new PolicyEntity();
+
+ entity.setId(KeycloakModelUtils.generateId());
+ entity.setName(name);
+ entity.setType(type);
+ entity.setResourceServerId(resourceServer.getId());
+
+ getMongoStore().insertEntity(entity, getInvocationContext());
+
+ return new PolicyAdapter(entity, getInvocationContext(), this.authorizationProvider) ;
+ }
+
+ @Override
+ public void delete(String id) {
+ getMongoStore().removeEntity(PolicyEntity.class, id, getInvocationContext());
+ }
+
+ @Override
+ public Policy findById(String id) {
+ PolicyEntity entity = getMongoStore().loadEntity(PolicyEntity.class, id, getInvocationContext());
+
+ if (entity == null) {
+ return null;
+ }
+
+ return new PolicyAdapter(entity, getInvocationContext(), this.authorizationProvider);
+ }
+
+
+ @Override
+ public Policy findByName(String name, String resourceServerId) {
+ DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
+ .and("name").is(name)
+ .get();
+
+ return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId())).findFirst().orElse(null);
+ }
+
+ @Override
+ public List<Policy> findByResourceServer(String resourceServerId) {
+ DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
+ .get();
+
+ return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId()))
+ .collect(toList());
+ }
+
+ @Override
+ public List<Policy> findByResource(String resourceId) {
+ DBObject query = new QueryBuilder()
+ .and("resources").is(resourceId)
+ .get();
+
+ return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId()))
+ .collect(toList());
+ }
+
+ @Override
+ public List<Policy> findByResourceType(String resourceType, String resourceServerId) {
+ DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
+ .get();
+
+ return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
+ .filter(policyEntity -> {
+ String defaultResourceType = policyEntity.getConfig().get("defaultResourceType");
+ return defaultResourceType != null && defaultResourceType.equals(resourceType);
+ })
+ .map(policyEntity -> findById(policyEntity.getId()))
+ .collect(toList());
+ }
+
+ @Override
+ public List<Policy> findByScopeIds(List<String> scopeIds, String resourceServerId) {
+ DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
+ .and("scopes").in(scopeIds)
+ .get();
+
+ return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId()))
+ .collect(toList());
+ }
+
+ @Override
+ public List<Policy> findByType(String type) {
+ DBObject query = new QueryBuilder()
+ .and("type").is(type)
+ .get();
+
+ return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId()))
+ .collect(toList());
+ }
+
+ @Override
+ public List<Policy> findDependentPolicies(String policyId) {
+ DBObject query = new QueryBuilder()
+ .and("associatedPolicies").is(policyId)
+ .get();
+
+ return getMongoStore().loadEntities(PolicyEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId()))
+ .collect(toList());
+ }
+
+ private MongoStoreInvocationContext getInvocationContext() {
+ return this.invocationContext;
+ }
+
+ private MongoStore getMongoStore() {
+ return getInvocationContext().getMongoStore();
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceServerStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceServerStore.java
new file mode 100644
index 0000000..25e5f67
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceServerStore.java
@@ -0,0 +1,90 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.store;
+
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.mongo.adapter.ResourceServerAdapter;
+import org.keycloak.authorization.mongo.entities.ResourceServerEntity;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.connections.mongo.api.MongoStore;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class MongoResourceServerStore implements ResourceServerStore {
+
+ private final MongoStoreInvocationContext invocationContext;
+ private final AuthorizationProvider authorizationProvider;
+
+ public MongoResourceServerStore(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) {
+ this.invocationContext = invocationContext;
+ this.authorizationProvider = authorizationProvider;
+ }
+
+ @Override
+ public ResourceServer create(String clientId) {
+ ResourceServerEntity entity = new ResourceServerEntity();
+
+ entity.setId(KeycloakModelUtils.generateId());
+ entity.setClientId(clientId);
+
+ getMongoStore().insertEntity(entity, getInvocationContext());
+
+ return new ResourceServerAdapter(entity, getInvocationContext());
+ }
+
+ @Override
+ public void delete(String id) {
+ getMongoStore().removeEntity(ResourceServerEntity.class, id, getInvocationContext());
+ }
+
+ @Override
+ public ResourceServer findById(String id) {
+ ResourceServerEntity entity = getMongoStore().loadEntity(ResourceServerEntity.class, id, getInvocationContext());
+
+ if (entity == null) {
+ return null;
+ }
+
+ return new ResourceServerAdapter(entity, getInvocationContext());
+ }
+
+ @Override
+ public ResourceServer findByClient(String clientId) {
+ DBObject query = new QueryBuilder()
+ .and("clientId").is(clientId)
+ .get();
+
+ return getMongoStore().loadEntities(ResourceServerEntity.class, query, getInvocationContext()).stream()
+ .map(resourceServer -> findById(resourceServer.getId())).findFirst().orElse(null);
+ }
+
+ private MongoStoreInvocationContext getInvocationContext() {
+ return this.invocationContext;
+ }
+
+ private MongoStore getMongoStore() {
+ return getInvocationContext().getMongoStore();
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java
new file mode 100644
index 0000000..11b735b
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoResourceStore.java
@@ -0,0 +1,141 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.store;
+
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.mongo.adapter.ResourceAdapter;
+import org.keycloak.authorization.mongo.entities.ResourceEntity;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
+import org.keycloak.connections.mongo.api.MongoStore;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import java.util.List;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class MongoResourceStore implements ResourceStore {
+
+ private final MongoStoreInvocationContext invocationContext;
+ private final AuthorizationProvider authorizationProvider;
+
+ public MongoResourceStore(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) {
+ this.invocationContext = invocationContext;
+ this.authorizationProvider = authorizationProvider;
+ }
+
+ @Override
+ public Resource create(String name, ResourceServer resourceServer, String owner) {
+ ResourceEntity entity = new ResourceEntity();
+
+ entity.setId(KeycloakModelUtils.generateId());
+ entity.setName(name);
+ entity.setResourceServerId(resourceServer.getId());
+ entity.setOwner(owner);
+
+ getMongoStore().insertEntity(entity, getInvocationContext());
+
+ return new ResourceAdapter(entity, getInvocationContext(), this.authorizationProvider);
+ }
+
+ @Override
+ public void delete(String id) {
+ getMongoStore().removeEntity(ResourceEntity.class, id, getInvocationContext());
+ }
+
+ @Override
+ public Resource findById(String id) {
+ ResourceEntity entity = getMongoStore().loadEntity(ResourceEntity.class, id, getInvocationContext());
+
+ if (entity == null) {
+ return null;
+ }
+
+ return new ResourceAdapter(entity, getInvocationContext(), this.authorizationProvider);
+ }
+
+ @Override
+ public List<Resource> findByOwner(String ownerId) {
+ DBObject query = new QueryBuilder()
+ .and("owner").is(ownerId)
+ .get();
+
+ return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
+ .map(scope -> findById(scope.getId())).collect(toList());
+ }
+
+ @Override
+ public List findByResourceServer(String resourceServerId) {
+ DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
+ .get();
+
+ return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
+ .map(scope -> findById(scope.getId())).collect(toList());
+ }
+
+ @Override
+ public List<Resource> findByScope(String... id) {
+ DBObject query = new QueryBuilder()
+ .and("scopes.id").in(id)
+ .get();
+
+ return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId()))
+ .collect(toList());
+ }
+
+ @Override
+ public Resource findByName(String name, String resourceServerId) {
+ DBObject query = new QueryBuilder()
+ .and("name").is(name)
+ .and("resourceServerId").is(resourceServerId)
+ .get();
+
+ return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId())).findFirst().orElse(null);
+ }
+
+ @Override
+ public List<Resource> findByType(String type) {
+ DBObject query = new QueryBuilder()
+ .and("type").is(type)
+ .get();
+
+ return getMongoStore().loadEntities(ResourceEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId()))
+ .collect(toList());
+ }
+
+ private MongoStoreInvocationContext getInvocationContext() {
+ return this.invocationContext;
+ }
+
+ private MongoStore getMongoStore() {
+ return getInvocationContext().getMongoStore();
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java
new file mode 100644
index 0000000..e57b69b
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoScopeStore.java
@@ -0,0 +1,108 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.store;
+
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.mongo.adapter.ScopeAdapter;
+import org.keycloak.authorization.mongo.entities.ScopeEntity;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.connections.mongo.api.MongoStore;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import java.util.List;
+
+import static java.util.stream.Collectors.toList;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class MongoScopeStore implements ScopeStore {
+
+ private final MongoStoreInvocationContext invocationContext;
+ private final AuthorizationProvider authorizationProvider;
+
+ public MongoScopeStore(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) {
+ this.invocationContext = invocationContext;
+ this.authorizationProvider = authorizationProvider;
+ }
+
+ @Override
+ public Scope create(final String name, final ResourceServer resourceServer) {
+ ScopeEntity entity = new ScopeEntity();
+
+ entity.setId(KeycloakModelUtils.generateId());
+ entity.setName(name);
+ entity.setResourceServerId(resourceServer.getId());
+
+ getMongoStore().insertEntity(entity, getInvocationContext());
+
+ return new ScopeAdapter(entity, getInvocationContext(), this.authorizationProvider);
+ }
+
+ @Override
+ public void delete(String id) {
+ getMongoStore().removeEntity(ScopeEntity.class, id, getInvocationContext());
+ }
+
+ @Override
+ public Scope findById(String id) {
+ ScopeEntity entity = getMongoStore().loadEntity(ScopeEntity.class, id, getInvocationContext());
+
+ if (entity == null) {
+ return null;
+ }
+
+ return new ScopeAdapter(entity, getInvocationContext(), this.authorizationProvider);
+ }
+
+ @Override
+ public Scope findByName(String name, String resourceServerId) {
+ DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
+ .and("name").is(name)
+ .get();
+
+ return getMongoStore().loadEntities(ScopeEntity.class, query, getInvocationContext()).stream()
+ .map(scope -> findById(scope.getId())).findFirst().orElse(null);
+ }
+
+ @Override
+ public List<Scope> findByResourceServer(String resourceServerId) {
+ DBObject query = new QueryBuilder()
+ .and("resourceServerId").is(resourceServerId)
+ .get();
+
+ return getMongoStore().loadEntities(ScopeEntity.class, query, getInvocationContext()).stream()
+ .map(policyEntity -> findById(policyEntity.getId()))
+ .collect(toList());
+ }
+
+ private MongoStoreInvocationContext getInvocationContext() {
+ return this.invocationContext;
+ }
+
+ private MongoStore getMongoStore() {
+ return getInvocationContext().getMongoStore();
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java
new file mode 100644
index 0000000..7a94ba5
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/authorization/mongo/store/MongoStoreFactory.java
@@ -0,0 +1,66 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.mongo.store;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class MongoStoreFactory implements StoreFactory {
+
+ private final MongoStoreInvocationContext invocationContext;
+ private final AuthorizationProvider authorizationProvider;
+
+ public MongoStoreFactory(MongoStoreInvocationContext invocationContext, AuthorizationProvider authorizationProvider) {
+ this.invocationContext = invocationContext;
+ this.authorizationProvider = authorizationProvider;
+ }
+
+ @Override
+ public PolicyStore getPolicyStore() {
+ return new MongoPolicyStore(this.invocationContext, this.authorizationProvider);
+ }
+
+ @Override
+ public ResourceServerStore getResourceServerStore() {
+ return new MongoResourceServerStore(this.invocationContext, this.authorizationProvider);
+ }
+
+ @Override
+ public ResourceStore getResourceStore() {
+ return new MongoResourceStore(this.invocationContext, this.authorizationProvider);
+ }
+
+ @Override
+ public ScopeStore getScopeStore() {
+ return new MongoScopeStore(this.invocationContext, this.authorizationProvider);
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
index 3bc9c7a..491ad71 100755
--- a/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java
@@ -74,6 +74,10 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
"org.keycloak.models.entities.RequiredActionProviderEntity",
"org.keycloak.models.entities.PersistentUserSessionEntity",
"org.keycloak.models.entities.PersistentClientSessionEntity",
+ "org.keycloak.authorization.mongo.entities.PolicyEntity",
+ "org.keycloak.authorization.mongo.entities.ResourceEntity",
+ "org.keycloak.authorization.mongo.entities.ResourceServerEntity",
+ "org.keycloak.authorization.mongo.entities.ScopeEntity"
};
private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class);
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java
index cef8a5a..14e5b03 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java
@@ -129,10 +129,26 @@ public class MongoRealmProvider implements RealmProvider {
@Override
public boolean removeRealm(String id) {
- RealmModel realm = getRealm(id);
+ final RealmModel realm = getRealm(id);
if (realm == null) return false;
session.users().preRemove(realm);
- return getMongoStore().removeEntity(MongoRealmEntity.class, id, invocationContext);
+ boolean removed = getMongoStore().removeEntity(MongoRealmEntity.class, id, invocationContext);
+
+ if (removed) {
+ session.getKeycloakSessionFactory().publish(new RealmModel.RealmRemovedEvent() {
+ @Override
+ public RealmModel getRealm() {
+ return realm;
+ }
+
+ @Override
+ public KeycloakSession getKeycloakSession() {
+ return session;
+ }
+ });
+ }
+
+ return removed;
}
protected MongoStore getMongoStore() {
@@ -408,12 +424,27 @@ public class MongoRealmProvider implements RealmProvider {
@Override
public boolean removeClient(String id, RealmModel realm) {
if (id == null) return false;
- ClientModel client = getClientById(id, realm);
+ final ClientModel client = getClientById(id, realm);
if (client == null) return false;
session.users().preRemove(realm, client);
+ boolean removed = getMongoStore().removeEntity(MongoClientEntity.class, id, invocationContext);
+
+ if (removed) {
+ session.getKeycloakSessionFactory().publish(new RealmModel.ClientRemovedEvent() {
+ @Override
+ public ClientModel getClient() {
+ return client;
+ }
+
+ @Override
+ public KeycloakSession getKeycloakSession() {
+ return session;
+ }
+ });
+ }
- return getMongoStore().removeEntity(MongoClientEntity.class, id, invocationContext);
+ return removed;
}
@Override
diff --git a/model/mongo/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory b/model/mongo/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory
new file mode 100644
index 0000000..e1d801c
--- /dev/null
+++ b/model/mongo/src/main/resources/META-INF/services/org.keycloak.authorization.store.AuthorizationStoreFactory
@@ -0,0 +1,37 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.mongo.store.MongoAuthorizationStoreFactory
\ No newline at end of file
pom.xml 56(+56 -0)
diff --git a/pom.xml b/pom.xml
index 38efdcf..082891b 100755
--- a/pom.xml
+++ b/pom.xml
@@ -75,6 +75,9 @@
<wildfly.build-tools.version>1.1.0.Final</wildfly.build-tools.version>
<xmlsec.version>2.0.5</xmlsec.version>
+ <!-- Authorization Drools Policy Provider -->
+ <version.org.drools>6.4.0.Final</version.org.drools>
+
<!-- Others -->
<apacheds.version>2.0.0-M17</apacheds.version>
<apacheds.codec.version>1.0.0-M23</apacheds.codec.version>
@@ -176,6 +179,7 @@
<module>wildfly</module>
<module>integration</module>
<module>adapters</module>
+ <module>authz</module>
<module>examples</module>
<module>testsuite</module>
</modules>
@@ -412,6 +416,15 @@
<version>${google.zxing.version}</version>
</dependency>
+ <!-- Authorization Drools Policy Provider -->
+ <dependency>
+ <groupId>org.drools</groupId>
+ <artifactId>drools-bom</artifactId>
+ <type>pom</type>
+ <version>${version.org.drools}</version>
+ <scope>import</scope>
+ </dependency>
+
<!-- Email Test Servers -->
<dependency>
<groupId>com.icegreen</groupId>
@@ -976,6 +989,49 @@
<artifactId>keycloak-services</artifactId>
<version>${project.version}</version>
</dependency>
+
+ <!-- Authorization -->
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-client</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-drools</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-resource</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-scope</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-identity</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-js</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-time</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-authz-policy-aggregate</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-as7-modules</artifactId>
server-spi/pom.xml 5(+5 -0)
diff --git a/server-spi/pom.xml b/server-spi/pom.xml
index 047a065..9b71e2d 100755
--- a/server-spi/pom.xml
+++ b/server-spi/pom.xml
@@ -30,6 +30,11 @@
<name>Keycloak Server SPI</name>
<description/>
+ <properties>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ </properties>
+
<dependencies>
<dependency>
<groupId>org.jboss.resteasy</groupId>
diff --git a/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java
new file mode 100644
index 0000000..ce2bc51
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/attribute/Attributes.java
@@ -0,0 +1,143 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.attribute;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+
+import static java.util.Collections.emptyList;
+
+/**
+ * <p>Holds attributes, their values and provides utlity methods to manage them.
+ *
+ * <p>In the future, it may be useful to provide different implementations for this interface in order to plug or integrate with different
+ * Policy Information Point (PIP).</p>
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface Attributes {
+
+ static Attributes from(Map<String, Collection<String>> attributes) {
+ return () -> attributes;
+ }
+
+ /**
+ * Converts to a {@link Map}.
+ *
+ * @return
+ */
+ Map<String, Collection<String>> toMap();
+
+ /**
+ * Checks if there is an attribute with the given <code>name</code>.
+ *
+ * @param name the attribute name
+ * @return true if any attribute with <code>name</code> exist. Otherwise, returns false.
+ */
+ default boolean exists(String name) {
+ return toMap().containsKey(name);
+ }
+
+ /**
+ * Checks if there is an attribute with the given <code>name</code> and <code>value</code>.
+ *
+ * @param name the attribute name
+ * @param value the attribute value
+ * @return true if any attribute with <code>name</code> and <code>value</code> exist. Otherwise, returns false.
+ */
+ default boolean containsValue(String name, String value) {
+ return toMap().getOrDefault(name, emptyList()).stream().anyMatch(value::equals);
+ }
+
+ /**
+ * Returns a {@link Entry} from where values can be obtained and parsed accordingly.
+ *
+ * @param name the attribute name
+ * @return an {@link Entry} holding the values for an attribute
+ */
+ default Entry getValue(String name) {
+ Collection<String> value = toMap().get(name);
+
+ if (value != null) {
+ return new Entry(name, value);
+ }
+
+ return null;
+ }
+
+ /**
+ * Holds an attribute and its values, providing useful methods for obtaining and formatting values. Specially useful
+ * for writing rule-based policies.
+ */
+ class Entry {
+
+ private final String[] values;
+ private final String name;
+
+ Entry(String name, Collection<String> values) {
+ this.name = name;
+ this.values = values.toArray(new String[values.size()]);
+ }
+
+ private String getName() {
+ return this.name;
+ }
+
+ public int size() {
+ return values.length;
+ }
+
+ public String asString(int idx) {
+ if (idx >= values.length) {
+ throw new IllegalArgumentException("Invalid index [" + idx + "]. Values are [" + values + "].");
+ }
+
+ return values[idx];
+ }
+
+ public int asInt(int idx) {
+ return Integer.parseInt(asString(idx));
+ }
+
+ public Date asDate(int idx, String pattern) {
+ try {
+ return new SimpleDateFormat(pattern).parse(asString(idx));
+ } catch (ParseException e) {
+ throw new RuntimeException("Error parsing date.", e);
+ }
+ }
+
+ public InetAddress asInetAddress(int idx) {
+ try {
+ return InetAddress.getByName(asString(idx));
+ } catch (UnknownHostException e) {
+ throw new RuntimeException("Error parsing address.", e);
+ }
+ }
+
+ public long asLong(int idx) {
+ return Long.parseLong(asString(idx));
+ }
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/attribute/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/attribute/package-info.java
new file mode 100644
index 0000000..c75b520
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/attribute/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Provides classes related with the representation of attributes and their manipulation.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+package org.keycloak.authorization.attribute;
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProvider.java b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
new file mode 100644
index 0000000..ff646bb
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProvider.java
@@ -0,0 +1,137 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization;
+
+import org.keycloak.authorization.permission.evaluator.Evaluators;
+import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
+
+/**
+ * <p>The main contract here is the creation of {@link org.keycloak.authorization.permission.evaluator.PermissionEvaluator} instances. Usually
+ * an application has a single {@link AuthorizationProvider} instance and threads servicing client requests obtain {@link org.keycloak.authorization.core.permission.evaluator.PermissionEvaluator}
+ * from the {@link #evaluators()} method.
+ *
+ * <p>The internal state of a {@link AuthorizationProvider} is immutable. This internal state includes all of the metadata
+ * used during the evaluation of policies.
+ *
+ * <p>Once created, {@link org.keycloak.authorization.permission.evaluator.PermissionEvaluator} instances can be obtained from the {@link #evaluators()} method:
+ *
+ * <pre>
+ * List<ResourcePermission> permissionsToEvaluate = getPermissions(); // the permissions to evaluate
+ * EvaluationContext evaluationContext = createEvaluationContext(); // the context with runtime environment information
+ * PermissionEvaluator evaluator = authorization.evaluators().from(permissionsToEvaluate, context);
+ *
+ * evaluator.evaluate(new Decision() {
+ *
+ * public void onDecision(Evaluation evaluation) {
+ * // do something on grant
+ * }
+ *
+ * });
+ * </pre>
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public final class AuthorizationProvider implements Provider {
+
+ private final DefaultPolicyEvaluator policyEvaluator;
+ private final Executor scheduller;
+ private final StoreFactory storeFactory;
+ private final List<PolicyProviderFactory> policyProviderFactories;
+ private final KeycloakSession keycloakSession;
+
+ public AuthorizationProvider(KeycloakSession session, StoreFactory storeFactory, Executor scheduller) {
+ this.keycloakSession = session;
+ this.storeFactory = storeFactory;
+ this.scheduller = scheduller;
+ this.policyProviderFactories = configurePolicyProviderFactories(session);
+ this.policyEvaluator = new DefaultPolicyEvaluator(this, this.policyProviderFactories);
+ }
+
+ public AuthorizationProvider(KeycloakSession session, StoreFactory storeFactory) {
+ this(session, storeFactory, Runnable::run);
+ }
+
+ /**
+ * Returns a {@link Evaluators} instance from where {@link org.keycloak.authorization.policy.evaluation.PolicyEvaluator} instances
+ * can be obtained.
+ *
+ * @return a {@link Evaluators} instance
+ */
+ public Evaluators evaluators() {
+ return new Evaluators(this.policyProviderFactories, this.policyEvaluator, this.scheduller);
+ }
+
+ /**
+ * Returns a {@link StoreFactory}.
+ *
+ * @return the {@link StoreFactory}
+ */
+ public StoreFactory getStoreFactory() {
+ return this.storeFactory;
+ }
+
+ /**
+ * Returns the registered {@link PolicyProviderFactory}.
+ *
+ * @return a {@link List} containing all registered {@link PolicyProviderFactory}
+ */
+ public List<PolicyProviderFactory> getProviderFactories() {
+ return this.policyProviderFactories;
+ }
+
+ /**
+ * Returns a {@link PolicyProviderFactory} given a <code>type</code>.
+ *
+ * @param type the type of the policy provider
+ * @param <F> the expected type of the provider
+ * @return a {@link PolicyProviderFactory} with the given <code>type</code>
+ */
+ public <F extends PolicyProviderFactory> F getProviderFactory(String type) {
+ return (F) getProviderFactories().stream().filter(policyProviderFactory -> policyProviderFactory.getId().equals(type)).findFirst().orElse(null);
+ }
+
+ public KeycloakSession getKeycloakSession() {
+ return this.keycloakSession;
+ }
+
+ private List<PolicyProviderFactory> configurePolicyProviderFactories(KeycloakSession session) {
+ List<ProviderFactory> providerFactories = session.getKeycloakSessionFactory().getProviderFactories(PolicyProvider.class);
+
+ if (providerFactories.isEmpty()) {
+ throw new RuntimeException("Could not find any policy provider.");
+ }
+
+ return providerFactories.stream().map(providerFactory -> (PolicyProviderFactory) providerFactory).collect(Collectors.toList());
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProviderFactory.java b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProviderFactory.java
new file mode 100644
index 0000000..ae4dcc2
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationProviderFactory.java
@@ -0,0 +1,27 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface AuthorizationProviderFactory extends ProviderFactory<AuthorizationProvider> {
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/AuthorizationSpi.java b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationSpi.java
new file mode 100644
index 0000000..65028b3
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/AuthorizationSpi.java
@@ -0,0 +1,48 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationSpi implements Spi {
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "authorization";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return AuthorizationProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return AuthorizationProviderFactory.class;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/Decision.java b/server-spi/src/main/java/org/keycloak/authorization/Decision.java
new file mode 100644
index 0000000..6ebd086
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/Decision.java
@@ -0,0 +1,41 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization;
+
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface Decision<D extends Evaluation> {
+
+ enum Effect {
+ PERMIT,
+ DENY
+ }
+
+ void onDecision(D evaluation);
+
+ default void onError(Throwable cause) {
+ throw new RuntimeException("Not implemented.", cause);
+ }
+
+ default void onComplete() {
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java b/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java
new file mode 100644
index 0000000..ebc9679
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/identity/Identity.java
@@ -0,0 +1,57 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.identity;
+
+import org.keycloak.authorization.attribute.Attributes;
+
+/**
+ * <p>Represents a security identity, which can be a person or non-person entity that was previously authenticated.
+ *
+ * <p>An {@link Identity} plays an important role during the evaluation of policies as they represent the entity to which one or more permissions
+ * should be granted or not, providing additional information and attributes that can be relevant to the different
+ * access control methods involved during the evaluation of policies.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface Identity {
+
+ /**
+ * Returns the unique identifier of this identity.
+ *
+ * @return the unique identifier of this identity
+ */
+ String getId();
+
+ /**
+ * Returns the attributes or claims associated with this identity.
+ *
+ * @return the attributes or claims associated with this identity
+ */
+ Attributes getAttributes();
+
+ /**
+ * Indicates if this identity is granted with a role with the given <code>roleName</code>.
+ *
+ * @param roleName the name of the role
+ *
+ * @return true if the identity has the given role. Otherwise, it returns false.
+ */
+ default boolean hasRole(String roleName) {
+ return getAttributes().containsValue("roles", roleName);
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/identity/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/identity/package-info.java
new file mode 100644
index 0000000..47a5746
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/identity/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Provides classes related with the representation and management of identities.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+package org.keycloak.authorization.identity;
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/model/package-info.java
new file mode 100644
index 0000000..38f9a8f
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/model/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Provides the domain model and any other type related with it
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+package org.keycloak.authorization.model;
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/Policy.java b/server-spi/src/main/java/org/keycloak/authorization/model/Policy.java
new file mode 100644
index 0000000..1960d6a
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/model/Policy.java
@@ -0,0 +1,193 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.model;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Represents an authorization policy and all the configuration associated with it.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface Policy {
+
+ /**
+ * Returns the unique identifier for this instance.
+ *
+ * @return the unique identifier for this instance
+ */
+ String getId();
+
+ /**
+ * Returns the type of this policy.
+ *
+ * @return the type of this policy
+ */
+ String getType();
+
+ /**
+ * Returns the {@link DecisionStrategy} for this policy.
+ *
+ * @return the decision strategy defined for this policy
+ */
+ DecisionStrategy getDecisionStrategy();
+
+ /**
+ * Sets the {DecisionStrategy} for this policy.
+ *
+ * @return the decision strategy for this policy
+ */
+ void setDecisionStrategy(DecisionStrategy decisionStrategy);
+
+ /**
+ * Returns the {@link Logic} for this policy.
+ *
+ * @return the decision strategy defined for this policy
+ */
+ Logic getLogic();
+
+ /**
+ * Sets the {Logic} for this policy.
+ *
+ * @return the decision strategy for this policy
+ */
+ void setLogic(Logic logic);
+
+ /**
+ * Returns a {@link Map} holding string-based key/value pairs representing any additional configuration for this policy.
+ *
+ * @return a map with any additional configuration defined for this policy.
+ */
+ Map<String, String> getConfig();
+
+ /**
+ * Sets a {@link Map} with string-based key/value pairs representing any additional configuration for this policy.
+ *
+ * @return a map with any additional configuration for this policy.
+ */
+ void setConfig(Map<String, String> config);
+
+ /**
+ * Returns the name of this policy.
+ *
+ * @return the name of this policy
+ */
+ String getName();
+
+ /**
+ * Sets an unique name to this policy.
+ *
+ * @param name an unique name
+ */
+ void setName(String name);
+
+ /**
+ * Returns the description of this policy.
+ *
+ * @return a description or null of there is no description
+ */
+ String getDescription();
+
+ /**
+ * Sets the description for this policy.
+ *
+ * @param description a description
+ */
+ void setDescription(String description);
+
+ /**
+ * Returns the {@link ResourceServer} where this policy belongs to.
+ *
+ * @return a resource server
+ */
+ <R extends ResourceServer> R getResourceServer();
+
+ /**
+ * Returns the {@link Policy} instances associated with this policy and used to evaluate authorization decisions when
+ * this policy applies.
+ *
+ * @return the associated policies or an empty set if no policy is associated with this policy
+ */
+ <P extends Policy> Set<P> getAssociatedPolicies();
+
+ /**
+ * Returns the {@link Resource} instances where this policy applies.
+ *
+ * @return a set with all resource instances where this policy applies. Or an empty set if there is no resource associated with this policy
+ */
+ <R extends Resource> Set<R> getResources();
+
+ /**
+ * Returns the {@link Scope} instances where this policy applies.
+ *
+ * @return a set with all scope instances where this policy applies. Or an empty set if there is no scope associated with this policy
+ */
+ <S extends Scope> Set<S> getScopes();
+
+ void addScope(Scope scope);
+
+ void removeScope(Scope scope);
+
+ void addAssociatedPolicy(Policy associatedPolicy);
+
+ void removeAssociatedPolicy(Policy associatedPolicy);
+
+ void addResource(Resource resource);
+
+ void removeResource(Resource resource);
+
+ /**
+ * The decision strategy dictates how the policies associated with a given policy are evaluated and how a final decision
+ * is obtained.
+ */
+ enum DecisionStrategy {
+ /**
+ * Defines that at least one policy must evaluate to a positive decision in order to the overall decision be also positive.
+ */
+ AFFIRMATIVE,
+
+ /**
+ * Defines that all policies must evaluate to a positive decision in order to the overall decision be also positive.
+ */
+ UNANIMOUS,
+
+ /**
+ * Defines that the number of positive decisions must be greater than the number of negative decisions. If the number of positive and negative is the same,
+ * the final decision will be negative.
+ */
+ CONSENSUS
+ }
+
+ /**
+ * The decision strategy dictates how the policies associated with a given policy are evaluated and how a final decision
+ * is obtained.
+ */
+ enum Logic {
+ /**
+ * Defines that this policy follows a positive logic. In other words, the final decision is the policy outcome.
+ */
+ POSITIVE,
+
+ /**
+ * Defines that this policy uses a logical negation. In other words, the final decision would be a negative of the policy outcome.
+ */
+ NEGATIVE,
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/Resource.java b/server-spi/src/main/java/org/keycloak/authorization/model/Resource.java
new file mode 100644
index 0000000..2bf2c6f
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/model/Resource.java
@@ -0,0 +1,116 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.model;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Represents a resource, which is usually protected by a set of policies within a resource server.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface Resource {
+
+ /**
+ * Returns the unique identifier for this instance.
+ *
+ * @return the unique identifier for this instance
+ */
+ String getId();
+
+ /**
+ * Returns the resource's name.
+ *
+ * @return the name of this resource
+ */
+ String getName();
+
+ /**
+ * Sets a name for this resource. The name must be unique.
+ *
+ * @param name the name of this resource
+ */
+ void setName(String name);
+
+ /**
+ * Returns a {@link java.net.URI} that uniquely identify this resource.
+ *
+ * @return an {@link java.net.URI} for this resource or null if not defined.
+ */
+ String getUri();
+
+ /**
+ * Sets a {@link java.net.URI} that uniquely identify this resource.
+ *
+ * @param uri an {@link java.net.URI} for this resource
+ */
+ void setUri(String uri);
+
+ /**
+ * Returns a string representing the type of this resource.
+ *
+ * @return the type of this resource or null if not defined
+ */
+ String getType();
+
+ /**
+ * Sets a string representing the type of this resource.
+ *
+ * @return the type of this resource or null if not defined
+ */
+ void setType(String type);
+
+ /**
+ * Returns a {@link List} containing all the {@link Scope} associated with this resource.
+ *
+ * @return a list with all scopes associated with this resource
+ */
+ <S extends Scope> List<S> getScopes();
+
+ /**
+ * Returns an icon {@link java.net.URI} for this resource.
+ *
+ * @return a uri for an icon
+ */
+ String getIconUri();
+
+ /**
+ * Sets an icon {@link java.net.URI} for this resource.
+ *
+ * @return a uri for an icon
+ */
+ void setIconUri(String iconUri);
+
+ /**
+ * Returns the {@link ResourceServer} to where this resource belongs to.
+ *
+ * @return the resource server associated with this resource
+ */
+ <R extends ResourceServer> R getResourceServer();
+
+ /**
+ * Returns the resource's owner, which is usually an identifier that uniquely identifies the resource's owner.
+ *
+ * @return the owner of this resource
+ */
+ String getOwner();
+
+ void updateScopes(Set<Scope> scopes);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/ResourceServer.java b/server-spi/src/main/java/org/keycloak/authorization/model/ResourceServer.java
new file mode 100644
index 0000000..2424c8d
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/model/ResourceServer.java
@@ -0,0 +1,91 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.model;
+
+/**
+ * Represents a resource server, whose resources are managed and protected. A resource server is basically an existing
+ * client application in Keycloak that will also act as a resource server.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ResourceServer {
+
+ /**
+ * Returns the unique identifier for this instance.
+ *
+ * @return the unique identifier for this instance
+ */
+ String getId();
+
+ /**
+ * Returns the identifier of the client application (which already exists in Keycloak) that is also acting as a resource
+ * server.
+ *
+ * @return the identifier of the client application associated with this instance.
+ */
+ String getClientId();
+
+ /**
+ * Indicates if the resource server is allowed to manage its own resources remotely using the Protection API.
+ *
+ * {@code true} if the resource server is allowed to managed them remotely
+ */
+ boolean isAllowRemoteResourceManagement();
+
+ /**
+ * Indicates if the resource server is allowed to manage its own resources remotely using the Protection API.
+ *
+ * @param allowRemoteResourceManagement {@code true} if the resource server is allowed to managed them remotely
+ */
+ void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement);
+
+ /**
+ * Returns the {@code PolicyEnforcementMode} configured for this instance.
+ *
+ * @return the {@code PolicyEnforcementMode} configured for this instance.
+ */
+ PolicyEnforcementMode getPolicyEnforcementMode();
+
+ /**
+ * Defines a {@code PolicyEnforcementMode} for this instance.
+ *
+ * @param enforcementMode one of the available options in {@code PolicyEnforcementMode}
+ */
+ void setPolicyEnforcementMode(PolicyEnforcementMode enforcementMode);
+
+ /**
+ * The policy enforcement mode dictates how authorization requests are handled by the server.
+ */
+ enum PolicyEnforcementMode {
+ /**
+ * Requests are denied by default even when there is no policy associated with a given resource.
+ */
+ ENFORCING,
+
+ /**
+ * Requests are allowed even when there is no policy associated with a given resource.
+ */
+ PERMISSIVE,
+
+ /**
+ * Completely disables the evaluation of policies and allow access to any resource.
+ */
+ DISABLED
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/model/Scope.java b/server-spi/src/main/java/org/keycloak/authorization/model/Scope.java
new file mode 100644
index 0000000..e13a789
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/model/Scope.java
@@ -0,0 +1,70 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.model;
+
+/**
+ * Represents a scope, which is usually associated with one or more resources in order to define the actions that can be performed
+ * or a specific access context.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface Scope {
+
+ /**
+ * Returns the unique identifier for this instance.
+ *
+ * @return the unique identifier for this instance
+ */
+ String getId();
+
+ /**
+ * Returns the name of this scope.
+ *
+ * @return the name of this scope
+ */
+ String getName();
+
+ /**
+ * Sets a name for this scope. The name must be unique.
+ *
+ * @param name the name of this scope
+ */
+ void setName(String name);
+
+ /**
+ * Returns an icon {@link java.net.URI} for this scope.
+ *
+ * @return a uri for an icon
+ */
+ String getIconUri();
+
+ /**
+ * Sets an icon {@link java.net.URI} for this scope.
+ *
+ * @return a uri for an icon
+ */
+ void setIconUri(String iconUri);
+
+ /**
+ * Returns the {@link ResourceServer} instance to where this scope belongs to.
+ *
+ * @return
+ */
+ ResourceServer getResourceServer();
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/package-info.java
new file mode 100644
index 0000000..6ff51af
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Fine-grained Authorization SPI.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+package org.keycloak.authorization;
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java
new file mode 100644
index 0000000..e26ad1c
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/Evaluators.java
@@ -0,0 +1,53 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.permission.evaluator;
+
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.DefaultPolicyEvaluator;
+import org.keycloak.authorization.policy.evaluation.EvaluationContext;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * A factory for the different {@link PermissionEvaluator} implementations.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public final class Evaluators {
+
+ private final List<PolicyProviderFactory> policyProviderFactories;
+ private final DefaultPolicyEvaluator policyEvaluator;
+ private final Executor scheduler;
+
+ public Evaluators(List<PolicyProviderFactory> policyProviderFactories, DefaultPolicyEvaluator policyEvaluator, Executor scheduler) {
+ this.policyProviderFactories = policyProviderFactories;
+ this.policyEvaluator = policyEvaluator;
+ this.scheduler = scheduler;
+ }
+
+ public PermissionEvaluator from(List<ResourcePermission> permissions, EvaluationContext evaluationContext) {
+ return schedule(permissions, evaluationContext);
+ }
+
+ public PermissionEvaluator schedule(List<ResourcePermission> permissions, EvaluationContext evaluationContext) {
+ return new ScheduledPermissionEvaluator(new IterablePermissionEvaluator(permissions.iterator(), evaluationContext, this.policyEvaluator), this.scheduler);
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java
new file mode 100644
index 0000000..dfda6a7
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/IterablePermissionEvaluator.java
@@ -0,0 +1,53 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.permission.evaluator;
+
+import org.keycloak.authorization.Decision;
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.EvaluationContext;
+import org.keycloak.authorization.policy.evaluation.PolicyEvaluator;
+
+import java.util.Iterator;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+class IterablePermissionEvaluator implements PermissionEvaluator {
+
+ private final Iterator<ResourcePermission> permissions;
+ private final EvaluationContext executionContext;
+ private final PolicyEvaluator policyEvaluator;
+
+ IterablePermissionEvaluator(Iterator<ResourcePermission> permissions, EvaluationContext executionContext, PolicyEvaluator policyEvaluator) {
+ this.permissions = permissions;
+ this.executionContext = executionContext;
+ this.policyEvaluator = policyEvaluator;
+ }
+
+ @Override
+ public void evaluate(Decision decision) {
+ try {
+ while (this.permissions.hasNext()) {
+ this.policyEvaluator.evaluate(this.permissions.next(), this.executionContext, decision);
+ }
+ decision.onComplete();
+ } catch (Throwable cause) {
+ decision.onError(cause);
+ }
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java
new file mode 100644
index 0000000..c129caf
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/PermissionEvaluator.java
@@ -0,0 +1,31 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.permission.evaluator;
+
+import org.keycloak.authorization.Decision;
+
+/**
+ * An {@link PermissionEvaluator} represents a source of {@link org.keycloak.authorization.permission.ResourcePermission}, responsible for emitting these permissions
+ * to a consumer in order to evaluate the authorization policies based on a {@link org.keycloak.authorization.policy.evaluation.EvaluationContext}.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PermissionEvaluator {
+
+ void evaluate(Decision decision);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java
new file mode 100644
index 0000000..13e08e4
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/permission/evaluator/ScheduledPermissionEvaluator.java
@@ -0,0 +1,43 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.permission.evaluator;
+
+import org.keycloak.authorization.Decision;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ * @see PermissionEvaluator
+ */
+class ScheduledPermissionEvaluator implements PermissionEvaluator {
+
+ private final PermissionEvaluator publisher;
+ private final Executor scheduler;
+
+ ScheduledPermissionEvaluator(PermissionEvaluator publisher, Executor scheduler) {
+ this.publisher = publisher;
+ this.scheduler = scheduler;
+ }
+
+ @Override
+ public void evaluate(Decision decision) {
+ CompletableFuture.runAsync(() -> publisher.evaluate(decision), scheduler);
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java b/server-spi/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java
new file mode 100644
index 0000000..1eef22a
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java
@@ -0,0 +1,71 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.permission;
+
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a permission for a given resource.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourcePermission {
+
+ private final Resource resource;
+ private final List<Scope> scopes;
+ private ResourceServer resourceServer;
+
+ public ResourcePermission(Resource resource, List<Scope> scopes, ResourceServer resourceServer) {
+ this.resource = resource;
+ this.scopes = scopes;
+ this.resourceServer = resourceServer;
+ }
+
+ /**
+ * Returns the resource to which this permission applies.
+ *
+ * @return the resource to which this permission applies
+ */
+ public Resource getResource() {
+ return this.resource;
+ }
+
+ /**
+ * Returns a list of permitted scopes associated with the resource
+ *
+ * @return a lit of permitted scopes
+ */
+ public List<Scope> getScopes() {
+ return Collections.unmodifiableList(this.scopes);
+ }
+
+ /**
+ * Returns the resource server associated with this permission.
+ *
+ * @return the resource server
+ */
+ public ResourceServer getResourceServer() {
+ return this.resourceServer;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
new file mode 100644
index 0000000..f06eb3f
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
@@ -0,0 +1,102 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.evaluation;
+
+import org.keycloak.authorization.Decision;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.permission.ResourcePermission;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class DecisionResultCollector implements Decision<DefaultEvaluation> {
+
+ private Map<ResourcePermission, Result> results = new HashMap();
+
+ @Override
+ public void onDecision(DefaultEvaluation evaluation) {
+ if (evaluation.getParentPolicy() != null) {
+ results.computeIfAbsent(evaluation.getPermission(), Result::new).policy(evaluation.getParentPolicy()).policy(evaluation.getPolicy()).setStatus(evaluation.getEffect());
+ } else {
+ results.computeIfAbsent(evaluation.getPermission(), Result::new).setStatus(evaluation.getEffect());
+ }
+ }
+
+ @Override
+ public void onComplete() {
+ for (Result result : results.values()) {
+ for (Result.PolicyResult policyResult : result.getResults()) {
+ if (isGranted(policyResult)) {
+ policyResult.setStatus(Effect.PERMIT);
+ } else {
+ policyResult.setStatus(Effect.DENY);
+ }
+ }
+
+ if (result.getResults().stream()
+ .filter(policyResult -> Effect.DENY.equals(policyResult.getStatus())).count() > 0) {
+ result.setStatus(Effect.DENY);
+ } else {
+ result.setStatus(Effect.PERMIT);
+ }
+ }
+
+ onComplete(results.values().stream().collect(Collectors.toList()));
+ }
+
+ protected abstract void onComplete(List<Result> results);
+
+ private boolean isGranted(Result.PolicyResult policyResult) {
+ List<Result.PolicyResult> values = policyResult.getAssociatedPolicies();
+
+ int grantCount = 0;
+ int denyCount = policyResult.getPolicy().getAssociatedPolicies().size();
+
+ for (Result.PolicyResult decision : values) {
+ if (decision.getStatus().equals(Effect.PERMIT)) {
+ grantCount++;
+ denyCount--;
+ }
+ }
+
+ Policy policy = policyResult.getPolicy();
+ Policy.DecisionStrategy decisionStrategy = policy.getDecisionStrategy();
+
+ if (decisionStrategy == null) {
+ decisionStrategy = Policy.DecisionStrategy.UNANIMOUS;
+ }
+
+ if (Policy.DecisionStrategy.AFFIRMATIVE.equals(decisionStrategy) && grantCount > 0) {
+ return true;
+ } else if (Policy.DecisionStrategy.UNANIMOUS.equals(decisionStrategy) && denyCount == 0) {
+ return true;
+ } else if (Policy.DecisionStrategy.CONSENSUS.equals(decisionStrategy)) {
+ if (grantCount > denyCount) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java
new file mode 100644
index 0000000..df379af
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultEvaluation.java
@@ -0,0 +1,105 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.evaluation;
+
+import org.keycloak.authorization.Decision;
+import org.keycloak.authorization.Decision.Effect;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Policy.Logic;
+import org.keycloak.authorization.permission.ResourcePermission;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class DefaultEvaluation implements Evaluation {
+
+ private final ResourcePermission permission;
+ private final EvaluationContext executionContext;
+ private final Decision decision;
+ private final Policy policy;
+ private final Policy parentPolicy;
+ private Effect effect;
+
+ public DefaultEvaluation(ResourcePermission permission, EvaluationContext executionContext, Policy parentPolicy, Policy policy, Decision decision) {
+ this.permission = permission;
+ this.executionContext = executionContext;
+ this.parentPolicy = parentPolicy;
+ this.policy = policy;
+ this.decision = decision;
+ }
+
+ /**
+ * Returns the {@link ResourcePermission} to be evaluated.
+ *
+ * @return the permission to be evaluated
+ */
+ public ResourcePermission getPermission() {
+ return this.permission;
+ }
+
+ /**
+ * Returns the {@link org.keycloak.authorization.permission.evaluator.PermissionEvaluator}. Which provides access to the whole evaluation runtime context.
+ *
+ * @return the evaluation context
+ */
+ public EvaluationContext getContext() {
+ return this.executionContext;
+ }
+
+ /**
+ * Grants all the requested permissions to the caller.
+ */
+ public void grant() {
+ if (policy != null && Logic.NEGATIVE.equals(policy.getLogic())) {
+ this.effect = Effect.DENY;
+ } else {
+ this.effect = Effect.PERMIT;
+ }
+
+ this.decision.onDecision(this);
+ }
+
+ public void deny() {
+ if (policy != null && Logic.NEGATIVE.equals(policy.getLogic())) {
+ this.effect = Effect.PERMIT;
+ } else {
+ this.effect = Effect.DENY;
+ }
+
+ this.decision.onDecision(this);
+ }
+
+ public Policy getPolicy() {
+ return this.policy;
+ }
+
+ public Policy getParentPolicy() {
+ return this.parentPolicy;
+ }
+
+ public Effect getEffect() {
+ return effect;
+ }
+
+ void denyIfNoEffect() {
+ if (this.effect == null) {
+ deny();
+ }
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
new file mode 100644
index 0000000..8b12558
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/DefaultPolicyEvaluator.java
@@ -0,0 +1,156 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.evaluation;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.Decision;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.ResourceServer.PolicyEnforcementMode;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.StoreFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class DefaultPolicyEvaluator implements PolicyEvaluator {
+
+ private final AuthorizationProvider authorization;
+ private Map<String, PolicyProviderFactory> policyProviders = new HashMap<>();
+
+ public DefaultPolicyEvaluator(AuthorizationProvider authorization, List<PolicyProviderFactory> policyProviderFactories) {
+ this.authorization = authorization;
+
+ for (PolicyProviderFactory providerFactory : policyProviderFactories) {
+ this.policyProviders.put(providerFactory.getId(), providerFactory);
+ }
+ }
+
+ @Override
+ public void evaluate(ResourcePermission permission, EvaluationContext executionContext, Decision decision) {
+ ResourceServer resourceServer = permission.getResourceServer();
+
+ if (PolicyEnforcementMode.DISABLED.equals(resourceServer.getPolicyEnforcementMode())) {
+ createEvaluation(permission, executionContext, decision, null, null).grant();
+ return;
+ }
+
+ StoreFactory storeFactory = this.authorization.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ AtomicInteger policiesCount = new AtomicInteger(0);
+ Consumer<Policy> consumer = createDecisionConsumer(permission, executionContext, decision, policiesCount);
+ Resource resource = permission.getResource();
+
+ if (resource != null) {
+ List<? extends Policy> resourcePolicies = policyStore.findByResource(resource.getId());
+
+ if (!resourcePolicies.isEmpty()) {
+ resourcePolicies.forEach(consumer);
+ }
+
+ if (resource.getType() != null) {
+ policyStore.findByResourceType(resource.getType(), resourceServer.getId()).forEach(consumer);
+ }
+
+ if (permission.getScopes().isEmpty() && !resource.getScopes().isEmpty()) {
+ policyStore.findByScopeIds(resource.getScopes().stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()).forEach(consumer);
+ }
+ }
+
+ if (!permission.getScopes().isEmpty()) {
+ policyStore.findByScopeIds(permission.getScopes().stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId()).forEach(consumer);
+ }
+
+ if (PolicyEnforcementMode.PERMISSIVE.equals(resourceServer.getPolicyEnforcementMode()) && policiesCount.get() == 0) {
+ createEvaluation(permission, executionContext, decision, null, null).grant();
+ }
+ }
+
+ private Consumer<Policy> createDecisionConsumer(ResourcePermission permission, EvaluationContext executionContext, Decision decision, AtomicInteger policiesCount) {
+ return (parentPolicy) -> {
+ if (hasRequestedScopes(permission, parentPolicy)) {
+ for (Policy associatedPolicy : parentPolicy.getAssociatedPolicies()) {
+ PolicyProviderFactory providerFactory = policyProviders.get(associatedPolicy.getType());
+
+ if (providerFactory == null) {
+ throw new RuntimeException("Could not find a policy provider for policy type [" + associatedPolicy.getType() + "].");
+ }
+
+ PolicyProvider policyProvider = providerFactory.create(associatedPolicy, this.authorization);
+
+ if (policyProvider == null) {
+ throw new RuntimeException("Unknown parentPolicy provider for type [" + associatedPolicy.getType() + "].");
+ }
+
+ DefaultEvaluation evaluation = createEvaluation(permission, executionContext, decision, parentPolicy, associatedPolicy);
+
+ policyProvider.evaluate(evaluation);
+ evaluation.denyIfNoEffect();
+
+ policiesCount.incrementAndGet();
+ }
+ }
+ };
+ }
+
+ private DefaultEvaluation createEvaluation(ResourcePermission permission, EvaluationContext executionContext, Decision decision, Policy parentPolicy, Policy associatedPolicy) {
+ return new DefaultEvaluation(permission, executionContext, parentPolicy, associatedPolicy, decision);
+ }
+
+ private boolean hasRequestedScopes(final ResourcePermission permission, final Policy policy) {
+ if (permission.getScopes().isEmpty()) {
+ return true;
+ }
+
+ if (policy.getScopes().isEmpty()) {
+ return true;
+ }
+
+ boolean hasScope = true;
+
+ for (Scope givenScope : policy.getScopes()) {
+ boolean hasGivenScope = false;
+
+ for (Scope scope : permission.getScopes()) {
+ if (givenScope.getId().equals(scope.getId())) {
+ hasGivenScope = true;
+ break;
+ }
+ }
+
+ if (!hasGivenScope) {
+ return false;
+ }
+ }
+
+ return hasScope;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java
new file mode 100644
index 0000000..f5b0868
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Evaluation.java
@@ -0,0 +1,54 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.evaluation;
+
+import org.keycloak.authorization.permission.ResourcePermission;
+
+/**
+ * <p>An {@link Evaluation} is mainly used by {@link org.keycloak.authorization.policy.provider.PolicyProvider} in order to evaluate a single
+ * and specific {@link ResourcePermission} against the configured policies.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface Evaluation {
+
+ /**
+ * Returns the {@link ResourcePermission} to be evaluated.
+ *
+ * @return the permission to be evaluated
+ */
+ ResourcePermission getPermission();
+
+ /**
+ * Returns the {@link EvaluationContext}. Which provides access to the whole evaluation runtime context.
+ *
+ * @return the evaluation context
+ */
+ EvaluationContext getContext();
+
+ /**
+ * Grants the requested permission to the caller.
+ */
+ void grant();
+
+ /**
+ * Denies the requested permission.
+ */
+ void deny();
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/EvaluationContext.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/EvaluationContext.java
new file mode 100644
index 0000000..db5ed04
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/EvaluationContext.java
@@ -0,0 +1,45 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.evaluation;
+
+import org.keycloak.authorization.attribute.Attributes;
+import org.keycloak.authorization.identity.Identity;
+
+/**
+ * This interface serves as a bridge between the policy evaluation runtime and the environment in which it is running. When evaluating
+ * policies, this interface can be used to query information from the execution environment/context and enrich decisions.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface EvaluationContext {
+
+ /**
+ * Returns the {@link Identity} that represents an entity (person or non-person) to which the permissions must be granted, or not.
+ *
+ * @return the identity to which the permissions must be granted, or not
+ */
+ Identity getIdentity();
+
+ /**
+ * Returns all attributes within the current execution and runtime environment.
+ *
+ * @return the attributes within the current execution and runtime environment
+ */
+ Attributes getAttributes();
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/package-info.java
new file mode 100644
index 0000000..dcae2ed
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Provides classes related with the evaluation of policies.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+package org.keycloak.authorization.policy.evaluation;
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java
new file mode 100644
index 0000000..c380ba6
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/PolicyEvaluator.java
@@ -0,0 +1,38 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.evaluation;
+
+import org.keycloak.authorization.Decision;
+import org.keycloak.authorization.permission.ResourcePermission;
+
+/**
+ * <p>A {@link PolicyEvaluator} evaluates authorization policies based on a given {@link ResourcePermission}, sending
+ * the results to a {@link Decision} point through the methods defined in that interface.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PolicyEvaluator {
+
+ /**
+ * Starts the evaluation of the configured authorization policies.
+ *
+ * @param decision a {@link Decision} point to where notifications events will be delivered during the evaluation
+ */
+ void evaluate(ResourcePermission permission, EvaluationContext executionContext, Decision decision);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java
new file mode 100644
index 0000000..325af3d
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/evaluation/Result.java
@@ -0,0 +1,120 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.evaluation;
+
+import org.keycloak.authorization.Decision.Effect;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.permission.ResourcePermission;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class Result {
+
+ private final ResourcePermission permission;
+ private List<PolicyResult> results = new ArrayList<>();
+ private Effect status;
+
+ public Result(ResourcePermission permission) {
+ this.permission = permission;
+ }
+
+ public ResourcePermission getPermission() {
+ return permission;
+ }
+
+ public List<PolicyResult> getResults() {
+ return results;
+ }
+
+ public PolicyResult policy(Policy policy) {
+ for (PolicyResult result : this.results) {
+ if (result.getPolicy().equals(policy)) {
+ return result;
+ }
+ }
+
+ PolicyResult policyResult = new PolicyResult(policy);
+
+ this.results.add(policyResult);
+
+ return policyResult;
+ }
+
+ public void setStatus(final Effect status) {
+ this.status = status;
+ }
+
+ public Effect getEffect() {
+ return status;
+ }
+
+ public static class PolicyResult {
+
+ private final Policy policy;
+ private List<PolicyResult> associatedPolicies = new ArrayList<>();
+ private Effect status;
+
+ public PolicyResult(Policy policy) {
+ this.policy = policy;
+ }
+
+ public PolicyResult status(Effect status) {
+ this.status = status;
+ return this;
+ }
+
+ public PolicyResult policy(Policy policy) {
+ return getPolicy(policy, this.associatedPolicies);
+ }
+
+ private PolicyResult getPolicy(Policy policy, List<PolicyResult> results) {
+ for (PolicyResult result : results) {
+ if (result.getPolicy().equals(policy)) {
+ return result;
+ }
+ }
+
+ PolicyResult policyResult = new PolicyResult(policy);
+
+ results.add(policyResult);
+
+ return policyResult;
+ }
+
+ public Policy getPolicy() {
+ return policy;
+ }
+
+ public List<PolicyResult> getAssociatedPolicies() {
+ return associatedPolicies;
+ }
+
+ public Effect getStatus() {
+ return status;
+ }
+
+ public void setStatus(final Effect status) {
+ this.status = status;
+ }
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/package-info.java
new file mode 100644
index 0000000..6a66949
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Provides classes and a SPI to plug different policy providers.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+package org.keycloak.authorization.policy.provider;
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProvider.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProvider.java
new file mode 100644
index 0000000..2405c3b
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProvider.java
@@ -0,0 +1,29 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.policy.provider;
+
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.provider.Provider;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PolicyProvider extends Provider {
+
+ void evaluate(Evaluation evaluation);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderAdminService.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderAdminService.java
new file mode 100644
index 0000000..d26208e
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderAdminService.java
@@ -0,0 +1,33 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.provider;
+
+import org.keycloak.authorization.model.Policy;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PolicyProviderAdminService {
+
+ void onCreate(Policy policy);
+
+ void onUpdate(Policy policy);
+
+ void onRemove(Policy policy);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java
new file mode 100644
index 0000000..1beedd9
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicyProviderFactory.java
@@ -0,0 +1,39 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.provider;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.provider.ProviderEvent;
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PolicyProviderFactory extends ProviderFactory<PolicyProvider> {
+
+ String getName();
+
+ String getGroup();
+
+ PolicyProvider create(Policy policy, AuthorizationProvider authorization);
+
+ PolicyProviderAdminService getAdminResource(ResourceServer resourceServer);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicySpi.java b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicySpi.java
new file mode 100644
index 0000000..422981d
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/policy/provider/PolicySpi.java
@@ -0,0 +1,48 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.provider;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicySpi implements Spi {
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "policy";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return PolicyProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return PolicyProviderFactory.class;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/AuthorizationStoreFactory.java b/server-spi/src/main/java/org/keycloak/authorization/store/AuthorizationStoreFactory.java
new file mode 100644
index 0000000..dac1b33
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/AuthorizationStoreFactory.java
@@ -0,0 +1,63 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.store;
+
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.authorization.store.syncronization.ClientApplicationSynchronizer;
+import org.keycloak.authorization.store.syncronization.RealmSynchronizer;
+import org.keycloak.authorization.store.syncronization.Synchronizer;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel.ClientRemovedEvent;
+import org.keycloak.models.RealmModel.RealmRemovedEvent;
+import org.keycloak.provider.ProviderEvent;
+import org.keycloak.provider.ProviderFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface AuthorizationStoreFactory extends ProviderFactory<StoreFactory> {
+
+ @Override
+ default void postInit(KeycloakSessionFactory factory) {
+ registerSynchronizationListeners(factory);
+ }
+
+ default void registerSynchronizationListeners(KeycloakSessionFactory factory) {
+ Map<Class<? extends ProviderEvent>, Synchronizer> synchronizers = new HashMap<>();
+
+ synchronizers.put(ClientRemovedEvent.class, new ClientApplicationSynchronizer());
+ synchronizers.put(RealmRemovedEvent.class, new RealmSynchronizer());
+
+ factory.register(event -> {
+ try {
+ synchronizers.forEach((eventType, synchronizer) -> {
+ if (eventType.isInstance(event)) {
+ synchronizer.synchronize(event, factory);
+ }
+ });
+ } catch (Exception e) {
+ throw new RuntimeException("Error synchronizing authorization data.", e);
+ }
+ });
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/package-info.java b/server-spi/src/main/java/org/keycloak/authorization/store/package-info.java
new file mode 100644
index 0000000..d9800da
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Provides classes and a SPI to plug different metadata storage implementations.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+package org.keycloak.authorization.store;
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/PolicyStore.java b/server-spi/src/main/java/org/keycloak/authorization/store/PolicyStore.java
new file mode 100644
index 0000000..f55db99
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/PolicyStore.java
@@ -0,0 +1,119 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.store;
+
+
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+
+import java.util.List;
+
+/**
+ * A {@link PolicyStore} is responsible to manage the persistence of {@link Policy} instances.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface PolicyStore {
+
+ /**
+ * Creates a new {@link Policy} instance. The new instance is not necessarily persisted though, which may require
+ * a call to the {#save} method to actually make it persistent.
+ *
+ * @param name the name of the policy
+ * @param type the type of the policy
+ * @param resourceServer the resource server to which this policy belongs
+ * @return a new instance of {@link Policy}
+ */
+ Policy create(String name, String type, ResourceServer resourceServer);
+
+ /**
+ * Deletes a policy from the underlying persistence mechanism.
+ *
+ * @param id the id of the policy to delete
+ */
+ void delete(String id);
+
+ /**
+ * Returns a {@link Policy} with the given <code>id</code>
+ *
+ * @param id the identifier of the policy
+ * @return a policy with the given identifier.
+ */
+ Policy findById(String id);
+
+ /**
+ * Returns a {@link Policy} with the given <code>name</code>
+ *
+ * @param name the name of the policy
+ * @param resourceServerId the resource server id
+ * @return a policy with the given name.
+ */
+ Policy findByName(String name, String resourceServerId);
+
+ /**
+ * Returns a list of {@link Policy} associated with a {@link ResourceServer} with the given <code>resourceServerId</code>.
+ *
+ * @param resourceServerId the identifier of a resource server
+ * @return a list of policies that belong to the given resource server
+ */
+ List<Policy> findByResourceServer(String resourceServerId);
+
+ /**
+ * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given <code>resourceId</code>.
+ *
+ * @param resourceId the identifier of a resource
+ * @return a list of policies associated with the given resource
+ */
+ List<Policy> findByResource(String resourceId);
+
+ /**
+ * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Resource} with the given <code>type</code>.
+ *
+ * @param resourceType the type of a resource
+ * @param resourceServerId the resource server id
+ * @return a list of policies associated with the given resource type
+ */
+ List<Policy> findByResourceType(String resourceType, String resourceServerId);
+
+ /**
+ * Returns a list of {@link Policy} associated with a {@link org.keycloak.authorization.core.model.Scope} with the given <code>scopeIds</code>.
+ *
+ * @param scopeIds the id of the scopes
+ * @param resourceServerId the resource server id
+ * @return a list of policies associated with the given scopes
+ */
+ List<Policy> findByScopeIds(List<String> scopeIds, String resourceServerId);
+
+ /**
+ * Returns a list of {@link Policy} with the given <code>type</code>.
+ *
+ * @param type the type of the policy
+ * @return a list of policies with the given type
+ */
+ List<Policy> findByType(String type);
+
+ /**
+ * Returns a list of {@link Policy} that depends on another policy with the given <code>id</code>.
+ *
+ * @param id the id of the policy to query its dependents
+ * @return a list of policies that depends on the a policy with the given identifier
+ */
+ List<Policy> findDependentPolicies(String id);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/ResourceServerStore.java b/server-spi/src/main/java/org/keycloak/authorization/store/ResourceServerStore.java
new file mode 100644
index 0000000..742f98b
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/ResourceServerStore.java
@@ -0,0 +1,63 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.store;
+
+import org.keycloak.authorization.model.ResourceServer;
+
+/**
+ * A {@link ResourceServerStore} is responsible to manage the persistence of {@link ResourceServer} instances.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ResourceServerStore {
+
+ /**
+ * <p>Creates a {@link ResourceServer} instance backed by this persistent storage implementation.
+ *
+ * @param clientId the client id acting as a resource server
+ *
+ * @return an instance backed by the underlying storage implementation
+ */
+ ResourceServer create(String clientId);
+
+ /**
+ * Removes a {@link ResourceServer} instance, with the given {@code id} from the persistent storage.
+ *
+ * @param id the identifier of an existing resource server instance
+ */
+ void delete(String id);
+
+ /**
+ * Returns a {@link ResourceServer} instance based on its identifier.
+ *
+ * @param id the identifier of an existing resource server instance
+ *
+ * @return the resource server instance with the given identifier or null if no instance was found
+ */
+ ResourceServer findById(String id);
+
+ /**
+ * Returns a {@link ResourceServer} instance based on the identifier of a client application.
+ *
+ * @param id the identifier of an existing client application
+ *
+ * @return the resource server instance, with the given client id or null if no instance was found
+ */
+ ResourceServer findByClient(String id);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/ResourceStore.java b/server-spi/src/main/java/org/keycloak/authorization/store/ResourceStore.java
new file mode 100644
index 0000000..5b92808
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/ResourceStore.java
@@ -0,0 +1,99 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.store;
+
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A {@link ResourceStore} is responsible to manage the persistence of {@link Resource} instances.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ResourceStore {
+
+ /**
+ * <p>Creates a {@link Resource} instance backed by this persistent storage implementation.
+ *
+ * @param name the name of this resource. It must be unique.
+ * @param resourceServer the resource server to where the given resource belongs to
+ * @param owner the owner of this resource or null if the resource server is the owner
+ * @return an instance backed by the underlying storage implementation
+ */
+ Resource create(String name, ResourceServer resourceServer, String owner);
+
+ /**
+ * Removes a {@link Resource} instance, with the given {@code id} from the persistent storage.
+ *
+ * @param id the identifier of an existing resource instance
+ */
+ void delete(String id);
+
+ /**
+ * Returns a {@link Resource} instance based on its identifier.
+ *
+ * @param id the identifier of an existing resource instance
+ * @return the resource instance with the given identifier or null if no instance was found
+ */
+ Resource findById(String id);
+
+ /**
+ * Finds all {@link Resource} instances with the given {@code ownerId}.
+ *
+ * @param ownerId the identifier of the owner
+ * @return a list with all resource instances owned by the given owner
+ */
+ List<Resource> findByOwner(String ownerId);
+
+ /**
+ * Finds all {@link Resource} instances associated with a given resource server.
+ *
+ * @param resourceServerId the identifier of the resource server
+ * @return a list with all resources associated with the given resource server
+ */
+ List<Resource> findByResourceServer(String resourceServerId);
+
+ /**
+ * Finds all {@link Resource} associated with a given scope.
+ *
+ * @param id one or more scope identifiers
+ * @return a list of resources associated with the given scope(s)
+ */
+ List<Resource> findByScope(String... id);
+
+ /**
+ * Find a {@link Resource} by its name.
+ *
+ * @param name the name of the resource
+ * @param resourceServerId the identifier of the resource server
+ * @return a resource with the given name
+ */
+ Resource findByName(String name, String resourceServerId);
+
+ /**
+ * Finds all {@link Resource} with the given type.
+ *
+ * @param type the type of the resource
+ * @return a list of resources with the given type
+ */
+ List<Resource> findByType(String type);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/ScopeStore.java b/server-spi/src/main/java/org/keycloak/authorization/store/ScopeStore.java
new file mode 100644
index 0000000..501217f
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/ScopeStore.java
@@ -0,0 +1,78 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.store;
+
+
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+
+import java.util.List;
+
+/**
+ * A {@link ScopeStore} is responsible to manage the persistence of {@link Scope} instances.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ScopeStore {
+
+ /**
+ * Creates a new {@link Scope} instance. The new instance is not necessarily persisted though, which may require
+ * a call to the {#save} method to actually make it persistent.
+ *
+ * @param name the name of the scope
+ * @param resourceServer the resource server to which this scope belongs
+ *
+ * @return a new instance of {@link Scope}
+ */
+ Scope create(String name, ResourceServer resourceServer);
+
+ /**
+ * Deletes a scope from the underlying persistence mechanism.
+ *
+ * @param id the id of the scope to delete
+ */
+ void delete(String id);
+
+ /**
+ * Returns a {@link Scope} with the given <code>id</code>
+ *
+ * @param id the identifier of the scope
+ *
+ * @return a scope with the given identifier.
+ */
+ Scope findById(String id);
+
+ /**
+ * Returns a {@link Scope} with the given <code>name</code>
+ *
+ * @param name the name of the scope
+ *
+ * @param resourceServerId
+ * @return a scope with the given name.
+ */
+ Scope findByName(String name, String resourceServerId);
+
+ /**
+ * Returns a list of {@link Scope} associated with a {@link ResourceServer} with the given <code>resourceServerId</code>.
+ *
+ * @param resourceServerId the identifier of a resource server
+ *
+ * @return a list of scopes that belong to the given resource server
+ */
+ List<Scope> findByResourceServer(String id);
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactory.java b/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactory.java
new file mode 100644
index 0000000..4f50c11
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactory.java
@@ -0,0 +1,61 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.store;
+
+import org.keycloak.provider.Provider;
+
+/**
+ * <p>A factory for the different types of storages that manage the persistence of the domain model types.
+ *
+ * <p>Implementations of this interface are usually related with the creation of those storage types accordingly with a
+ * specific persistence mechanism such as relational and NoSQL databases, filesystem, etc.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface StoreFactory extends Provider {
+
+ /**
+ * Returns a {@link ResourceStore}.
+ *
+ * @return the resource store
+ */
+ ResourceStore getResourceStore();
+
+ /**
+ * Returns a {@link ResourceServerStore}.
+ *
+ * @return the resource server store
+ */
+ ResourceServerStore getResourceServerStore();
+
+ /**
+ * Returns a {@link ScopeStore}.
+ *
+ * @return the scope store
+ */
+ ScopeStore getScopeStore();
+
+ /**
+ * Returns a {@link PolicyStore}.
+ *
+ * @return the policy store
+ */
+ PolicyStore getPolicyStore();
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactorySpi.java b/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactorySpi.java
new file mode 100644
index 0000000..53bfb25
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/StoreFactorySpi.java
@@ -0,0 +1,48 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.store;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class StoreFactorySpi implements Spi {
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "authorizationPersister";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return StoreFactory.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return AuthorizationStoreFactory.class;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java
new file mode 100644
index 0000000..67683ff
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/ClientApplicationSynchronizer.java
@@ -0,0 +1,51 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.store.syncronization;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.store.AuthorizationStoreFactory;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel.ClientRemovedEvent;
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ClientApplicationSynchronizer implements Synchronizer<ClientRemovedEvent> {
+
+ @Override
+ public void synchronize(ClientRemovedEvent event, KeycloakSessionFactory factory) {
+ ProviderFactory<AuthorizationProvider> providerFactory = factory.getProviderFactory(AuthorizationProvider.class);
+ AuthorizationProvider authorizationProvider = providerFactory.create(event.getKeycloakSession());
+ StoreFactory storeFactory = authorizationProvider.getStoreFactory();
+ ResourceServerStore store = storeFactory.getResourceServerStore();
+ ResourceServer resourceServer = store.findByClient(event.getClient().getId());
+
+ if (resourceServer != null) {
+ String id = resourceServer.getId();
+ storeFactory.getResourceStore().findByResourceServer(id).forEach(resource -> storeFactory.getResourceStore().delete(resource.getId()));
+ storeFactory.getScopeStore().findByResourceServer(id).forEach(scope -> storeFactory.getScopeStore().delete(scope.getId()));
+ storeFactory.getPolicyStore().findByResourceServer(id).forEach(scope -> storeFactory.getPolicyStore().delete(scope.getId()));
+ storeFactory.getResourceServerStore().delete(id);
+ }
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java
new file mode 100644
index 0000000..4f0ef32
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/RealmSynchronizer.java
@@ -0,0 +1,50 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.store.syncronization;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel.RealmRemovedEvent;
+import org.keycloak.provider.ProviderFactory;
+
+/*
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RealmSynchronizer implements Synchronizer<RealmRemovedEvent> {
+ @Override
+ public void synchronize(RealmRemovedEvent event, KeycloakSessionFactory factory) {
+ ProviderFactory<AuthorizationProvider> providerFactory = factory.getProviderFactory(AuthorizationProvider.class);
+ AuthorizationProvider authorizationProvider = providerFactory.create(event.getKeycloakSession());
+ StoreFactory storeFactory = authorizationProvider.getStoreFactory();
+
+ event.getRealm().getClients().forEach(clientModel -> {
+ ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(clientModel.getClientId());
+
+ if (resourceServer != null) {
+ String id = resourceServer.getId();
+ storeFactory.getResourceStore().findByResourceServer(id).forEach(resource -> storeFactory.getResourceStore().delete(resource.getId()));
+ storeFactory.getScopeStore().findByResourceServer(id).forEach(scope -> storeFactory.getScopeStore().delete(scope.getId()));
+ storeFactory.getPolicyStore().findByResourceServer(id).forEach(scope -> storeFactory.getPolicyStore().delete(scope.getId()));
+ storeFactory.getResourceServerStore().delete(id);
+ }
+ });
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/Synchronizer.java b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/Synchronizer.java
new file mode 100644
index 0000000..eb07947
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/authorization/store/syncronization/Synchronizer.java
@@ -0,0 +1,31 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.store.syncronization;
+
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.ProviderEvent;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface Synchronizer<E extends ProviderEvent> {
+
+ void synchronize(E event, KeycloakSessionFactory factory);
+
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/AdminRoles.java b/server-spi/src/main/java/org/keycloak/models/AdminRoles.java
index 34cdb36..24455b8 100755
--- a/server-spi/src/main/java/org/keycloak/models/AdminRoles.java
+++ b/server-spi/src/main/java/org/keycloak/models/AdminRoles.java
@@ -37,13 +37,15 @@ public class AdminRoles {
public static String VIEW_CLIENTS = "view-clients";
public static String VIEW_EVENTS = "view-events";
public static String VIEW_IDENTITY_PROVIDERS = "view-identity-providers";
+ public static String VIEW_AUTHORIZATION = "view-authorization";
public static String MANAGE_REALM = "manage-realm";
public static String MANAGE_USERS = "manage-users";
public static String MANAGE_IDENTITY_PROVIDERS = "manage-identity-providers";
public static String MANAGE_CLIENTS = "manage-clients";
public static String MANAGE_EVENTS = "manage-events";
+ public static String MANAGE_AUTHORIZATION = "manage-authorization";
- public static String[] ALL_REALM_ROLES = {CREATE_CLIENT, VIEW_REALM, VIEW_USERS, VIEW_CLIENTS, VIEW_EVENTS, VIEW_IDENTITY_PROVIDERS, MANAGE_REALM, MANAGE_USERS, MANAGE_CLIENTS, MANAGE_EVENTS, MANAGE_IDENTITY_PROVIDERS};
+ public static String[] ALL_REALM_ROLES = {CREATE_CLIENT, VIEW_REALM, VIEW_USERS, VIEW_CLIENTS, VIEW_EVENTS, VIEW_IDENTITY_PROVIDERS, VIEW_AUTHORIZATION, MANAGE_REALM, MANAGE_USERS, MANAGE_CLIENTS, MANAGE_EVENTS, MANAGE_IDENTITY_PROVIDERS, MANAGE_AUTHORIZATION};
}
diff --git a/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactoryProvider.java b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactoryProvider.java
new file mode 100644
index 0000000..3be3b78
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactoryProvider.java
@@ -0,0 +1,27 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.authorization;
+
+import org.keycloak.authorization.store.StoreFactory;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface CachedStoreFactoryProvider extends StoreFactory {
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactorySpi.java b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactorySpi.java
new file mode 100644
index 0000000..226949d
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreFactorySpi.java
@@ -0,0 +1,48 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.authorization;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class CachedStoreFactorySpi implements Spi {
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "authz-fached-store-factory";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return CachedStoreFactoryProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return CachedStoreProviderFactory.class;
+ }
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreProviderFactory.java b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreProviderFactory.java
new file mode 100644
index 0000000..b8563cb
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/models/cache/authorization/CachedStoreProviderFactory.java
@@ -0,0 +1,27 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.models.cache.authorization;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface CachedStoreProviderFactory extends ProviderFactory<CachedStoreFactoryProvider> {
+}
diff --git a/server-spi/src/main/java/org/keycloak/models/Constants.java b/server-spi/src/main/java/org/keycloak/models/Constants.java
index 460d08f..7f998df 100755
--- a/server-spi/src/main/java/org/keycloak/models/Constants.java
+++ b/server-spi/src/main/java/org/keycloak/models/Constants.java
@@ -37,6 +37,10 @@ public interface Constants {
String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};
String OFFLINE_ACCESS_ROLE = OAuth2Constants.OFFLINE_ACCESS;
+ String AUTHZ_UMA_PROTECTION = "uma_protection";
+ String AUTHZ_UMA_AUTHORIZATION = "uma_authorization";
+ String[] AUTHZ_DEFAULT_AUTHORIZATION_ROLES = {AUTHZ_UMA_AUTHORIZATION};
+
String DEFAULT_HASH_ALGORITHM = "pbkdf2";
// 15 minutes
diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
index 1c42e45..9fe36ac 100755
--- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java
@@ -37,10 +37,20 @@ public interface RealmModel extends RoleContainerModel {
RealmModel getCreatedRealm();
}
+ interface RealmRemovedEvent extends ProviderEvent {
+ RealmModel getRealm();
+ KeycloakSession getKeycloakSession();
+ }
+
interface ClientCreationEvent extends ProviderEvent {
ClientModel getCreatedClient();
}
+ interface ClientRemovedEvent extends ProviderEvent {
+ ClientModel getClient();
+ KeycloakSession getKeycloakSession();
+ }
+
interface UserFederationProviderCreationEvent extends ProviderEvent {
UserFederationProviderModel getCreatedFederationProvider();
RealmModel getRealm();
diff --git a/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java b/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java
index 24c60b3..00542eb 100755
--- a/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/RoleContainerModel.java
@@ -17,6 +17,8 @@
package org.keycloak.models;
+import org.keycloak.provider.ProviderEvent;
+
import java.util.List;
import java.util.Set;
@@ -25,6 +27,12 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public interface RoleContainerModel {
+
+ interface RoleRemovedEvent extends ProviderEvent {
+ RoleModel getRole();
+ KeycloakSession getKeycloakSession();
+ }
+
String getId();
RoleModel getRole(String name);
diff --git a/server-spi/src/main/java/org/keycloak/models/UserModel.java b/server-spi/src/main/java/org/keycloak/models/UserModel.java
index 0453977..013ff9c 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserModel.java
@@ -17,6 +17,8 @@
package org.keycloak.models;
+import org.keycloak.provider.ProviderEvent;
+
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -32,6 +34,11 @@ public interface UserModel extends RoleMapperModel {
String EMAIL = "email";
String LOCALE = "locale";
+ interface UserRemovedEvent extends ProviderEvent {
+ UserModel getUser();
+ KeycloakSession getKeycloakSession();
+ }
+
String getId();
String getUsername();
diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 24e3ac8..0bec462 100755
--- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -17,6 +17,9 @@
package org.keycloak.models.utils;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.store.ResourceServerStore;
import org.keycloak.hash.Pbkdf2PasswordHashProvider;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.Constants;
@@ -88,6 +91,8 @@ import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
+import static java.lang.Boolean.TRUE;
+
public class RepresentationToModel {
private static Logger logger = Logger.getLogger(RepresentationToModel.class);
@@ -984,6 +989,21 @@ public class RepresentationToModel {
if (resourceRep.isUseTemplateMappers() != null) client.setUseTemplateMappers(resourceRep.isUseTemplateMappers());
else client.setUseTemplateMappers(resourceRep.getClientTemplate() != null);
+ boolean createResourceServer = TRUE.equals(resourceRep.getAuthorizationServicesEnabled());
+
+ if (createResourceServer) {
+ AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class);
+ ResourceServerStore resourceServerStore = provider.getStoreFactory().getResourceServerStore();
+
+ client.setServiceAccountsEnabled(true);
+ client.setBearerOnly(false);
+ client.setPublicClient(false);
+
+ ResourceServer resourceServer = resourceServerStore.create(client.getId());
+
+ resourceServer.setAllowRemoteResourceManagement(true);
+ resourceServer.setPolicyEnforcementMode(ResourceServer.PolicyEnforcementMode.ENFORCING);
+ }
return client;
}
diff --git a/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProvider.java b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProvider.java
new file mode 100644
index 0000000..e30f5da
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.protocol.oidc;
+
+import org.keycloak.provider.Provider;
+
+import javax.ws.rs.core.Response;
+
+/**
+ * Provides introspection for a determined OAuth2 token type.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface TokenIntrospectionProvider extends Provider {
+
+ /**
+ * Introspect the <code>token</code>.
+ *
+ * @param token the token to introspect.
+ * @return the response with the information about the token
+ */
+ Response introspect(String token);
+}
diff --git a/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProviderFactory.java b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProviderFactory.java
new file mode 100644
index 0000000..48b7556
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionProviderFactory.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.protocol.oidc;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * A factory that creates {@link TokenIntrospectionProvider} instances.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface TokenIntrospectionProviderFactory extends ProviderFactory<TokenIntrospectionProvider> {
+}
diff --git a/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionSpi.java b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionSpi.java
new file mode 100644
index 0000000..4eb6d39
--- /dev/null
+++ b/server-spi/src/main/java/org/keycloak/protocol/oidc/TokenIntrospectionSpi.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.protocol.oidc;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * <p>A {@link Spi} to support additional tokens types to the OAuth2 Token Introspection Endpoint.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class TokenIntrospectionSpi implements Spi {
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "oauth2-token-introspection";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return TokenIntrospectionProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return TokenIntrospectionProviderFactory.class;
+ }
+}
diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 63015cf..9a84898 100755
--- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -53,4 +53,9 @@ org.keycloak.authentication.RequiredActionSpi
org.keycloak.authentication.FormAuthenticatorSpi
org.keycloak.authentication.FormActionSpi
org.keycloak.cluster.ClusterSpi
+org.keycloak.authorization.policy.provider.PolicySpi
+org.keycloak.authorization.store.StoreFactorySpi
+org.keycloak.authorization.AuthorizationSpi
+org.keycloak.models.cache.authorization.CachedStoreFactorySpi
+org.keycloak.protocol.oidc.TokenIntrospectionSpi
services/pom.xml 2(+2 -0)
diff --git a/services/pom.xml b/services/pom.xml
index 41ca54d..16b19e7 100755
--- a/services/pom.xml
+++ b/services/pom.xml
@@ -32,6 +32,8 @@
<properties>
<version.swagger.doclet>1.0.5</version.swagger.doclet>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ <maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
diff --git a/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java b/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java
new file mode 100644
index 0000000..0521c1f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/AuthorizationService.java
@@ -0,0 +1,73 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.admin;
+
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.resources.admin.RealmAuth;
+
+import javax.ws.rs.Path;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationService {
+
+ private final RealmAuth auth;
+ private final ClientModel client;
+ private final KeycloakSession session;
+ private final ResourceServer resourceServer;
+ private final AuthorizationProvider authorization;
+
+ public AuthorizationService(KeycloakSession session, ClientModel client, RealmAuth auth) {
+ this.session = session;
+ this.client = client;
+ this.authorization = session.getProvider(AuthorizationProvider.class);
+ this.resourceServer = this.authorization.getStoreFactory().getResourceServerStore().findByClient(this.client.getId());
+ this.auth = auth;
+
+ if (auth != null) {
+ this.auth.init(RealmAuth.Resource.AUTHORIZATION);
+ }
+ }
+
+ @Path("/resource-server")
+ public ResourceServerService resourceServer() {
+ ResourceServerService resource = new ResourceServerService(this.authorization, this.resourceServer, this.client, this.auth);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
+
+ public void enable() {
+ resourceServer().create();
+ }
+
+ public void disable() {
+ resourceServer().delete();
+ }
+
+ public boolean isEnabled() {
+ return this.resourceServer != null;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
new file mode 100644
index 0000000..2847d6d
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyEvaluationService.java
@@ -0,0 +1,205 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.admin;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.representation.PolicyEvaluationRequest;
+import org.keycloak.authorization.admin.representation.PolicyEvaluationResponse;
+import org.keycloak.authorization.attribute.Attributes;
+import org.keycloak.authorization.common.KeycloakEvaluationContext;
+import org.keycloak.authorization.common.KeycloakIdentity;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
+import org.keycloak.authorization.policy.evaluation.EvaluationContext;
+import org.keycloak.authorization.policy.evaluation.Result;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.authorization.util.Permissions;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.services.Urls;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Produces;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyEvaluationService {
+
+ private final AuthorizationProvider authorization;
+ @Context
+ private HttpRequest httpRequest;
+
+ private final ResourceServer resourceServer;
+
+ PolicyEvaluationService(ResourceServer resourceServer, AuthorizationProvider authorization) {
+ this.resourceServer = resourceServer;
+ this.authorization = authorization;
+ }
+
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public void evaluate(PolicyEvaluationRequest evaluationRequest, @Suspended AsyncResponse asyncResponse) {
+ EvaluationContext evaluationContext = createEvaluationContext(evaluationRequest);
+ authorization.evaluators().from(createPermissions(evaluationRequest, evaluationContext, authorization), evaluationContext).evaluate(createDecisionCollector(evaluationRequest, authorization, asyncResponse));
+ }
+
+ private DecisionResultCollector createDecisionCollector(PolicyEvaluationRequest evaluationRequest, AuthorizationProvider authorization, AsyncResponse asyncResponse) {
+ return new DecisionResultCollector() {
+ @Override
+ protected void onComplete(List<Result> results) {
+ try {
+ asyncResponse.resume(Response.ok(PolicyEvaluationResponse.build(evaluationRequest, results, resourceServer, authorization)).build());
+ } catch (Throwable cause) {
+ asyncResponse.resume(cause);
+ }
+ }
+
+ @Override
+ public void onError(Throwable cause) {
+ asyncResponse.resume(cause);
+ }
+ };
+ }
+
+ private EvaluationContext createEvaluationContext(PolicyEvaluationRequest representation) {
+ return new KeycloakEvaluationContext(createIdentity(representation), this.authorization.getKeycloakSession()) {
+ @Override
+ public Attributes getAttributes() {
+ Map<String, Collection<String>> attributes = new HashMap<>(super.getAttributes().toMap());
+ Map<String, String> givenAttributes = representation.getContext().get("attributes");
+
+ if (givenAttributes != null) {
+ givenAttributes.forEach((key, entryValue) -> {
+ if (entryValue != null) {
+ List<String> values = new ArrayList();
+
+ for (String value : entryValue.split(",")) {
+ values.add(value);
+ }
+
+ attributes.put(key, values);
+ }
+ });
+ }
+
+ return Attributes.from(attributes);
+ }
+ };
+ }
+
+ private List<ResourcePermission> createPermissions(PolicyEvaluationRequest representation, EvaluationContext evaluationContext, AuthorizationProvider authorization) {
+ if (representation.isEntitlements()) {
+ return Permissions.all(this.resourceServer, evaluationContext.getIdentity(), authorization);
+ }
+
+ return representation.getResources().stream().flatMap((Function<PolicyEvaluationRequest.Resource, Stream<ResourcePermission>>) resource -> {
+ Set<String> givenScopes = resource.getScopes();
+
+ if (givenScopes == null) {
+ givenScopes = new HashSet();
+ }
+
+ StoreFactory storeFactory = authorization.getStoreFactory();
+
+ List<Scope> scopes = givenScopes.stream().map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, this.resourceServer.getId())).collect(Collectors.toList());
+
+ if (resource.getId() != null) {
+ Resource resourceModel = storeFactory.getResourceStore().findById(resource.getId());
+ return Stream.of(new ResourcePermission(resourceModel, scopes, resourceServer));
+ } else if (resource.getType() != null) {
+ return storeFactory.getResourceStore().findByType(resource.getType()).stream().map(resource1 -> new ResourcePermission(resource1, scopes, resourceServer));
+ } else {
+ return scopes.stream().map(scope -> new ResourcePermission(null, asList(scope), resourceServer));
+ }
+ }).collect(Collectors.toList());
+ }
+
+ private KeycloakIdentity createIdentity(PolicyEvaluationRequest representation) {
+ RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm();
+ AccessToken accessToken = new AccessToken();
+
+ accessToken.subject(representation.getUserId());
+ accessToken.issuedFor(representation.getClientId());
+ accessToken.audience(representation.getClientId());
+ accessToken.issuer(Urls.realmIssuer(this.authorization.getKeycloakSession().getContext().getUri().getBaseUri(), realm.getName()));
+ accessToken.setRealmAccess(new AccessToken.Access());
+
+ Map<String, Object> claims = accessToken.getOtherClaims();
+ Map<String, String> givenAttributes = representation.getContext().get("attributes");
+
+ if (givenAttributes != null) {
+ givenAttributes.forEach((key, value) -> claims.put(key, asList(value)));
+ }
+
+ String subject = accessToken.getSubject();
+
+ if (subject != null) {
+ UserModel userModel = this.authorization.getKeycloakSession().users().getUserById(subject, realm);
+
+ if (userModel != null) {
+ Set<RoleModel> roleMappings = userModel.getRoleMappings();
+
+ roleMappings.stream().map(RoleModel::getName).forEach(roleName -> accessToken.getRealmAccess().addRole(roleName));
+
+ String clientId = representation.getClientId();
+
+ if (clientId != null) {
+ ClientModel clientModel = realm.getClientById(clientId);
+
+ accessToken.addAccess(clientModel.getClientId());
+
+ userModel.getClientRoleMappings(clientModel).stream().map(RoleModel::getName).forEach(roleName -> accessToken.getResourceAccess(clientModel.getClientId()).addRole(roleName));
+
+ //TODO: would be awesome if we could transform the access token using the configured protocol mappers. Tried, but without a clientSession and userSession is tuff.
+ }
+ }
+ }
+
+ if (representation.getRoleIds() != null) {
+ representation.getRoleIds().forEach(roleName -> accessToken.getRealmAccess().addRole(roleName));
+ }
+
+ return new KeycloakIdentity(accessToken, this.authorization.getKeycloakSession());
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
new file mode 100644
index 0000000..cffba23
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java
@@ -0,0 +1,356 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.admin;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.representation.PolicyProviderRepresentation;
+import org.keycloak.authorization.admin.representation.PolicyRepresentation;
+import org.keycloak.authorization.admin.util.Models;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.policy.provider.PolicyProviderAdminService;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.StoreFactory;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.stream.Collectors;
+
+import static org.keycloak.authorization.admin.util.Models.toRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyService {
+
+ private final ResourceServer resourceServer;
+ private final AuthorizationProvider authorization;
+
+ public PolicyService(ResourceServer resourceServer, AuthorizationProvider authorization) {
+ this.resourceServer = resourceServer;
+ this.authorization = authorization;
+ }
+
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response create(PolicyRepresentation representation) {
+ Policy policy = Models.toModel(representation, this.resourceServer, authorization);
+
+ updateResources(policy, authorization);
+ updateAssociatedPolicies(policy);
+ updateScopes(policy, authorization);
+
+ PolicyProviderAdminService resource = getPolicyProviderAdminResource(policy.getType(), authorization);
+
+ if (resource != null) {
+ try {
+ resource.onCreate(policy);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ representation.setId(policy.getId());
+
+ return Response.status(Status.CREATED).entity(representation).build();
+ }
+
+ @Path("{id}")
+ @PUT
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response update(@PathParam("id") String id, PolicyRepresentation representation) {
+ representation.setId(id);
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Policy policy = storeFactory.getPolicyStore().findById(representation.getId());
+
+ if (policy == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ policy.setName(representation.getName());
+ policy.setDescription(representation.getDescription());
+ policy.setConfig(representation.getConfig());
+ policy.setDecisionStrategy(representation.getDecisionStrategy());
+ policy.setLogic(representation.getLogic());
+
+ updateResources(policy, authorization);
+ updateAssociatedPolicies(policy);
+ updateScopes(policy, authorization);
+
+ PolicyProviderAdminService resource = getPolicyProviderAdminResource(policy.getType(), authorization);
+
+ if (resource != null) {
+ try {
+ resource.onUpdate(policy);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return Response.status(Status.CREATED).build();
+ }
+
+ @Path("{id}")
+ @DELETE
+ public Response delete(@PathParam("id") String id) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ Policy policy = policyStore.findById(id);
+
+ if (policy == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ PolicyProviderAdminService resource = getPolicyProviderAdminResource(policy.getType(), authorization);
+
+ if (resource != null) {
+ try {
+ resource.onRemove(policy);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ policyStore.findDependentPolicies(id).forEach(dependentPolicy -> {
+ dependentPolicy.removeAssociatedPolicy(policy);
+ });
+
+ policyStore.delete(policy.getId());
+
+ return Response.noContent().build();
+ }
+
+ @Path("{id}")
+ @GET
+ @Produces("application/json")
+ public Response findById(@PathParam("id") String id) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Policy model = storeFactory.getPolicyStore().findById(id);
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(toRepresentation(model, authorization)).build();
+ }
+
+ @GET
+ @Produces("application/json")
+ public Response findAll() {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ return Response.ok(
+ storeFactory.getPolicyStore().findByResourceServer(resourceServer.getId()).stream()
+ .map(policy -> toRepresentation(policy, authorization))
+ .collect(Collectors.toList()))
+ .build();
+ }
+
+ @Path("providers")
+ @GET
+ @Produces("application/json")
+ public Response findPolicyProviders() {
+ return Response.ok(
+ authorization.getProviderFactories().stream()
+ .map(provider -> {
+ PolicyProviderRepresentation representation = new PolicyProviderRepresentation();
+
+ representation.setName(provider.getName());
+ representation.setGroup(provider.getGroup());
+ representation.setType(provider.getId());
+
+ return representation;
+ })
+ .collect(Collectors.toList()))
+ .build();
+ }
+
+ @Path("evaluate")
+ public PolicyEvaluationService getPolicyEvaluateResource() {
+ PolicyEvaluationService resource = new PolicyEvaluationService(this.resourceServer, this.authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
+
+ @Path("{policyType}")
+ public Object getPolicyTypeResource(@PathParam("policyType") String policyType) {
+ return getPolicyProviderAdminResource(policyType, this.authorization);
+ }
+
+ private PolicyProviderAdminService getPolicyProviderAdminResource(String policyType, AuthorizationProvider authorization) {
+ PolicyProviderFactory providerFactory = authorization.getProviderFactory(policyType);
+
+ if (providerFactory != null) {
+ return providerFactory.getAdminResource(this.resourceServer);
+ }
+
+ return null;
+ }
+
+ private void updateScopes(Policy policy, AuthorizationProvider authorization) {
+ String scopes = policy.getConfig().get("scopes");
+ if (scopes != null) {
+ String[] scopeIds;
+
+ try {
+ scopeIds = new ObjectMapper().readValue(scopes, String[].class);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+
+ for (String scopeId : scopeIds) {
+ boolean hasScope = false;
+
+ for (Scope scopeModel : new HashSet<Scope>(policy.getScopes())) {
+ if (scopeModel.getId().equals(scopeId)) {
+ hasScope = true;
+ }
+ }
+ if (!hasScope) {
+ policy.addScope(storeFactory.getScopeStore().findById(scopeId));
+ }
+ }
+
+ for (Scope scopeModel : new HashSet<Scope>(policy.getScopes())) {
+ boolean hasScope = false;
+
+ for (String scopeId : scopeIds) {
+ if (scopeModel.getId().equals(scopeId)) {
+ hasScope = true;
+ }
+ }
+ if (!hasScope) {
+ policy.removeScope(scopeModel);
+ }
+ }
+ }
+ }
+
+ private void updateAssociatedPolicies(Policy policy) {
+ String policies = policy.getConfig().get("applyPolicies");
+
+ if (policies != null) {
+ String[] policyIds;
+
+ try {
+ policyIds = new ObjectMapper().readValue(policies, String[].class);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+
+ for (String policyId : policyIds) {
+ boolean hasPolicy = false;
+
+ for (Policy policyModel : new HashSet<Policy>(policy.getAssociatedPolicies())) {
+ if (policyModel.getId().equals(policyId)) {
+ hasPolicy = true;
+ }
+ }
+
+
+ if (!hasPolicy) {
+ Policy associatedPolicy = policyStore.findById(policyId);
+
+ if (associatedPolicy == null) {
+ associatedPolicy = policyStore.findByName(policyId, this.resourceServer.getId());
+ }
+
+ policy.addAssociatedPolicy(associatedPolicy);
+ }
+ }
+
+ for (Policy policyModel : new HashSet<Policy>(policy.getAssociatedPolicies())) {
+ boolean hasPolicy = false;
+
+ for (String policyId : policyIds) {
+ if (policyModel.getId().equals(policyId) || policyModel.getName().equals(policyId)) {
+ hasPolicy = true;
+ }
+ }
+ if (!hasPolicy) {
+ policy.removeAssociatedPolicy(policyModel);;
+ }
+ }
+ }
+ }
+
+ private void updateResources(Policy policy, AuthorizationProvider authorization) {
+ String resources = policy.getConfig().get("resources");
+ if (resources != null) {
+ String[] resourceIds;
+
+ try {
+ resourceIds = new ObjectMapper().readValue(resources, String[].class);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ StoreFactory storeFactory = authorization.getStoreFactory();
+
+ for (String resourceId : resourceIds) {
+ boolean hasResource = false;
+ for (Resource resourceModel : new HashSet<Resource>(policy.getResources())) {
+ if (resourceModel.getId().equals(resourceId)) {
+ hasResource = true;
+ }
+ }
+ if (!hasResource && !"".equals(resourceId)) {
+ policy.addResource(storeFactory.getResourceStore().findById(resourceId));
+ }
+ }
+
+ for (Resource resourceModel : new HashSet<Resource>(policy.getResources())) {
+ boolean hasResource = false;
+
+ for (String resourceId : resourceIds) {
+ if (resourceModel.getId().equals(resourceId)) {
+ hasResource = true;
+ }
+ }
+
+ if (!hasResource) {
+ policy.removeResource(resourceModel);
+ }
+ }
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java
new file mode 100644
index 0000000..17edef9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationRequest.java
@@ -0,0 +1,123 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.admin.representation;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyEvaluationRequest {
+
+ private Map<String, Map<String, String>> context;
+ private List<Resource> resources;
+ private String clientId;
+ private String userId;
+ private List<String> roleIds;
+ private boolean entitlements;
+
+ public Map<String, Map<String, String>> getContext() {
+ return this.context;
+ }
+
+ public void setContext(Map<String, Map<String, String>> context) {
+ this.context = context;
+ }
+
+ public List<Resource> getResources() {
+ return this.resources;
+ }
+
+ public void setResources(List<Resource> resources) {
+ this.resources = resources;
+ }
+
+ public String getClientId() {
+ return this.clientId;
+ }
+
+ public void setClientId(final String clientId) {
+ this.clientId = clientId;
+ }
+
+ public String getUserId() {
+ return this.userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public List<String> getRoleIds() {
+ return this.roleIds;
+ }
+
+ public void setRoleIds(List<String> roleIds) {
+ this.roleIds = roleIds;
+ }
+
+ public boolean isEntitlements() {
+ return entitlements;
+ }
+
+ public void setEntitlements(boolean entitlements) {
+ this.entitlements = entitlements;
+ }
+
+ public static class Resource {
+ private String id;
+ private String name;
+ private String type;
+ private Set<String> scopes;
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(final String type) {
+ this.type = type;
+ }
+
+ public Set<String> getScopes() {
+ return scopes;
+ }
+
+ public void setScopes(final Set<String> scopes) {
+ this.scopes = scopes;
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java
new file mode 100644
index 0000000..57b3e4e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponse.java
@@ -0,0 +1,223 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.admin.representation;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.Decision.Effect;
+import org.keycloak.authorization.admin.util.Models;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.policy.evaluation.Result;
+import org.keycloak.authorization.policy.evaluation.Result.PolicyResult;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.authorization.util.Permissions;
+import org.keycloak.representations.authorization.Permission;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyEvaluationResponse {
+
+ private List<EvaluationResultRepresentation> results;
+ private boolean entitlements;
+ private Effect status;
+
+ private PolicyEvaluationResponse() {
+
+ }
+
+ public static PolicyEvaluationResponse build(PolicyEvaluationRequest evaluationRequest, List<Result> results, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ PolicyEvaluationResponse response = new PolicyEvaluationResponse();
+ List<EvaluationResultRepresentation> resultsRep = new ArrayList<>();
+
+ response.entitlements = evaluationRequest.isEntitlements();
+
+ if (response.entitlements) {
+ List<Permission> entitlements = Permissions.allPermits(results);
+
+ if (entitlements.isEmpty()) {
+ response.status = Effect.DENY;
+ } else {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+
+ for (Permission permission : entitlements) {
+ EvaluationResultRepresentation rep = new EvaluationResultRepresentation();
+
+ rep.setStatus(Effect.PERMIT);
+ resultsRep.add(rep);
+
+ Resource resource = storeFactory.getResourceStore().findById(permission.getResourceSetId());
+
+ if (resource != null) {
+ rep.setResource(Models.toRepresentation(resource, resourceServer, authorization));
+ } else {
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setName("Any Resource with Scopes " + permission.getScopes());
+
+ rep.setResource(representation);
+ }
+
+ rep.setScopes(permission.getScopes().stream().map(ScopeRepresentation::new).collect(Collectors.toList()));
+ }
+ }
+ } else {
+ if (results.stream().anyMatch(evaluationResult -> evaluationResult.getEffect().equals(Effect.DENY))) {
+ response.status = Effect.DENY;
+ } else {
+ response.status = Effect.PERMIT;
+ }
+
+ for (Result result : results) {
+ EvaluationResultRepresentation rep = new EvaluationResultRepresentation();
+
+ rep.setStatus(result.getEffect());
+ resultsRep.add(rep);
+
+ if (result.getPermission().getResource() != null) {
+ rep.setResource(Models.toRepresentation(result.getPermission().getResource(), resourceServer, authorization));
+ } else {
+ ResourceRepresentation resource = new ResourceRepresentation();
+
+ resource.setName("Any Resource with Scopes " + result.getPermission().getScopes());
+
+ rep.setResource(resource);
+ }
+
+ rep.setScopes(result.getPermission().getScopes().stream().map(new Function<Scope, ScopeRepresentation>() {
+ @Override
+ public ScopeRepresentation apply(Scope scope) {
+ return Models.toRepresentation(scope, authorization);
+ }
+ }).collect(Collectors.toList()));
+
+ List<PolicyResultRepresentation> policies = new ArrayList<>();
+
+ for (PolicyResult policy : result.getResults()) {
+ policies.add(toRepresentation(policy, authorization));
+ }
+
+ rep.setPolicies(policies);
+ }
+ }
+
+ response.results = resultsRep;
+
+ return response;
+ }
+
+ private static PolicyResultRepresentation toRepresentation(PolicyResult policy, AuthorizationProvider authorization) {
+ PolicyResultRepresentation policyResultRep = new PolicyResultRepresentation();
+
+ policyResultRep.setPolicy(Models.toRepresentation(policy.getPolicy(), authorization));
+ policyResultRep.setStatus(policy.getStatus());
+ policyResultRep.setAssociatedPolicies(policy.getAssociatedPolicies().stream().map(result -> toRepresentation(result, authorization)).collect(Collectors.toList()));
+
+ return policyResultRep;
+ }
+
+ public List<EvaluationResultRepresentation> getResults() {
+ return results;
+ }
+
+ public Effect getStatus() {
+ return status;
+ }
+
+ public boolean isEntitlements() {
+ return entitlements;
+ }
+
+ public static class EvaluationResultRepresentation {
+
+ private ResourceRepresentation resource;
+ private List<ScopeRepresentation> scopes;
+ private List<PolicyResultRepresentation> policies;
+ private Effect status;
+
+ public void setResource(final ResourceRepresentation resource) {
+ this.resource = resource;
+ }
+
+ public ResourceRepresentation getResource() {
+ return resource;
+ }
+
+ public void setScopes(List<ScopeRepresentation> scopes) {
+ this.scopes = scopes;
+ }
+
+ public List<ScopeRepresentation> getScopes() {
+ return scopes;
+ }
+
+ public void setPolicies(final List<PolicyResultRepresentation> policies) {
+ this.policies = policies;
+ }
+
+ public List<PolicyResultRepresentation> getPolicies() {
+ return policies;
+ }
+
+ public void setStatus(final Effect status) {
+ this.status = status;
+ }
+
+ public Effect getStatus() {
+ return status;
+ }
+ }
+
+ public static class PolicyResultRepresentation {
+
+ private PolicyRepresentation policy;
+ private Effect status;
+ private List<PolicyResultRepresentation> associatedPolicies;
+
+ public PolicyRepresentation getPolicy() {
+ return policy;
+ }
+
+ public void setPolicy(final PolicyRepresentation policy) {
+ this.policy = policy;
+ }
+
+ public Effect getStatus() {
+ return status;
+ }
+
+ public void setStatus(final Effect status) {
+ this.status = status;
+ }
+
+ public List<PolicyResultRepresentation> getAssociatedPolicies() {
+ return associatedPolicies;
+ }
+
+ public void setAssociatedPolicies(final List<PolicyResultRepresentation> associatedPolicies) {
+ this.associatedPolicies = associatedPolicies;
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyProviderRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyProviderRepresentation.java
new file mode 100644
index 0000000..add09b0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyProviderRepresentation.java
@@ -0,0 +1,53 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.admin.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyProviderRepresentation {
+
+ private String type;
+ private String name;
+ private String group;
+
+ public String getType() {
+ return this.type;
+ }
+
+ public void setType( String type) {
+ this.type = type;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName( String name) {
+ this.name = name;
+ }
+
+ public String getGroup() {
+ return this.group;
+ }
+
+ public void setGroup( String group) {
+ this.group = group;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyRepresentation.java
new file mode 100644
index 0000000..a3c302b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyRepresentation.java
@@ -0,0 +1,119 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.admin.representation;
+
+import org.keycloak.authorization.model.Policy.DecisionStrategy;
+import org.keycloak.authorization.model.Policy.Logic;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PolicyRepresentation {
+
+ private String id;
+ private String name;
+ private String description;
+ private String type;
+ private Logic logic = Logic.POSITIVE;
+ private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS;
+ private Map<String, String> config = new HashMap();
+ private List<PolicyRepresentation> dependentPolicies;
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public DecisionStrategy getDecisionStrategy() {
+ return this.decisionStrategy;
+ }
+
+ public void setDecisionStrategy(DecisionStrategy decisionStrategy) {
+ this.decisionStrategy = decisionStrategy;
+ }
+
+ public Logic getLogic() {
+ return logic;
+ }
+
+ public void setLogic(Logic logic) {
+ this.logic = logic;
+ }
+
+ public Map<String, String> getConfig() {
+ return this.config;
+ }
+
+ public void setConfig(Map<String, String> config) {
+ this.config = config;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final PolicyRepresentation policy = (PolicyRepresentation) o;
+ return Objects.equals(getId(), policy.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getId());
+ }
+
+ public void setDependentPolicies(List<PolicyRepresentation> dependentPolicies) {
+ this.dependentPolicies = dependentPolicies;
+ }
+
+ public List<PolicyRepresentation> getDependentPolicies() {
+ return this.dependentPolicies;
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceOwnerRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceOwnerRepresentation.java
new file mode 100644
index 0000000..498ab9f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceOwnerRepresentation.java
@@ -0,0 +1,44 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.admin.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceOwnerRepresentation {
+
+ private String id;
+ private String name;
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceRepresentation.java
new file mode 100644
index 0000000..b56248b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceRepresentation.java
@@ -0,0 +1,170 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.admin.representation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * <p>One or more resources that the resource server manages as a set of protected resources.
+ *
+ * <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.2">OAuth-resource-reg</a>.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceRepresentation {
+
+ @JsonProperty("_id")
+ private String id;
+
+ private String name;
+ private String uri;
+ private String type;
+ private Set<ScopeRepresentation> scopes;
+
+ @JsonProperty("icon_uri")
+ private String iconUri;
+ private ResourceOwnerRepresentation owner;
+
+ private List<PolicyRepresentation> policies;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param name a human-readable string describing a set of one or more resources
+ * @param uri a {@link URI} that provides the network location for the resource set being registered
+ * @param type a string uniquely identifying the semantics of the resource set
+ * @param scopes the available scopes for this resource set
+ * @param iconUri a {@link URI} for a graphic icon representing the resource set
+ */
+ public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, String uri, String type, String iconUri) {
+ this.name = name;
+ this.scopes = scopes;
+ this.uri = uri;
+ this.type = type;
+ this.iconUri = iconUri;
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param name a human-readable string describing a set of one or more resources
+ * @param uri a {@link URI} that provides the network location for the resource set being registered
+ * @param type a string uniquely identifying the semantics of the resource set
+ * @param scopes the available scopes for this resource set
+ */
+ public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, String uri, String type) {
+ this(name, scopes, uri, type, null);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param name a human-readable string describing a set of one or more resources
+ * @param serverUri a {@link URI} that identifies this resource server
+ * @param scopes the available scopes for this resource set
+ */
+ public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes) {
+ this(name, scopes, null, null, null);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ */
+ public ResourceRepresentation() {
+ this(null, null, null, null, null);
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getUri() {
+ return this.uri;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public Set<ScopeRepresentation> getScopes() {
+ if (this.scopes == null) {
+ return Collections.emptySet();
+ }
+
+ return Collections.unmodifiableSet(this.scopes);
+ }
+
+ public String getIconUri() {
+ return this.iconUri;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public void setScopes(Set<ScopeRepresentation> scopes) {
+ this.scopes = scopes;
+ }
+
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ public ResourceOwnerRepresentation getOwner() {
+ return this.owner;
+ }
+
+ public void setOwner(ResourceOwnerRepresentation owner) {
+ this.owner = owner;
+ }
+
+ public List<PolicyRepresentation> getPolicies() {
+ return this.policies;
+ }
+
+ public void setPolicies(List<PolicyRepresentation> policies) {
+ this.policies = policies;
+ }
+
+ <T> T test(Predicate<T> t) {
+ return null;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceServerRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceServerRepresentation.java
new file mode 100644
index 0000000..4549ef5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/ResourceServerRepresentation.java
@@ -0,0 +1,104 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.admin.representation;
+
+import org.keycloak.authorization.model.ResourceServer.PolicyEnforcementMode;
+
+import java.util.List;
+
+import static java.util.Collections.emptyList;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceServerRepresentation {
+
+ private String id;
+
+ private String clientId;
+ private String name;
+ private boolean allowRemoteResourceManagement = true;
+ private PolicyEnforcementMode policyEnforcementMode = PolicyEnforcementMode.ENFORCING;
+ private List<ResourceRepresentation> resources = emptyList();
+ private List<PolicyRepresentation> policies = emptyList();
+ private List<ScopeRepresentation> scopes = emptyList();
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isAllowRemoteResourceManagement() {
+ return this.allowRemoteResourceManagement;
+ }
+
+ public void setAllowRemoteResourceManagement(boolean allowRemoteResourceManagement) {
+ this.allowRemoteResourceManagement = allowRemoteResourceManagement;
+ }
+
+ public PolicyEnforcementMode getPolicyEnforcementMode() {
+ return this.policyEnforcementMode;
+ }
+
+ public void setPolicyEnforcementMode(PolicyEnforcementMode policyEnforcementMode) {
+ this.policyEnforcementMode = policyEnforcementMode;
+ }
+
+ public void setResources(List<ResourceRepresentation> resources) {
+ this.resources = resources;
+ }
+
+ public List<ResourceRepresentation> getResources() {
+ return resources;
+ }
+
+ public void setPolicies(List<PolicyRepresentation> policies) {
+ this.policies = policies;
+ }
+
+ public List<PolicyRepresentation> getPolicies() {
+ return policies;
+ }
+
+ public void setScopes(List<ScopeRepresentation> scopes) {
+ this.scopes = scopes;
+ }
+
+ public List<ScopeRepresentation> getScopes() {
+ return scopes;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/ScopeRepresentation.java b/services/src/main/java/org/keycloak/authorization/admin/representation/ScopeRepresentation.java
new file mode 100644
index 0000000..74efa7a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/representation/ScopeRepresentation.java
@@ -0,0 +1,108 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.admin.representation;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * <p>A bounded extent of access that is possible to perform on a resource set. In authorization policy terminology,
+ * a scope is one of the potentially many "verbs" that can logically apply to a resource set ("object").
+ *
+ * <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.1">OAuth-resource-reg</a>.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScopeRepresentation {
+
+ private String id;
+ private String name;
+ private String iconUri;
+ private List<PolicyRepresentation> policies;
+
+ /**
+ * Creates an instance.
+ *
+ * @param name the a human-readable string describing some scope (extent) of access
+ * @param iconUri a {@link URI} for a graphic icon representing the scope
+ */
+ public ScopeRepresentation(String name, String iconUri) {
+ this.name = name;
+ this.iconUri = iconUri;
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param name the a human-readable string describing some scope (extent) of access
+ */
+ public ScopeRepresentation(String name) {
+ this(name, null);
+ }
+
+ /**
+ * Creates an instance.
+ */
+ public ScopeRepresentation() {
+ this(null, null);
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getIconUri() {
+ return this.iconUri;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ public List<PolicyRepresentation> getPolicies() {
+ return this.policies;
+ }
+
+ public void setPolicies(List<PolicyRepresentation> policies) {
+ this.policies = policies;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ScopeRepresentation scope = (ScopeRepresentation) o;
+ return Objects.equals(getName(), scope.getName());
+ }
+
+ public int hashCode() {
+ return Objects.hash(getName());
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
new file mode 100644
index 0000000..23a5d32
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
@@ -0,0 +1,617 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.admin;
+
+import org.jboss.resteasy.plugins.providers.multipart.InputPart;
+import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.representation.PolicyRepresentation;
+import org.keycloak.authorization.admin.representation.ResourceOwnerRepresentation;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.authorization.admin.representation.ResourceServerRepresentation;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.admin.util.Models;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.resources.admin.RealmAuth;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceServerService {
+
+ private final AuthorizationProvider authorization;
+ private final RealmAuth auth;
+ private ResourceServer resourceServer;
+ private final ClientModel client;
+
+ public ResourceServerService(AuthorizationProvider authorization, ResourceServer resourceServer, ClientModel client, RealmAuth auth) {
+ this.authorization = authorization;
+ this.client = client;
+ this.resourceServer = resourceServer;
+ this.auth = auth;
+ }
+
+ public void create() {
+ if (resourceServer == null) {
+ RoleModel umaProtectionRole = client.getRole(Constants.AUTHZ_UMA_PROTECTION);
+
+ if (umaProtectionRole == null) {
+ umaProtectionRole = client.addRole(Constants.AUTHZ_UMA_PROTECTION);
+ }
+
+ KeycloakSession session = this.authorization.getKeycloakSession();
+ UserModel serviceAccount = session.users().getUserByServiceAccountClient(client);
+
+ if (!serviceAccount.hasRole(umaProtectionRole)) {
+ serviceAccount.grantRole(umaProtectionRole);
+ }
+
+ this.resourceServer = this.authorization.getStoreFactory().getResourceServerStore().create(this.client.getId());
+
+ ResourceRepresentation defaultResource = new ResourceRepresentation();
+
+ defaultResource.setName("Default Resource");
+ defaultResource.setUri("/*");
+ defaultResource.setType("urn:" + this.client.getClientId() + ":resources:default");
+
+ getResourceSetResource().create(defaultResource);
+
+ PolicyRepresentation defaultPolicy = new PolicyRepresentation();
+
+ defaultPolicy.setName("Only From Realm Policy");
+ defaultPolicy.setDescription("A policy that grants access only for users within this realm");
+ defaultPolicy.setType("js");
+ defaultPolicy.setDecisionStrategy(Policy.DecisionStrategy.AFFIRMATIVE);
+ defaultPolicy.setLogic(Policy.Logic.POSITIVE);
+
+ HashMap<String, String> defaultPolicyConfig = new HashMap<>();
+
+ defaultPolicyConfig.put("code", "var context = $evaluation.getContext();\n" +
+ "\n" +
+ "// using attributes from the evaluation context to obtain the realm\n" +
+ "var contextAttributes = context.getAttributes();\n" +
+ "var realmName = contextAttributes.getValue('kc.authz.context.authc.realm').asString(0);\n" +
+ "\n" +
+ "// using attributes from the identity to obtain the issuer\n" +
+ "var identity = context.getIdentity();\n" +
+ "var identityAttributes = identity.getAttributes();\n" +
+ "var issuer = identityAttributes.getValue('iss').asString(0);\n" +
+ "\n" +
+ "// only users from the realm have access granted \n" +
+ "if (issuer.endsWith(realmName)) {\n" +
+ " $evaluation.grant();\n" +
+ "}");
+
+ defaultPolicy.setConfig(defaultPolicyConfig);
+
+ getPolicyResource().create(defaultPolicy);
+
+ PolicyRepresentation defaultPermission = new PolicyRepresentation();
+
+ defaultPermission.setName("Default Permission");
+ defaultPermission.setType("resource");
+ defaultPermission.setDescription("A permission that applies to the default resource type");
+ defaultPermission.setDecisionStrategy(Policy.DecisionStrategy.UNANIMOUS);
+ defaultPermission.setLogic(Policy.Logic.POSITIVE);
+
+ HashMap<String, String> defaultPermissionConfig = new HashMap<>();
+
+ defaultPermissionConfig.put("default", "true");
+ defaultPermissionConfig.put("defaultResourceType", defaultResource.getType());
+ defaultPermissionConfig.put("applyPolicies", "[\"Only From Realm Policy\"]");
+
+ defaultPermission.setConfig(defaultPermissionConfig);
+
+ getPolicyResource().create(defaultPermission);
+ }
+ }
+
+ @PUT
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response update(ResourceServerRepresentation server) {
+ this.auth.requireManage();
+
+ this.resourceServer.setAllowRemoteResourceManagement(server.isAllowRemoteResourceManagement());
+ this.resourceServer.setPolicyEnforcementMode(server.getPolicyEnforcementMode());
+
+ return Response.noContent().build();
+ }
+
+ public void delete() {
+ if (this.resourceServer != null) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ ResourceStore resourceStore = storeFactory.getResourceStore();
+ String id = resourceServer.getId();
+
+ resourceStore.findByResourceServer(id).forEach(resource -> resourceStore.delete(resource.getId()));
+
+ ScopeStore scopeStore = storeFactory.getScopeStore();
+
+ scopeStore.findByResourceServer(id).forEach(scope -> scopeStore.delete(scope.getId()));
+
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+
+ policyStore.findByResourceServer(id).forEach(scope -> policyStore.delete(scope.getId()));
+
+ storeFactory.getResourceServerStore().delete(id);
+ }
+ }
+
+ @GET
+ @Produces("application/json")
+ public Response findById() {
+ this.auth.requireView();
+ return Response.ok(Models.toRepresentation(this.resourceServer, getRealm())).build();
+ }
+
+ @Path("/settings")
+ @GET
+ @Produces("application/json")
+ public Response exportSettings() {
+ this.auth.requireManage();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ ResourceServerRepresentation settings = Models.toRepresentation(resourceServer, getRealm());
+
+ settings.setId(null);
+ settings.setName(null);
+ settings.setClientId(null);
+
+ List<ResourceRepresentation> resources = storeFactory.getResourceStore().findByResourceServer(resourceServer.getId())
+ .stream().map(resource -> {
+ ResourceRepresentation rep = Models.toRepresentation(resource, resourceServer, authorization);
+
+ rep.getOwner().setId(null);
+ rep.setId(null);
+ rep.setPolicies(null);
+ rep.getScopes().forEach(scopeRepresentation -> {
+ scopeRepresentation.setId(null);
+ scopeRepresentation.setIconUri(null);
+ });
+
+ return rep;
+ }).collect(Collectors.toList());
+
+ settings.setResources(resources);
+
+ List<PolicyRepresentation> policies = new ArrayList<>();
+
+ policies.addAll(storeFactory.getPolicyStore().findByResourceServer(resourceServer.getId())
+ .stream().filter(policy -> !policy.getType().equals("resource") && !policy.getType().equals("scope"))
+ .map(policy -> createPolicyRepresentation(storeFactory, policy)).collect(Collectors.toList()));
+ policies.addAll(storeFactory.getPolicyStore().findByResourceServer(resourceServer.getId())
+ .stream().filter(policy -> policy.getType().equals("resource") || policy.getType().equals("scope"))
+ .map(policy -> createPolicyRepresentation(storeFactory, policy)).collect(Collectors.toList()));
+
+ settings.setPolicies(policies);
+
+ List<ScopeRepresentation> scopes = storeFactory.getScopeStore().findByResourceServer(resourceServer.getId()).stream().map(scope -> {
+ ScopeRepresentation rep = Models.toRepresentation(scope, authorization);
+
+ rep.setId(null);
+
+ rep.getPolicies().forEach(policyRepresentation -> {
+ policyRepresentation.setId(null);
+ policyRepresentation.setConfig(null);
+ policyRepresentation.setType(null);
+ policyRepresentation.setDecisionStrategy(null);
+ policyRepresentation.setDescription(null);
+ policyRepresentation.setDependentPolicies(null);
+ });
+
+ return rep;
+ }).collect(Collectors.toList());
+
+ settings.setScopes(scopes);
+
+ return Response.ok(settings).build();
+ }
+
+ private PolicyRepresentation createPolicyRepresentation(StoreFactory storeFactory, Policy policy) {
+ PolicyRepresentation rep = Models.toRepresentation(policy, authorization);
+
+ rep.setId(null);
+ rep.setDependentPolicies(null);
+
+ Map<String, String> config = rep.getConfig();
+
+ String roles = config.get("roles");
+
+ if (roles != null && !roles.isEmpty()) {
+ roles = roles.replace("[", "");
+ roles = roles.replace("]", "");
+
+ if (!roles.isEmpty()) {
+ String roleNames = "";
+
+ for (String role : roles.split(",")) {
+ if (!roleNames.isEmpty()) {
+ roleNames = roleNames + ",";
+ }
+
+ role = role.replace("\"", "");
+
+ roleNames = roleNames + "\"" + getRealm().getRoleById(role).getName() + "\"";
+ }
+
+ config.put("roles", "[" + roleNames + "]");
+ }
+ }
+
+ String users = config.get("users");
+
+ if (users != null) {
+ users = users.replace("[", "");
+ users = users.replace("]", "");
+
+ if (!users.isEmpty()) {
+ String userNames = "";
+
+ for (String user : users.split(",")) {
+ if (!userNames.isEmpty()) {
+ userNames = userNames + ",";
+ }
+
+ user = user.replace("\"", "");
+
+ userNames = userNames + "\"" + this.authorization.getKeycloakSession().users().getUserById(user, getRealm()).getUsername() + "\"";
+ }
+
+ config.put("users", "[" + userNames + "]");
+ }
+ }
+
+ String scopes = config.get("scopes");
+
+ if (scopes != null && !scopes.isEmpty()) {
+ scopes = scopes.replace("[", "");
+ scopes = scopes.replace("]", "");
+
+ if (!scopes.isEmpty()) {
+ String scopeNames = "";
+
+ for (String scope : scopes.split(",")) {
+ if (!scopeNames.isEmpty()) {
+ scopeNames = scopeNames + ",";
+ }
+
+ scope = scope.replace("\"", "");
+
+ scopeNames = scopeNames + "\"" + storeFactory.getScopeStore().findById(scope).getName() + "\"";
+ }
+
+ config.put("scopes", "[" + scopeNames + "]");
+ }
+ }
+
+ String policyResources = config.get("resources");
+
+ if (policyResources != null && !policyResources.isEmpty()) {
+ policyResources = policyResources.replace("[", "");
+ policyResources = policyResources.replace("]", "");
+
+ if (!policyResources.isEmpty()) {
+ String resourceNames = "";
+
+ for (String resource : policyResources.split(",")) {
+ if (!resourceNames.isEmpty()) {
+ resourceNames = resourceNames + ",";
+ }
+
+ resource = resource.replace("\"", "");
+
+ resourceNames = resourceNames + "\"" + storeFactory.getResourceStore().findById(resource).getName() + "\"";
+ }
+
+ config.put("resources", "[" + resourceNames + "]");
+ }
+ }
+
+ String policyNames = "";
+ Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
+
+ if (!associatedPolicies.isEmpty()) {
+ for (Policy associatedPolicy : associatedPolicies) {
+ if (!policyNames.isEmpty()) {
+ policyNames = policyNames + ",";
+ }
+
+ policyNames = policyNames + "\"" + associatedPolicy.getName() + "\"";
+ }
+
+ config.put("applyPolicies", "[" + policyNames + "]");
+ }
+
+ return rep;
+ }
+
+ @POST
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ public Response importSettings(@Context final UriInfo uriInfo, MultipartFormDataInput input) throws IOException {
+ Map<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());
+
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ ResourceStore resourceStore = storeFactory.getResourceStore();
+ ScopeStore scopeStore = storeFactory.getScopeStore();
+ ScopeService scopeResource = new ScopeService(resourceServer, this.authorization, this.auth);
+
+ ResteasyProviderFactory.getInstance().injectProperties(scopeResource);
+
+ rep.getScopes().forEach(scope -> {
+ Scope existing = scopeStore.findByName(scope.getName(), resourceServer.getId());
+
+ if (existing != null) {
+ scopeResource.update(existing.getId(), scope);
+ } else {
+ scopeResource.create(scope);
+ }
+ });
+
+ ResourceSetService resourceSetResource = new ResourceSetService(resourceServer, this.authorization, this.auth);
+
+ rep.getResources().forEach(resourceRepresentation -> {
+ ResourceOwnerRepresentation owner = resourceRepresentation.getOwner();
+
+ if (owner == null) {
+ owner = new ResourceOwnerRepresentation();
+ }
+
+ owner.setId(resourceServer.getClientId());
+
+ if (owner.getName() != null) {
+ UserModel user = this.authorization.getKeycloakSession().users().getUserByUsername(owner.getName(), getRealm());
+
+ if (user != null) {
+ owner.setId(user.getId());
+ }
+ }
+
+ Resource existing = resourceStore.findByName(resourceRepresentation.getName(), this.resourceServer.getId());
+
+ if (existing != null) {
+ resourceSetResource.update(existing.getId(), resourceRepresentation);
+ } else {
+ resourceSetResource.create(resourceRepresentation);
+ }
+ });
+
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ PolicyService policyResource = new PolicyService(resourceServer, this.authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(policyResource);
+
+ rep.getPolicies().forEach(policyRepresentation -> {
+ Map<String, String> config = policyRepresentation.getConfig();
+
+ String roles = config.get("roles");
+
+ if (roles != null && !roles.isEmpty()) {
+ roles = roles.replace("[", "");
+ roles = roles.replace("]", "");
+
+ if (!roles.isEmpty()) {
+ String roleNames = "";
+
+ for (String role : roles.split(",")) {
+ if (!roleNames.isEmpty()) {
+ roleNames = roleNames + ",";
+ }
+
+ role = role.replace("\"", "");
+
+ roleNames = roleNames + "\"" + getRealm().getRole(role).getId() + "\"";
+ }
+
+ config.put("roles", "[" + roleNames + "]");
+ }
+ }
+
+ String users = config.get("users");
+
+ if (users != null) {
+ users = users.replace("[", "");
+ users = users.replace("]", "");
+
+ if (!users.isEmpty()) {
+ String userNames = "";
+
+ for (String user : users.split(",")) {
+ if (!userNames.isEmpty()) {
+ userNames = userNames + ",";
+ }
+
+ user = user.replace("\"", "");
+
+ userNames = userNames + "\"" + this.authorization.getKeycloakSession().users().getUserByUsername(user, getRealm()).getId() + "\"";
+ }
+
+ config.put("users", "[" + userNames + "]");
+ }
+ }
+
+ String scopes = config.get("scopes");
+
+ if (scopes != null && !scopes.isEmpty()) {
+ scopes = scopes.replace("[", "");
+ scopes = scopes.replace("]", "");
+
+ if (!scopes.isEmpty()) {
+ String scopeNames = "";
+
+ for (String scope : scopes.split(",")) {
+ if (!scopeNames.isEmpty()) {
+ scopeNames = scopeNames + ",";
+ }
+
+ scope = scope.replace("\"", "");
+
+ Scope newScope = scopeStore.findByName(scope, resourceServer.getId());
+
+ if (newScope == null) {
+ throw new RuntimeException("Scope with name [" + scope + "] not defined.");
+ }
+
+ scopeNames = scopeNames + "\"" + newScope.getId() + "\"";
+ }
+
+ config.put("scopes", "[" + scopeNames + "]");
+ }
+ }
+
+ String policyResources = config.get("resources");
+
+ if (policyResources != null && !policyResources.isEmpty()) {
+ policyResources = policyResources.replace("[", "");
+ policyResources = policyResources.replace("]", "");
+
+ if (!policyResources.isEmpty()) {
+ String resourceNames = "";
+
+ for (String resource : policyResources.split(",")) {
+ if (!resourceNames.isEmpty()) {
+ resourceNames = resourceNames + ",";
+ }
+
+ resource = resource.replace("\"", "");
+
+ if ("".equals(resource)) {
+ continue;
+ }
+
+ resourceNames = resourceNames + "\"" + storeFactory.getResourceStore().findByName(resource, resourceServer.getId()).getId() + "\"";
+ }
+
+ config.put("resources", "[" + resourceNames + "]");
+ }
+ }
+
+ String applyPolicies = config.get("applyPolicies");
+
+ if (applyPolicies != null && !applyPolicies.isEmpty()) {
+ applyPolicies = applyPolicies.replace("[", "");
+ applyPolicies = applyPolicies.replace("]", "");
+
+ if (!applyPolicies.isEmpty()) {
+ String policyNames = "";
+
+ for (String pId : applyPolicies.split(",")) {
+ if (!policyNames.isEmpty()) {
+ policyNames = policyNames + ",";
+ }
+
+ pId = pId.replace("\"", "").trim();
+
+ Policy policy = policyStore.findByName(pId, resourceServer.getId());
+
+ if (policy == null) {
+ throw new RuntimeException("Policy with name [" + pId + "] not defined.");
+ }
+
+ policyNames = policyNames + "\"" + policy.getId() + "\"";
+ }
+
+ config.put("applyPolicies", "[" + policyNames + "]");
+ }
+ }
+
+ Policy existing = policyStore.findByName(policyRepresentation.getName(), this.resourceServer.getId());
+
+ if (existing != null) {
+ policyResource.update(existing.getId(), policyRepresentation);
+ } else {
+ policyResource.create(policyRepresentation);
+ }
+ });
+ }
+
+ return Response.noContent().build();
+ }
+
+ @Path("/resource")
+ public ResourceSetService getResourceSetResource() {
+ ResourceSetService resource = new ResourceSetService(this.resourceServer, this.authorization, this.auth);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
+
+ @Path("/scope")
+ public ScopeService getScopeResource() {
+ ScopeService resource = new ScopeService(this.resourceServer, this.authorization, this.auth);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
+
+ @Path("/policy")
+ public PolicyService getPolicyResource() {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ PolicyService resource = new PolicyService(this.resourceServer, this.authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
+
+ private RealmModel getRealm() {
+ return this.authorization.getKeycloakSession().getContext().getRealm();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
new file mode 100644
index 0000000..9177479
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -0,0 +1,176 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.admin;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.admin.util.Models;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.resources.admin.RealmAuth;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceSetService {
+
+ private final AuthorizationProvider authorization;
+ private final RealmAuth auth;
+ private ResourceServer resourceServer;
+
+ public ResourceSetService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) {
+ this.resourceServer = resourceServer;
+ this.authorization = authorization;
+ this.auth = auth;
+ }
+
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response create(ResourceRepresentation resource) {
+ requireManage();
+ StoreFactory storeFactory = this.authorization.getStoreFactory();
+ Resource existingResource = storeFactory.getResourceStore().findByName(resource.getName(), this.resourceServer.getId());
+
+ if (existingResource != null && existingResource.getResourceServer().getId().equals(this.resourceServer.getId())
+ && existingResource.getOwner().equals(resource.getOwner())) {
+ return ErrorResponse.exists("Resource with name [" + resource.getName() + "] already exists.");
+ }
+
+ Resource model = Models.toModel(resource, this.resourceServer, authorization);
+
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setId(model.getId());
+
+ return Response.status(Status.CREATED).entity(representation).build();
+ }
+
+ @Path("{id}")
+ @PUT
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response update(@PathParam("id") String id, ResourceRepresentation resource) {
+ requireManage();
+ resource.setId(id);
+ StoreFactory storeFactory = this.authorization.getStoreFactory();
+ ResourceStore resourceStore = storeFactory.getResourceStore();
+ Resource model = resourceStore.findById(resource.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ model.setName(resource.getName());
+ model.setType(resource.getType());
+ model.setUri(resource.getUri());
+ model.setIconUri(resource.getIconUri());
+
+ model.updateScopes(resource.getScopes().stream()
+ .map((ScopeRepresentation scope) -> Models.toModel(scope, this.resourceServer, authorization))
+ .collect(Collectors.toSet()));
+
+ return Response.noContent().build();
+ }
+
+ @Path("{id}")
+ @DELETE
+ public Response delete(@PathParam("id") String id) {
+ requireManage();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Resource resource = storeFactory.getResourceStore().findById(id);
+
+ if (resource == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ List<Policy> policies = policyStore.findByResource(id);
+
+ for (Policy policyModel : policies) {
+ if (policyModel.getResources().size() == 1) {
+ policyStore.delete(policyModel.getId());
+ } else {
+ policyModel.addResource(resource);
+ }
+ }
+
+ storeFactory.getResourceStore().delete(id);
+
+ return Response.noContent().build();
+ }
+
+ @Path("{id}")
+ @GET
+ @Produces("application/json")
+ public Response findById(@PathParam("id") String id) {
+ requireView();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Resource model = storeFactory.getResourceStore().findById(id);
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(Models.toRepresentation(model, this.resourceServer, authorization)).build();
+ }
+
+ @GET
+ @Produces("application/json")
+ public Response findAll() {
+ requireView();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+
+ return Response.ok(
+ storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream()
+ .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization))
+ .collect(Collectors.toList()))
+ .build();
+ }
+
+ private void requireManage() {
+ if (this.auth != null) {
+ this.auth.requireManage();
+ }
+ }
+
+ private void requireView() {
+ if (this.auth != null) {
+ this.auth.requireView();
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
new file mode 100644
index 0000000..56291c8
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java
@@ -0,0 +1,147 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.admin;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.services.ErrorResponse;
+import org.keycloak.services.resources.admin.RealmAuth;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.keycloak.authorization.admin.util.Models.toModel;
+import static org.keycloak.authorization.admin.util.Models.toRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScopeService {
+
+ private final AuthorizationProvider authorization;
+ private final RealmAuth auth;
+ private ResourceServer resourceServer;
+
+ public ScopeService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) {
+ this.resourceServer = resourceServer;
+ this.authorization = authorization;
+ this.auth = auth;
+ }
+
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response create(ScopeRepresentation scope) {
+ this.auth.requireManage();
+ Scope model = toModel(scope, this.resourceServer, authorization);
+
+ scope.setId(model.getId());
+
+ return Response.status(Status.CREATED).entity(scope).build();
+ }
+
+ @Path("{id}")
+ @PUT
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response update(@PathParam("id") String id, ScopeRepresentation scope) {
+ this.auth.requireManage();
+ scope.setId(id);
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Scope model = storeFactory.getScopeStore().findById(scope.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ model.setName(scope.getName());
+ model.setIconUri(scope.getIconUri());
+
+ return Response.noContent().build();
+ }
+
+ @Path("{id}")
+ @DELETE
+ public Response delete(@PathParam("id") String id) {
+ this.auth.requireManage();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ List<Resource> resources = storeFactory.getResourceStore().findByScope(id);
+
+ if (!resources.isEmpty()) {
+ return ErrorResponse.exists("Scopes can not be removed while associated with resources.");
+ }
+
+ Scope scope = storeFactory.getScopeStore().findById(id);
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ List<Policy> policies = policyStore.findByScopeIds(Arrays.asList(scope.getId()), resourceServer.getId());
+
+ for (Policy policyModel : policies) {
+ if (policyModel.getScopes().size() == 1) {
+ policyStore.delete(policyModel.getId());
+ } else {
+ policyModel.removeScope(scope);
+ }
+ }
+
+ storeFactory.getScopeStore().delete(id);
+
+ return Response.noContent().build();
+ }
+
+ @Path("{id}")
+ @GET
+ @Produces("application/json")
+ public Response findById(@PathParam("id") String id) {
+ this.auth.requireView();
+ Scope model = this.authorization.getStoreFactory().getScopeStore().findById(id);
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(toRepresentation(model, this.authorization)).build();
+ }
+
+ @GET
+ @Produces("application/json")
+ public Response findAll() {
+ this.auth.requireView();
+ return Response.ok(
+ this.authorization.getStoreFactory().getScopeStore().findByResourceServer(this.resourceServer.getId()).stream()
+ .map(scope -> toRepresentation(scope, this.authorization))
+ .collect(Collectors.toList()))
+ .build();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/admin/util/Models.java b/services/src/main/java/org/keycloak/authorization/admin/util/Models.java
new file mode 100644
index 0000000..abdd980
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/admin/util/Models.java
@@ -0,0 +1,285 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.admin.util;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.ErrorCode;
+import org.keycloak.authorization.admin.representation.PolicyRepresentation;
+import org.keycloak.authorization.admin.representation.ResourceOwnerRepresentation;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.authorization.admin.representation.ResourceServerRepresentation;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.core.Response.Status;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Some utility methods to transform models to representations and vice-versa.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public final class Models {
+
+ public static ScopeRepresentation toRepresentation(Scope model, AuthorizationProvider authorizationProvider) {
+ ScopeRepresentation scope = new ScopeRepresentation();
+
+ scope.setId(model.getId());
+ scope.setName(model.getName());
+ scope.setIconUri(model.getIconUri());
+ scope.setPolicies(new ArrayList<>());
+
+ Set<Policy> policies = new HashSet<>();
+
+ policies.addAll(authorizationProvider.getStoreFactory().getPolicyStore().findByScopeIds(Arrays.asList(model.getId()), model.getResourceServer().getId()));
+
+ for (Policy policyModel : policies) {
+ PolicyRepresentation policy = new PolicyRepresentation();
+
+ policy.setId(policyModel.getId());
+ policy.setName(policyModel.getName());
+ policy.setType(policyModel.getType());
+
+ if (!scope.getPolicies().contains(policy)) {
+ scope.getPolicies().add(policy);
+ }
+ }
+
+ return scope;
+ }
+
+ public static Scope toModel(ScopeRepresentation scope, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Scope model = storeFactory.getScopeStore().findByName(scope.getName(), resourceServer.getId());
+
+ if (model == null) {
+ model = storeFactory.getScopeStore().create(scope.getName(), resourceServer);
+
+ model.setIconUri(scope.getIconUri());
+ }
+
+ return model;
+ }
+
+ public static ResourceServerRepresentation toRepresentation(ResourceServer model, RealmModel realm) {
+ ResourceServerRepresentation server = new ResourceServerRepresentation();
+
+ server.setId(model.getId());
+ server.setClientId(model.getClientId());
+ ClientModel clientById = realm.getClientById(model.getClientId());
+ server.setName(clientById.getClientId());
+ server.setAllowRemoteResourceManagement(model.isAllowRemoteResourceManagement());
+ server.setPolicyEnforcementMode(model.getPolicyEnforcementMode());
+
+ return server;
+ }
+
+ public static ResourceServer toModel(ResourceServerRepresentation server, AuthorizationProvider authorization) {
+ RealmModel realm = authorization.getKeycloakSession().getContext().getRealm();
+ ClientModel client = realm.getClientById(server.getClientId());
+
+ if (client == null) {
+ throw new ErrorResponseException(ErrorCode.INVALID_CLIENT_ID, "Client with id [" + server.getClientId() + "] not found in realm [" + realm.getName() + "].", Status.BAD_REQUEST);
+ }
+
+ if (!client.isServiceAccountsEnabled()) {
+ throw new ErrorResponseException(ErrorCode.INVALID_CLIENT_ID, "Client with id [" + server.getClientId() + "] must have a service account.", Status.BAD_REQUEST);
+ }
+
+ ResourceServer existingResourceServer = authorization.getStoreFactory().getResourceServerStore().findByClient(client.getId());
+
+ if (existingResourceServer != null) {
+ throw new ErrorResponseException(ErrorCode.INVALID_CLIENT_ID, "Resource server already exists with client id [" + server.getClientId() + "].", Status.BAD_REQUEST);
+ }
+
+ if (server.getName() == null) {
+ server.setName(client.getName());
+ }
+
+ ResourceServer model = authorization.getStoreFactory().getResourceServerStore().create(client.getId());
+
+ model.setAllowRemoteResourceManagement(server.isAllowRemoteResourceManagement());
+ model.setPolicyEnforcementMode(server.getPolicyEnforcementMode());
+
+ return model;
+ }
+
+ public static PolicyRepresentation toRepresentation(Policy model, AuthorizationProvider authorization) {
+ PolicyRepresentation representation = new PolicyRepresentation();
+
+ representation.setId(model.getId());
+ representation.setName(model.getName());
+ representation.setDescription(model.getDescription());
+ representation.setType(model.getType());
+ representation.setDecisionStrategy(model.getDecisionStrategy());
+ representation.setLogic(model.getLogic());
+ representation.setConfig(new HashMap<>(model.getConfig()));
+
+ List<Policy> policies = authorization.getStoreFactory().getPolicyStore().findDependentPolicies(model.getId());
+
+ representation.setDependentPolicies(policies.stream().map(policy -> {
+ PolicyRepresentation representation1 = new PolicyRepresentation();
+
+ representation1.setId(policy.getId());
+ representation1.setName(policy.getName());
+
+ return representation1;
+ }).collect(Collectors.toList()));
+
+ List<String> obj = model.getAssociatedPolicies().stream().map(new Function<Policy, String>() {
+ @Override
+ public String apply(Policy policy) {
+ return policy.getId();
+ }
+ }).collect(Collectors.toList());
+
+ try {
+ representation.getConfig().put("applyPolicies", JsonSerialization.writeValueAsString(obj));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return representation;
+ }
+
+ public static Policy toModel(PolicyRepresentation policy, ResourceServer resourceServer, AuthorizationProvider authorizationManager) {
+ Policy model = authorizationManager.getStoreFactory().getPolicyStore().create(policy.getName(), policy.getType(), resourceServer);
+
+ model.setDescription(policy.getDescription());
+ model.setDecisionStrategy(policy.getDecisionStrategy());
+ model.setLogic(policy.getLogic());
+ model.setConfig(policy.getConfig());
+
+ return model;
+ }
+
+ public static ResourceRepresentation toRepresentation(Resource model, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ ResourceRepresentation resource = new ResourceRepresentation();
+
+ resource.setId(model.getId());
+ resource.setType(model.getType());
+ resource.setName(model.getName());
+ resource.setUri(model.getUri());
+ resource.setIconUri(model.getIconUri());
+
+ ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation();
+
+ owner.setId(model.getOwner());
+
+ KeycloakSession keycloakSession = authorization.getKeycloakSession();
+ RealmModel realm = keycloakSession.getContext().getRealm();
+
+ if (owner.getId().equals(resourceServer.getClientId())) {
+ ClientModel clientModel = realm.getClientById(resourceServer.getClientId());
+ owner.setName(clientModel.getClientId());
+ } else {
+ UserModel userModel = keycloakSession.users().getUserById(owner.getId(), realm);
+
+ if (userModel == null) {
+ throw new ErrorResponseException("invalid_owner", "Could not find the user [" + owner.getId() + "] who owns the Resource [" + resource.getId() + "].", Status.BAD_REQUEST);
+ }
+
+ owner.setName(userModel.getUsername());
+ }
+
+ resource.setOwner(owner);
+
+ resource.setScopes(model.getScopes().stream().map(model1 -> {
+ ScopeRepresentation scope = new ScopeRepresentation();
+ scope.setId(model1.getId());
+ scope.setName(model1.getName());
+ String iconUri = model1.getIconUri();
+ if (iconUri != null) {
+ scope.setIconUri(iconUri);
+ }
+ return scope;
+ }).collect(Collectors.toSet()));
+
+ resource.setPolicies(new ArrayList<>());
+
+ Set<Policy> policies = new HashSet<>();
+ PolicyStore policyStore = authorization.getStoreFactory().getPolicyStore();
+
+ policies.addAll(policyStore.findByResource(resource.getId()));
+ policies.addAll(policyStore.findByResourceType(resource.getType(), resourceServer.getId()));
+ policies.addAll(policyStore.findByScopeIds(resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toList()), resourceServer.getId()));
+
+ for (Policy policyModel : policies) {
+ PolicyRepresentation policy = new PolicyRepresentation();
+
+ policy.setId(policyModel.getId());
+ policy.setName(policyModel.getName());
+ policy.setType(policyModel.getType());
+
+ if (!resource.getPolicies().contains(policy)) {
+ resource.getPolicies().add(policy);
+ }
+ }
+
+ return resource;
+ }
+
+ public static Resource toModel(ResourceRepresentation resource, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ ResourceOwnerRepresentation owner = resource.getOwner();
+
+ if (owner == null) {
+ owner = new ResourceOwnerRepresentation();
+ owner.setId(resourceServer.getClientId());
+ }
+
+ if (owner.getId() == null) {
+ throw new ErrorResponseException("invalid_owner", "No owner specified for resource [" + resource.getName() + "].", Status.BAD_REQUEST);
+ }
+
+ ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
+ Resource model = resourceStore.create(resource.getName(), resourceServer, owner.getId());
+
+ model.setType(resource.getType());
+ model.setUri(resource.getUri());
+ model.setIconUri(resource.getIconUri());
+
+ Set<ScopeRepresentation> scopes = resource.getScopes();
+
+ if (scopes != null) {
+ model.updateScopes(scopes.stream().map((Function<ScopeRepresentation, Scope>) scope -> toModel(scope, resourceServer, authorization)).collect(Collectors.toSet()));
+ }
+
+ return model;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
new file mode 100644
index 0000000..eaa8c78
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
@@ -0,0 +1,236 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.authorization;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.OAuthErrorException;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.authorization.representation.AuthorizationRequest;
+import org.keycloak.authorization.authorization.representation.AuthorizationResponse;
+import org.keycloak.authorization.common.KeycloakEvaluationContext;
+import org.keycloak.authorization.common.KeycloakIdentity;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
+import org.keycloak.authorization.policy.evaluation.Result;
+import org.keycloak.authorization.protection.permission.PermissionTicket;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.authorization.util.Permissions;
+import org.keycloak.authorization.util.Tokens;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.authorization.Permission;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.resources.Cors;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.Produces;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationTokenService {
+
+ private final AuthorizationProvider authorization;
+
+ @Context
+ private HttpRequest httpRequest;
+
+ public AuthorizationTokenService(AuthorizationProvider authorization) {
+ this.authorization = authorization;
+ }
+
+ @OPTIONS
+ public Response authorizepPreFlight() {
+ return Cors.add(this.httpRequest, Response.ok()).auth().preflight().build();
+ }
+
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public void authorize(AuthorizationRequest authorizationRequest, @Suspended AsyncResponse asyncResponse) {
+ KeycloakEvaluationContext evaluationContext = new KeycloakEvaluationContext(this.authorization.getKeycloakSession());
+ KeycloakIdentity identity = (KeycloakIdentity) evaluationContext.getIdentity();
+
+ if (!identity.hasRole("uma_authorization")) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_authorization scope.", Status.FORBIDDEN);
+ }
+
+ if (authorizationRequest == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid authorization request.", Status.BAD_REQUEST);
+ }
+
+ PermissionTicket ticket = verifyPermissionTicket(authorizationRequest);
+
+ authorization.evaluators().from(createPermissions(ticket, authorizationRequest, authorization), evaluationContext).evaluate(new DecisionResultCollector() {
+ @Override
+ public void onComplete(List<Result> results) {
+ List<Permission> entitlements = Permissions.allPermits(results);
+
+ if (entitlements.isEmpty()) {
+ asyncResponse.resume(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN));
+ } else {
+ AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(entitlements, identity.getAccessToken()));
+ asyncResponse.resume(Cors.add(httpRequest, Response.status(Status.CREATED).entity(response)).allowedOrigins(identity.getAccessToken())
+ .allowedMethods("POST")
+ .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
+ }
+ }
+
+ @Override
+ public void onError(Throwable cause) {
+ asyncResponse.resume(cause);
+ }
+ });
+ }
+
+ private List<ResourcePermission> createPermissions(PermissionTicket ticket, AuthorizationRequest request, AuthorizationProvider authorization) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Map<String, Set<String>> permissionsToEvaluate = new HashMap<>();
+
+ ticket.getResources().forEach(requestedResource -> {
+ Resource resource;
+
+ if (requestedResource.getId() != null) {
+ resource = storeFactory.getResourceStore().findById(requestedResource.getId());
+ } else {
+ resource = storeFactory.getResourceStore().findByName(requestedResource.getName(), ticket.getResourceServerId());
+ }
+
+ if (resource == null) {
+ throw new ErrorResponseException("invalid_resource", "Resource with id [" + requestedResource.getId() + "] or name [" + requestedResource.getName() + "] does not exist.", Status.FORBIDDEN);
+ }
+
+ Set<ScopeRepresentation> requestedScopes = requestedResource.getScopes();
+
+ permissionsToEvaluate.put(resource.getId(), requestedScopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet()));
+ });
+
+ String rpt = request.getRpt();
+
+ if (rpt != null && !"".equals(rpt)) {
+ if (!Tokens.verifySignature(rpt, getRealm().getPublicKey())) {
+ throw new ErrorResponseException("invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN);
+ }
+
+ AccessToken requestingPartyToken;
+
+ try {
+ requestingPartyToken = new JWSInput(rpt).readJsonContent(AccessToken.class);
+ } catch (JWSInputException e) {
+ throw new ErrorResponseException("invalid_rpt", "Invalid RPT", Status.FORBIDDEN);
+ }
+
+ if (requestingPartyToken.isActive()) {
+ AccessToken.Authorization authorizationData = requestingPartyToken.getAuthorization();
+
+ if (authorizationData != null) {
+ List<Permission> permissions = authorizationData.getPermissions();
+
+ if (permissions != null) {
+ permissions.forEach(permission -> {
+ Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId());
+
+ if (resourcePermission != null) {
+ Set<String> scopes = permissionsToEvaluate.get(resourcePermission.getId());
+
+ if (scopes == null) {
+ scopes = new HashSet<>();
+ permissionsToEvaluate.put(resourcePermission.getId(), scopes);
+ }
+
+ Set<String> scopePermission = permission.getScopes();
+
+ if (scopePermission != null) {
+ scopes.addAll(scopePermission);
+ }
+ }
+ });
+ }
+ }
+ }
+ }
+
+ return permissionsToEvaluate.entrySet().stream()
+ .flatMap((Function<Entry<String, Set<String>>, Stream<ResourcePermission>>) entry -> {
+ Resource entryResource = storeFactory.getResourceStore().findById(entry.getKey());
+ if (entry.getValue().isEmpty()) {
+ return Arrays.asList(new ResourcePermission(entryResource, Collections.emptyList(), entryResource.getResourceServer())).stream();
+ } else {
+ return entry.getValue().stream()
+ .map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, entryResource.getResourceServer().getId()))
+ .filter(scope -> scope != null)
+ .map(scope -> new ResourcePermission(entryResource, Arrays.asList(scope), entryResource.getResourceServer()));
+ }
+ }).collect(Collectors.toList());
+ }
+
+ private RealmModel getRealm() {
+ return this.authorization.getKeycloakSession().getContext().getRealm();
+ }
+
+ private String createRequestingPartyToken(List<Permission> permissions, AccessToken accessToken) {
+ AccessToken.Authorization authorization = new AccessToken.Authorization();
+
+ authorization.setPermissions(permissions);
+ accessToken.setAuthorization(authorization);
+
+ return new TokenManager().encodeToken(getRealm(), accessToken);
+ }
+
+ private PermissionTicket verifyPermissionTicket(AuthorizationRequest request) {
+ if (!Tokens.verifySignature(request.getTicket(), getRealm().getPublicKey())) {
+ throw new ErrorResponseException("invalid_ticket", "Ticket verification failed", Status.FORBIDDEN);
+ }
+
+ try {
+ PermissionTicket ticket = new JWSInput(request.getTicket()).readJsonContent(PermissionTicket.class);
+
+ if (!ticket.isActive()) {
+ throw new ErrorResponseException("invalid_ticket", "Invalid permission ticket.", Status.FORBIDDEN);
+ }
+
+ return ticket;
+ } catch (JWSInputException e) {
+ throw new ErrorResponseException("invalid_ticket", "Could not parse permission ticket.", Status.FORBIDDEN);
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java
new file mode 100644
index 0000000..d4f0f24
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java
@@ -0,0 +1,49 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.authorization.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationRequest {
+
+ private String ticket;
+ private String rpt;
+
+ public AuthorizationRequest(String ticket, String rpt) {
+ this.ticket = ticket;
+ this.rpt = rpt;
+ }
+
+ public AuthorizationRequest(String ticket) {
+ this(ticket, null);
+ }
+
+ public AuthorizationRequest() {
+ this(null, null);
+ }
+
+ public String getTicket() {
+ return this.ticket;
+ }
+
+ public String getRpt() {
+ return this.rpt;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java
new file mode 100644
index 0000000..cd0a521
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationResponse.java
@@ -0,0 +1,43 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.authorization.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationResponse {
+
+ private String rpt;
+
+ public AuthorizationResponse(String rpt) {
+ this.rpt = rpt;
+ }
+
+ public AuthorizationResponse() {
+ this(null);
+ }
+
+ public String getRpt() {
+ return this.rpt;
+ }
+
+ public void setRpt(final String rpt) {
+ this.rpt = rpt;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/AuthorizationService.java b/services/src/main/java/org/keycloak/authorization/AuthorizationService.java
new file mode 100644
index 0000000..f519b40
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/AuthorizationService.java
@@ -0,0 +1,65 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization;
+
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.authorization.authorization.AuthorizationTokenService;
+import org.keycloak.authorization.entitlement.EntitlementService;
+import org.keycloak.authorization.protection.ProtectionService;
+
+import javax.ws.rs.Path;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AuthorizationService {
+
+ private final AuthorizationProvider authorization;
+
+ public AuthorizationService(AuthorizationProvider authorization) {
+ this.authorization = authorization;
+ }
+
+ @Path("/entitlement")
+ public Object getEntitlementService() {
+ EntitlementService service = new EntitlementService(this.authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(service);
+
+ return service;
+ }
+
+ @Path("/protection")
+ public Object getProtectionService() {
+ ProtectionService service = new ProtectionService(this.authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(service);
+
+ return service;
+ }
+
+ @Path("/authorize")
+ public Object authorize() {
+ AuthorizationTokenService resource = new AuthorizationTokenService(this.authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
new file mode 100644
index 0000000..bc967b9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakEvaluationContext.java
@@ -0,0 +1,80 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.common;
+
+import org.keycloak.authorization.attribute.Attributes;
+import org.keycloak.authorization.identity.Identity;
+import org.keycloak.authorization.policy.evaluation.EvaluationContext;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.representations.AccessToken;
+
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class KeycloakEvaluationContext implements EvaluationContext {
+
+ private final KeycloakIdentity identity;
+ private final KeycloakSession keycloakSession;
+
+ public KeycloakEvaluationContext(KeycloakSession keycloakSession) {
+ this(new KeycloakIdentity(keycloakSession), keycloakSession);
+ }
+
+ public KeycloakEvaluationContext(KeycloakIdentity identity, KeycloakSession keycloakSession) {
+ this.identity = identity;
+ this.keycloakSession = keycloakSession;
+ }
+
+ @Override
+ public Identity getIdentity() {
+ return this.identity;
+ }
+
+ @Override
+ public Attributes getAttributes() {
+ HashMap<String, Collection<String>> attributes = new HashMap<>();
+
+ attributes.put("kc.authz.context.time.date_time", Arrays.asList(new SimpleDateFormat("MM/dd/yyyy hh:mm:ss").format(new Date())));
+ attributes.put("kc.authz.context.client.network.ip_address", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteAddr()));
+ attributes.put("kc.authz.context.client.network.host", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteHost()));
+
+ AccessToken accessToken = this.identity.getAccessToken();
+
+ if (accessToken != null) {
+ attributes.put("kc.authz.context.client_id", Arrays.asList(accessToken.getIssuedFor()));
+ }
+
+ List<String> userAgents = this.keycloakSession.getContext().getRequestHeaders().getRequestHeader("User-Agent");
+
+ if (userAgents != null) {
+ attributes.put("kc.authz.context.client.user_agent", userAgents);
+ }
+
+ attributes.put("kc.authz.context.authc.realm", Arrays.asList(this.keycloakSession.getContext().getRealm().getName()));
+
+ return Attributes.from(attributes);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
new file mode 100644
index 0000000..9227854
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/common/KeycloakIdentity.java
@@ -0,0 +1,155 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.common;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.keycloak.authorization.attribute.Attributes;
+import org.keycloak.authorization.identity.Identity;
+import org.keycloak.authorization.util.Tokens;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.core.Response.Status;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class KeycloakIdentity implements Identity {
+
+ private final AccessToken accessToken;
+ private final RealmModel realm;
+ private final KeycloakSession keycloakSession;
+ private final Attributes attributes;
+
+ public KeycloakIdentity(KeycloakSession keycloakSession) {
+ this(Tokens.getAccessToken(keycloakSession), keycloakSession);
+ }
+
+ public KeycloakIdentity(AccessToken accessToken, KeycloakSession keycloakSession) {
+ this.accessToken = accessToken;
+
+ if (this.accessToken == null) {
+ throw new ErrorResponseException("invalid_bearer_token", "Could not obtain bearer access_token from request.", Status.FORBIDDEN);
+ }
+
+ this.keycloakSession = keycloakSession;
+ this.realm = keycloakSession.getContext().getRealm();
+
+ HashMap<String, Collection<String>> attributes = new HashMap<>();
+
+ try {
+ ObjectNode objectNode = JsonSerialization.createObjectNode(this.accessToken);
+ Iterator<String> iterator = objectNode.fieldNames();
+ List<String> roleNames = new ArrayList<>();
+
+ while (iterator.hasNext()) {
+ String fieldName = iterator.next();
+ JsonNode fieldValue = objectNode.get(fieldName);
+ List<String> values = new ArrayList<>();
+
+ values.add(fieldValue.asText());
+
+ if (fieldName.equals("realm_access")) {
+ JsonNode grantedRoles = fieldValue.get("roles");
+
+ if (grantedRoles != null) {
+ Iterator<JsonNode> rolesIt = grantedRoles.iterator();
+
+ while (rolesIt.hasNext()) {
+ roleNames.add(rolesIt.next().asText());
+ }
+ }
+ }
+
+ if (fieldName.equals("resource_access")) {
+ Iterator<JsonNode> resourceAccessIt = fieldValue.iterator();
+
+ while (resourceAccessIt.hasNext()) {
+ JsonNode grantedRoles = resourceAccessIt.next().get("roles");
+
+ if (grantedRoles != null) {
+ Iterator<JsonNode> rolesIt = grantedRoles.iterator();
+
+ while (rolesIt.hasNext()) {
+ roleNames.add(rolesIt.next().asText());
+ }
+ }
+ }
+ }
+
+ attributes.put(fieldName, values);
+ }
+
+ attributes.put("roles", roleNames);
+ } catch (Exception e) {
+ throw new RuntimeException("Error while reading attributes from security token.", e);
+ }
+
+ this.attributes = Attributes.from(attributes);
+ }
+
+ @Override
+ public String getId() {
+ if (isResourceServer()) {
+ ClientSessionModel clientSession = this.keycloakSession.sessions().getClientSession(this.accessToken.getClientSession());
+ return clientSession.getClient().getId();
+ }
+
+ return this.accessToken.getSubject();
+ }
+
+ @Override
+ public Attributes getAttributes() {
+ return this.attributes;
+ }
+
+ public AccessToken getAccessToken() {
+ return this.accessToken;
+ }
+
+ private boolean isResourceServer() {
+ UserModel clientUser = null;
+
+ if (this.accessToken.getClientSession() != null) {
+ ClientSessionModel clientSession = this.keycloakSession.sessions().getClientSession(this.accessToken.getClientSession());
+ clientUser = this.keycloakSession.users().getUserByServiceAccountClient(clientSession.getClient());
+ } else if (this.accessToken.getIssuedFor() != null) {
+ ClientModel clientModel = this.keycloakSession.realms().getClientById(this.accessToken.getIssuedFor(), this.realm);
+ clientUser = this.keycloakSession.users().getUserByServiceAccountClient(clientModel);
+ }
+
+
+ if (clientUser == null) {
+ return false;
+ }
+
+ return this.accessToken.getSubject().equals(clientUser.getId());
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/config/Configuration.java b/services/src/main/java/org/keycloak/authorization/config/Configuration.java
new file mode 100644
index 0000000..6669edc
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/config/Configuration.java
@@ -0,0 +1,269 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.config;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.keycloak.protocol.oidc.OIDCWellKnownProvider.DEFAULT_GRANT_TYPES_SUPPORTED;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class Configuration {
+
+ private static final String UMA_VERSION = "1.0";
+ private static final List<String> DEFAULT_TOKEN_PROFILES = Arrays.asList("bearer");
+
+ public static final Configuration fromDefault(String authzServerUri,
+ String realm,
+ URI authorizationEndpoint,
+ URI tokenEndpoint, String publicKeyPem) {
+ Configuration configuration = new Configuration();
+
+ if (authzServerUri.endsWith("/")) {
+ authzServerUri = authzServerUri.substring(0, authzServerUri.lastIndexOf("/"));
+ }
+
+ configuration.setVersion(UMA_VERSION);
+ configuration.setIssuer(URI.create(authzServerUri));
+ configuration.setPatProfiles(DEFAULT_TOKEN_PROFILES);
+ configuration.setAatProfiles(DEFAULT_TOKEN_PROFILES);
+ configuration.setRptProfiles(DEFAULT_TOKEN_PROFILES);
+ configuration.setPatGrantTypes(DEFAULT_GRANT_TYPES_SUPPORTED);
+ configuration.setAatGrantTypes(DEFAULT_GRANT_TYPES_SUPPORTED);
+ configuration.setTokenEndpoint(tokenEndpoint);
+ configuration.setAuthorizationEndpoint(authorizationEndpoint);
+ configuration.setResourceSetRegistrationEndpoint(URI.create(authzServerUri + "/authz/protection/resource_set"));
+ configuration.setPermissionRegistrationEndpoint(URI.create(authzServerUri + "/authz/protection/permission"));
+ configuration.setRptEndpoint(URI.create(authzServerUri + "/authz/authorize"));
+ configuration.setRealmPublicKey(publicKeyPem);
+ configuration.setServerUrl(URI.create(authzServerUri));
+ configuration.setRealm(realm);
+
+ return configuration;
+ }
+
+ private String realmPublicKey;
+ private String version;
+ private URI issuer;
+
+ @JsonProperty("pat_profiles_supported")
+ private List<String> patProfiles;
+
+ @JsonProperty("pat_grant_types_supported")
+ private List<String> patGrantTypes;
+
+ @JsonProperty("aat_profiles_supported")
+ private List<String> aatProfiles;
+
+ @JsonProperty("aat_grant_types_supported")
+ private List<String> aatGrantTypes;
+
+ @JsonProperty("rpt_profiles_supported")
+ private List<String> rptProfiles;
+
+ @JsonProperty("claim_token_profiles_supported")
+ private List<String> claimTokenProfiles;
+
+ @JsonProperty("dynamic_client_endpoint")
+ private URI dynamicClientEndpoint;
+
+ @JsonProperty("token_endpoint")
+ private URI tokenEndpoint;
+
+ @JsonProperty("authorization_endpoint")
+ private URI authorizationEndpoint;
+
+ @JsonProperty("requesting_party_claims_endpoint")
+ private URI requestingPartyClaimsEndpoint;
+
+ @JsonProperty("resource_set_registration_endpoint")
+ private URI resourceSetRegistrationEndpoint;
+
+ @JsonProperty("introspection_endpoint")
+ private URI introspectionEndpoint;
+
+ @JsonProperty("permission_registration_endpoint")
+ private URI permissionRegistrationEndpoint;
+
+ @JsonProperty("rpt_endpoint")
+ private URI rptEndpoint;
+
+ /**
+ * Non-standard, Keycloak specific configuration options
+ */
+ private String realm;
+
+ private URI serverUrl;
+
+ public String getVersion() {
+ return this.version;
+ }
+
+ void setVersion(final String version) {
+ this.version = version;
+ }
+
+ public URI getIssuer() {
+ return this.issuer;
+ }
+
+ void setIssuer(final URI issuer) {
+ this.issuer = issuer;
+ }
+
+ public List<String> getPatProfiles() {
+ return this.patProfiles;
+ }
+
+ void setPatProfiles(final List<String> patProfiles) {
+ this.patProfiles = patProfiles;
+ }
+
+ public List<String> getPatGrantTypes() {
+ return this.patGrantTypes;
+ }
+
+ void setPatGrantTypes(final List<String> patGrantTypes) {
+ this.patGrantTypes = patGrantTypes;
+ }
+
+ public List<String> getAatProfiles() {
+ return this.aatProfiles;
+ }
+
+ void setAatProfiles(final List<String> aatProfiles) {
+ this.aatProfiles = aatProfiles;
+ }
+
+ public List<String> getAatGrantTypes() {
+ return this.aatGrantTypes;
+ }
+
+ void setAatGrantTypes(final List<String> aatGrantTypes) {
+ this.aatGrantTypes = aatGrantTypes;
+ }
+
+ public List<String> getRptProfiles() {
+ return this.rptProfiles;
+ }
+
+ void setRptProfiles(final List<String> rptProfiles) {
+ this.rptProfiles = rptProfiles;
+ }
+
+ public List<String> getClaimTokenProfiles() {
+ return this.claimTokenProfiles;
+ }
+
+ void setClaimTokenProfiles(final List<String> claimTokenProfiles) {
+ this.claimTokenProfiles = claimTokenProfiles;
+ }
+
+ public URI getDynamicClientEndpoint() {
+ return this.dynamicClientEndpoint;
+ }
+
+ void setDynamicClientEndpoint(final URI dynamicClientEndpoint) {
+ this.dynamicClientEndpoint = dynamicClientEndpoint;
+ }
+
+ public URI getTokenEndpoint() {
+ return this.tokenEndpoint;
+ }
+
+ void setTokenEndpoint(final URI tokenEndpoint) {
+ this.tokenEndpoint = tokenEndpoint;
+ }
+
+ public URI getAuthorizationEndpoint() {
+ return this.authorizationEndpoint;
+ }
+
+ void setAuthorizationEndpoint(final URI authorizationEndpoint) {
+ this.authorizationEndpoint = authorizationEndpoint;
+ }
+
+ public URI getRequestingPartyClaimsEndpoint() {
+ return this.requestingPartyClaimsEndpoint;
+ }
+
+ void setRequestingPartyClaimsEndpoint(final URI requestingPartyClaimsEndpoint) {
+ this.requestingPartyClaimsEndpoint = requestingPartyClaimsEndpoint;
+ }
+
+ public URI getResourceSetRegistrationEndpoint() {
+ return this.resourceSetRegistrationEndpoint;
+ }
+
+ void setResourceSetRegistrationEndpoint(final URI resourceSetRegistrationEndpoint) {
+ this.resourceSetRegistrationEndpoint = resourceSetRegistrationEndpoint;
+ }
+
+ public URI getIntrospectionEndpoint() {
+ return this.introspectionEndpoint;
+ }
+
+ void setIntrospectionEndpoint(final URI introspectionEndpoint) {
+ this.introspectionEndpoint = introspectionEndpoint;
+ }
+
+ public URI getPermissionRegistrationEndpoint() {
+ return this.permissionRegistrationEndpoint;
+ }
+
+ void setPermissionRegistrationEndpoint(final URI permissionRegistrationEndpoint) {
+ this.permissionRegistrationEndpoint = permissionRegistrationEndpoint;
+ }
+
+ public URI getRptEndpoint() {
+ return this.rptEndpoint;
+ }
+
+ void setRptEndpoint(final URI rptEndpoint) {
+ this.rptEndpoint = rptEndpoint;
+ }
+
+ public String getRealm() {
+ return this.realm;
+ }
+
+ public void setRealm(final String realm) {
+ this.realm = realm;
+ }
+
+ public URI getServerUrl() {
+ return this.serverUrl;
+ }
+
+ public void setServerUrl(URI serverUrl) {
+ this.serverUrl = serverUrl;
+ }
+
+ public void setRealmPublicKey(String realmPublicKey) {
+ this.realmPublicKey = realmPublicKey;
+ }
+
+ public String getRealmPublicKey() {
+ return realmPublicKey;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java
new file mode 100644
index 0000000..07e5908
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.authorization.config;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.wellknown.WellKnownProvider;
+
+import javax.ws.rs.core.UriInfo;
+import java.net.URI;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaWellKnownProvider implements WellKnownProvider {
+
+ private final KeycloakSession session;
+
+ public UmaWellKnownProvider(KeycloakSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public Object getConfig() {
+ RealmModel realm = this.session.getContext().getRealm();
+ UriInfo uriInfo = this.session.getContext().getUri();
+
+ return Configuration.fromDefault(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString(), realm.getName(),
+ URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "auth").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()),
+ URI.create(RealmsResource.protocolUrl(uriInfo).path(OIDCLoginProtocolService.class, "token").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()),
+ realm.getPublicKeyPem());
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java
new file mode 100644
index 0000000..7776720
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/config/UmaWellKnownProviderFactory.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.authorization.config;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.wellknown.WellKnownProvider;
+import org.keycloak.wellknown.WellKnownProviderFactory;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaWellKnownProviderFactory implements WellKnownProviderFactory {
+ @Override
+ public WellKnownProvider create(KeycloakSession session) {
+ return new UmaWellKnownProvider(session);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "uma-configuration";
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java
new file mode 100644
index 0000000..5df5a0b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/DefaultAuthorizationProviderFactory.java
@@ -0,0 +1,73 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization;
+
+import org.keycloak.Config;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.cache.authorization.CachedStoreFactoryProvider;
+
+import java.util.concurrent.Executor;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class DefaultAuthorizationProviderFactory implements AuthorizationProviderFactory {
+
+ private Executor scheduler;
+
+ @Override
+ public AuthorizationProvider create(KeycloakSession session) {
+ StoreFactory storeFactory = session.getProvider(CachedStoreFactoryProvider.class);
+
+ if (storeFactory == null) {
+ storeFactory = session.getProvider(StoreFactory.class);
+ }
+
+ return new AuthorizationProvider(session, storeFactory);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ //TODO: user-defined configuration
+// Executor executor = Executors.newWorkStealingPool();
+// this.scheduler = command -> {
+// Map<Class<?>, Object> contextDataMap = ResteasyProviderFactory.getContextDataMap();
+// executor.execute(() -> {
+// ResteasyProviderFactory.pushContextDataMap(contextDataMap);
+// command.run();
+// });
+// };
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "authorization";
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
new file mode 100644
index 0000000..983646b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
@@ -0,0 +1,260 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.entitlement;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.OAuthErrorException;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.common.KeycloakEvaluationContext;
+import org.keycloak.authorization.common.KeycloakIdentity;
+import org.keycloak.authorization.entitlement.representation.EntitlementRequest;
+import org.keycloak.authorization.entitlement.representation.EntitlementResponse;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
+import org.keycloak.authorization.policy.evaluation.Result;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.authorization.util.Permissions;
+import org.keycloak.authorization.util.Tokens;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakContext;
+import org.keycloak.models.RealmModel;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.authorization.Permission;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.resources.Cors;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.OPTIONS;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class EntitlementService {
+
+ private final AuthorizationProvider authorization;
+
+ @Context
+ private HttpRequest request;
+
+ public EntitlementService(AuthorizationProvider authorization) {
+ this.authorization = authorization;
+ }
+
+ @OPTIONS
+ public Response authorizePreFlight() {
+ return Cors.add(this.request, Response.ok()).auth().preflight().build();
+ }
+
+ @Path("{resource_server_id}")
+ @GET()
+ @Produces("application/json")
+ @Consumes("application/json")
+ public void getAll(@PathParam("resource_server_id") String resourceServerId, @Suspended AsyncResponse asyncResponse) {
+ KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession());
+
+ if (resourceServerId == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Requires resource_server_id request parameter.", Status.BAD_REQUEST);
+ }
+
+ RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm();
+ ClientModel client = realm.getClientByClientId(resourceServerId);
+
+ if (client == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Identifier is not associated with any client and resource server.", Status.BAD_REQUEST);
+ }
+
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(client.getId());
+
+ authorization.evaluators().from(Permissions.all(resourceServer, identity, authorization), new KeycloakEvaluationContext(this.authorization.getKeycloakSession())).evaluate(new DecisionResultCollector() {
+
+ @Override
+ public void onError(Throwable cause) {
+ asyncResponse.resume(cause);
+ }
+
+ @Override
+ protected void onComplete(List<Result> results) {
+ List<Permission> entitlements = Permissions.allPermits(results);
+
+ if (entitlements.isEmpty()) {
+ asyncResponse.resume(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN));
+ } else {
+ asyncResponse.resume(Cors.add(request, Response.ok().entity(new EntitlementResponse(createRequestingPartyToken(entitlements)))).allowedOrigins(identity.getAccessToken()).allowedMethods("GET").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
+ }
+ }
+ });
+ }
+
+ @Path("{resource_server_id}")
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public void get(@PathParam("resource_server_id") String resourceServerId, EntitlementRequest entitlementRequest, @Suspended AsyncResponse asyncResponse) {
+ KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession());
+
+ if (entitlementRequest == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid entitlement request.", Status.BAD_REQUEST);
+ }
+
+ if (resourceServerId == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Invalid resource_server_id.", Status.BAD_REQUEST);
+ }
+
+ RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm();
+
+ ClientModel client = realm.getClientByClientId(resourceServerId);
+
+ if (client == null) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Identifier is not associated with any resource server.", Status.BAD_REQUEST);
+ }
+
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(client.getId());
+
+ authorization.evaluators().from(createPermissions(entitlementRequest, resourceServer, authorization), new KeycloakEvaluationContext(this.authorization.getKeycloakSession())).evaluate(new DecisionResultCollector() {
+
+ @Override
+ public void onError(Throwable cause) {
+ asyncResponse.resume(cause);
+ }
+
+ @Override
+ protected void onComplete(List<Result> results) {
+ List<Permission> entitlements = Permissions.allPermits(results);
+
+ if (entitlements.isEmpty()) {
+ asyncResponse.resume(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN));
+ } else {
+ asyncResponse.resume(Cors.add(request, Response.ok().entity(new EntitlementResponse(createRequestingPartyToken(entitlements)))).allowedOrigins(identity.getAccessToken()).allowedMethods("GET").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
+ }
+ }
+ });
+ }
+
+ private String createRequestingPartyToken(List<Permission> permissions) {
+ AccessToken accessToken = Tokens.getAccessToken(this.authorization.getKeycloakSession());
+ RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm();
+ AccessToken.Authorization authorization = new AccessToken.Authorization();
+
+ authorization.setPermissions(permissions);
+
+ accessToken.setAuthorization(authorization);
+ ;
+ return new TokenManager().encodeToken(realm, accessToken);
+ }
+
+ private List<ResourcePermission> createPermissions(EntitlementRequest entitlementRequest, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Map<String, Set<String>> permissionsToEvaluate = new HashMap<>();
+
+ entitlementRequest.getPermissions().forEach(requestedResource -> {
+ Resource resource;
+
+ if (requestedResource.getResourceSetId() != null) {
+ resource = storeFactory.getResourceStore().findById(requestedResource.getResourceSetId());
+ } else {
+ resource = storeFactory.getResourceStore().findByName(requestedResource.getResourceSetName(), resourceServer.getId());
+ }
+
+ if (resource == null) {
+ throw new ErrorResponseException("invalid_resource", "Resource with id [" + requestedResource.getResourceSetId() + "] or name [" + requestedResource.getResourceSetName() + "] does not exist.", Status.FORBIDDEN);
+ }
+
+ permissionsToEvaluate.put(resource.getId(), requestedResource.getScopes());
+ });
+
+ String rpt = entitlementRequest.getRpt();
+
+ if (rpt != null && !"".equals(rpt)) {
+ KeycloakContext context = authorization.getKeycloakSession().getContext();
+
+ if (!Tokens.verifySignature(rpt, context.getRealm().getPublicKey())) {
+ throw new ErrorResponseException("invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN);
+ }
+
+ AccessToken requestingPartyToken;
+
+ try {
+ requestingPartyToken = new JWSInput(rpt).readJsonContent(AccessToken.class);
+ } catch (JWSInputException e) {
+ throw new ErrorResponseException("invalid_rpt", "Invalid RPT", Status.FORBIDDEN);
+ }
+
+ if (requestingPartyToken.isActive()) {
+ AccessToken.Authorization authorizationData = requestingPartyToken.getAuthorization();
+
+ if (authorizationData != null) {
+ authorizationData.getPermissions().forEach(permission -> {
+ Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId());
+
+ if (resourcePermission != null) {
+ Set<String> scopes = permissionsToEvaluate.get(resourcePermission.getId());
+
+ if (scopes == null) {
+ scopes = new HashSet<>();
+ permissionsToEvaluate.put(resourcePermission.getId(), scopes);
+ }
+
+ scopes.addAll(permission.getScopes());
+ }
+ });
+ }
+ }
+ }
+
+ return permissionsToEvaluate.entrySet().stream()
+ .flatMap((Function<Map.Entry<String, Set<String>>, Stream<ResourcePermission>>) entry -> {
+ Resource entryResource = storeFactory.getResourceStore().findById(entry.getKey());
+
+ if (entry.getValue().isEmpty()) {
+ return Arrays.asList(new ResourcePermission(entryResource, Collections.emptyList(), entryResource.getResourceServer())).stream();
+ } else {
+ return entry.getValue().stream()
+ .map(scopeName -> storeFactory.getScopeStore().findByName(scopeName, entryResource.getResourceServer().getId()))
+ .filter(scope -> scope != null)
+ .map(scope -> new ResourcePermission(entryResource, Arrays.asList(scope), entryResource.getResourceServer()));
+ }
+ }).collect(Collectors.toList());
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java
new file mode 100644
index 0000000..3afcc31
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java
@@ -0,0 +1,25 @@
+package org.keycloak.authorization.entitlement.representation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.keycloak.authorization.protection.permission.representation.PermissionRequest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class EntitlementRequest {
+
+ private String rpt;
+
+ private List<PermissionRequest> permissions = new ArrayList<>();
+
+ public List<PermissionRequest> getPermissions() {
+ return permissions;
+ }
+
+ public String getRpt() {
+ return rpt;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java
new file mode 100644
index 0000000..8e883cd
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementResponse.java
@@ -0,0 +1,42 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.entitlement.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class EntitlementResponse {
+
+ private String rpt;
+
+ public EntitlementResponse(String rpt) {
+ this.rpt = rpt;
+ }
+
+ public EntitlementResponse() {
+ this(null);
+ }
+
+ public String getRpt() {
+ return this.rpt;
+ }
+
+ public void setRpt(final String rpt) {
+ this.rpt = rpt;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/ErrorCode.java b/services/src/main/java/org/keycloak/authorization/ErrorCode.java
new file mode 100644
index 0000000..63ac38a
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/ErrorCode.java
@@ -0,0 +1,28 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface ErrorCode {
+
+ String INVALID_CLIENT_ID = "invalid_client_id";
+
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java
new file mode 100644
index 0000000..a31e834
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProvider.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.authorization.protection.introspect;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.AccessToken.Authorization;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * Introspects token accordingly with UMA Bearer Token Profile.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RPTIntrospectionProvider extends AccessTokenIntrospectionProvider {
+
+ protected static final Logger LOGGER = Logger.getLogger(RPTIntrospectionProvider.class);
+
+ public RPTIntrospectionProvider(KeycloakSession session) {
+ super(session);
+ }
+
+ @Override
+ public Response introspect(String token) {
+ LOGGER.debug("Introspecting requesting party token");
+ try {
+ AccessToken requestingPartyToken = toAccessToken(token);
+ boolean active = isActive(requestingPartyToken);
+ ObjectNode tokenMetadata;
+
+ if (active) {
+ LOGGER.debug("Token is active");
+ AccessToken introspect = new AccessToken();
+ introspect.type(requestingPartyToken.getType());
+ introspect.expiration(requestingPartyToken.getExpiration());
+ introspect.issuedAt(requestingPartyToken.getIssuedAt());
+ introspect.audience(requestingPartyToken.getAudience());
+ introspect.notBefore(requestingPartyToken.getNotBefore());
+ introspect.setRealmAccess(null);
+ introspect.setResourceAccess(null);
+ tokenMetadata = JsonSerialization.createObjectNode(introspect);
+ tokenMetadata.putPOJO("permissions", requestingPartyToken.getAuthorization().getPermissions());
+ } else {
+ LOGGER.debug("Token is not active");
+ tokenMetadata = JsonSerialization.createObjectNode();
+ }
+
+ tokenMetadata.put("active", active);
+
+ return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).type(MediaType.APPLICATION_JSON_TYPE).build();
+ } catch (Exception e) {
+ throw new RuntimeException("Error creating token introspection response.", e);
+ }
+ }
+
+ private boolean isActive(AccessToken requestingPartyToken) {
+ Authorization authorization = requestingPartyToken.getAuthorization();
+ return requestingPartyToken.isActive() && authorization != null && authorization.getPermissions() != null && !authorization.getPermissions().isEmpty();
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProviderFactory.java b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProviderFactory.java
new file mode 100644
index 0000000..1c4d894
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/introspect/RPTIntrospectionProviderFactory.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.authorization.protection.introspect;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.protocol.oidc.TokenIntrospectionProvider;
+import org.keycloak.protocol.oidc.TokenIntrospectionProviderFactory;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RPTIntrospectionProviderFactory implements TokenIntrospectionProviderFactory {
+ @Override
+ public TokenIntrospectionProvider create(KeycloakSession session) {
+ return new RPTIntrospectionProvider(session);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return "requesting_party_token";
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
new file mode 100644
index 0000000..cf2f9e0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
@@ -0,0 +1,102 @@
+package org.keycloak.authorization.protection.permission;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.common.KeycloakIdentity;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.protection.permission.representation.PermissionRequest;
+import org.keycloak.authorization.protection.permission.representation.PermissionResponse;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.services.ErrorResponseException;
+
+import javax.ws.rs.core.Response;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AbstractPermissionService {
+
+ private final AuthorizationProvider authorization;
+ private final KeycloakIdentity identity;
+ private final ResourceServer resourceServer;
+
+ public AbstractPermissionService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ this.identity = identity;
+ this.resourceServer = resourceServer;
+ this.authorization = authorization;
+ }
+
+ public Response create(List<PermissionRequest> request) {
+ if (request == null) {
+ throw new ErrorResponseException("invalid_permission_request", "Invalid permission request.", Response.Status.BAD_REQUEST);
+ }
+
+ List<ResourceRepresentation> resource = verifyRequestedResource(request);
+
+ return Response.status(Response.Status.CREATED).entity(new PermissionResponse(createPermissionTicket(resource))).build();
+ }
+
+ private List<ResourceRepresentation> verifyRequestedResource(List<PermissionRequest> request) {
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ return request.stream().map(request1 -> {
+ String resourceSetId = request1.getResourceSetId();
+ String resourceSetName = request1.getResourceSetName();
+
+ if (resourceSetId == null && resourceSetName == null) {
+ throw new ErrorResponseException("invalid_resource_set_id", "Resource id or name not provided.", Response.Status.BAD_REQUEST);
+ }
+
+ Resource resource;
+
+ if (resourceSetId != null) {
+ resource = storeFactory.getResourceStore().findById(resourceSetId);
+ } else {
+ resource = storeFactory.getResourceStore().findByName(resourceSetName, this.resourceServer.getId());
+ }
+
+ if (resource == null) {
+ if (resourceSetId != null) {
+ throw new ErrorResponseException("nonexistent_resource_set_id", "Resource set with id[" + resourceSetId + "] does not exists in this server.", Response.Status.BAD_REQUEST);
+ } else {
+ throw new ErrorResponseException("nonexistent_resource_set_name", "Resource set with name[" + resourceSetName + "] does not exists in this server.", Response.Status.BAD_REQUEST);
+ }
+ }
+
+ return new ResourceRepresentation(resource.getName(), verifyRequestedScopes(request1, resource));
+ }).collect(Collectors.toList());
+ }
+
+ private Set<ScopeRepresentation> verifyRequestedScopes(PermissionRequest request, Resource resource) {
+ return request.getScopes().stream().map(scopeName -> {
+ for (Scope scope : resource.getScopes()) {
+ if (scope.getName().equals(scopeName)) {
+ return new ScopeRepresentation(scopeName);
+ }
+ }
+
+ for (Resource baseResource : authorization.getStoreFactory().getResourceStore().findByType(resource.getType())) {
+ if (baseResource.getOwner().equals(resource.getResourceServer().getClientId())) {
+ for (Scope baseScope : baseResource.getScopes()) {
+ if (baseScope.getName().equals(scopeName)) {
+ return new ScopeRepresentation(scopeName);
+ }
+ }
+ }
+ }
+
+ throw new ErrorResponseException("invalid_scope", "Scope [" + scopeName + " is not valid.", Response.Status.BAD_REQUEST);
+ }).collect(Collectors.toSet());
+ }
+
+ private String createPermissionTicket(List<ResourceRepresentation> resources) {
+ return new JWSBuilder().jsonContent(new PermissionTicket(resources, this.resourceServer.getId(), this.identity.getAccessToken()))
+ .rsa256(this.authorization.getKeycloakSession().getContext().getRealm().getPrivateKey());
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java
new file mode 100644
index 0000000..9d54730
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionService.java
@@ -0,0 +1,59 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.protection.permission;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.common.KeycloakIdentity;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.protection.permission.representation.PermissionRequest;
+import org.keycloak.authorization.protection.permission.representation.PermissionResponse;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.jose.jws.JWSBuilder;
+import org.keycloak.services.ErrorResponseException;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionService extends AbstractPermissionService {
+
+ public PermissionService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ super(identity, resourceServer, authorization);
+ }
+
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response create(PermissionRequest request) {
+ return create(Arrays.asList(request));
+ }
+
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java
new file mode 100644
index 0000000..eea2108
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionsService.java
@@ -0,0 +1,46 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.protection.permission;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.common.KeycloakIdentity;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.protection.permission.representation.PermissionRequest;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionsService extends AbstractPermissionService {
+
+ public PermissionsService(KeycloakIdentity identity, ResourceServer resourceServer, AuthorizationProvider authorization) {
+ super(identity, resourceServer, authorization);
+ }
+
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response create(List<PermissionRequest> request) {
+ return super.create(request);
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java
new file mode 100644
index 0000000..9ee6368
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicket.java
@@ -0,0 +1,58 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.protection.permission;
+
+import org.keycloak.TokenIdGenerator;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.JsonWebToken;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionTicket extends JsonWebToken {
+
+ private final List<ResourceRepresentation> resources = new ArrayList<>();
+ private final String resourceServerId;
+
+ public PermissionTicket() {
+ this.resourceServerId = null;
+ }
+
+ public PermissionTicket(List<ResourceRepresentation> resources, String resourceServerId, AccessToken accessToken) {
+ id(TokenIdGenerator.generateId());
+ subject(accessToken.getSubject());
+ expiration(accessToken.getExpiration());
+ notBefore(accessToken.getNotBefore());
+ issuedAt(accessToken.getIssuedAt());
+ issuedFor(accessToken.getIssuedFor());
+ this.resources.addAll(resources);
+ this.resourceServerId = resourceServerId;
+ }
+
+ public List<ResourceRepresentation> getResources() {
+ return this.resources;
+ }
+
+ public String getResourceServerId() {
+ return this.resourceServerId;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java
new file mode 100644
index 0000000..31d6d55
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionRequest.java
@@ -0,0 +1,67 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.protection.permission.representation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionRequest {
+
+ @JsonProperty("resource_set_id")
+ private final String resourceSetId;
+
+ @JsonProperty("resource_set_name")
+ private final String resourceSetName;
+
+ private final Set<String> scopes;
+
+ public PermissionRequest(String resourceSetId, String... scopes) {
+ this.resourceSetId = resourceSetId;
+
+ if (scopes != null) {
+ this.scopes = new HashSet(Arrays.asList(scopes));
+ } else {
+ this.scopes = new HashSet<>();
+ }
+
+ this.resourceSetName = null;
+ }
+
+ public PermissionRequest() {
+ this(null, null);
+ }
+
+ public String getResourceSetId() {
+ return this.resourceSetId;
+ }
+
+ public String getResourceSetName() {
+ return resourceSetName;
+ }
+
+ public Set<String> getScopes() {
+ return this.scopes;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java
new file mode 100644
index 0000000..24efefa
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/permission/representation/PermissionResponse.java
@@ -0,0 +1,39 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.protection.permission.representation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class PermissionResponse {
+
+ private final String ticket;
+
+ public PermissionResponse(String ticket) {
+ this.ticket = ticket;
+ }
+
+ public PermissionResponse() {
+ this(null);
+ }
+
+ public String getTicket() {
+ return this.ticket;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
new file mode 100644
index 0000000..f3695d0
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
@@ -0,0 +1,117 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.protection;
+
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.OAuthErrorException;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.ResourceSetService;
+import org.keycloak.authorization.common.KeycloakIdentity;
+import org.keycloak.authorization.identity.Identity;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.protection.permission.PermissionService;
+import org.keycloak.authorization.protection.permission.PermissionsService;
+import org.keycloak.authorization.protection.resource.ResourceService;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.ErrorResponseException;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Response.Status;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ProtectionService {
+
+ private final AuthorizationProvider authorization;
+
+ public ProtectionService(AuthorizationProvider authorization) {
+ this.authorization = authorization;
+ }
+
+ @Path("/resource_set")
+ public Object resource() {
+ KeycloakIdentity identity = createIdentity();
+
+ if (!identity.hasRole("uma_protection")) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN);
+ }
+
+ ResourceSetService resourceManager = new ResourceSetService(getResourceServer(identity), this.authorization, null);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resourceManager);
+
+ ResourceService resource = new ResourceService(getResourceServer(identity), identity, resourceManager, this.authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
+
+ @Path("/permission")
+ public Object permission() {
+ KeycloakIdentity identity = createIdentity();
+
+ if (!identity.hasRole("uma_protection")) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN);
+ }
+
+ PermissionService resource = new PermissionService(identity, getResourceServer(identity), this.authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
+
+ @Path("/permissions")
+ public Object permissions() {
+ KeycloakIdentity identity = createIdentity();
+
+ if (!identity.hasRole("uma_protection")) {
+ throw new ErrorResponseException(OAuthErrorException.INVALID_SCOPE, "Requires uma_protection scope.", Status.FORBIDDEN);
+ }
+
+ PermissionsService resource = new PermissionsService(identity, getResourceServer(identity), this.authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
+
+ private KeycloakIdentity createIdentity() {
+ return new KeycloakIdentity(this.authorization.getKeycloakSession());
+ }
+
+ private ResourceServer getResourceServer(Identity identity) {
+ RealmModel realm = this.authorization.getKeycloakSession().getContext().getRealm();
+ ClientModel clientApplication = realm.getClientById(identity.getId());
+
+ if (clientApplication == null) {
+ throw new ErrorResponseException("invalid_clientId", "Client application with id [" + identity.getId() + "] does not exist in realm [" + realm.getName() + "]", Status.BAD_REQUEST);
+ }
+
+ ResourceServer resourceServer = this.authorization.getStoreFactory().getResourceServerStore().findByClient(identity.getId());
+
+ if (resourceServer == null) {
+ throw new ErrorResponseException("invalid_clientId", "Client application [" + clientApplication.getClientId() + "] is not registered as resource server.", Status.FORBIDDEN);
+ }
+
+ return resourceServer;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/RegistrationResponse.java b/services/src/main/java/org/keycloak/authorization/protection/resource/RegistrationResponse.java
new file mode 100644
index 0000000..4f82242
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/RegistrationResponse.java
@@ -0,0 +1,50 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.protection.resource;
+
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+import org.keycloak.authorization.protection.resource.representation.UmaResourceRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RegistrationResponse {
+
+ private final UmaResourceRepresentation resourceDescription;
+
+ public RegistrationResponse(UmaResourceRepresentation resourceDescription) {
+ this.resourceDescription = resourceDescription;
+ }
+
+ public RegistrationResponse() {
+ this(null);
+ }
+
+ @JsonUnwrapped
+ public UmaResourceRepresentation getResourceDescription() {
+ return this.resourceDescription;
+ }
+
+ public String getId() {
+ if (this.resourceDescription != null) {
+ return this.resourceDescription.getId();
+ }
+
+ return null;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/RegistrationResponse.java b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/RegistrationResponse.java
new file mode 100644
index 0000000..6922d51
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/RegistrationResponse.java
@@ -0,0 +1,50 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.protection.resource.representation;
+
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RegistrationResponse {
+
+ private final UmaResourceRepresentation resourceDescription;
+
+ public RegistrationResponse(UmaResourceRepresentation resourceDescription) {
+ this.resourceDescription = resourceDescription;
+ }
+
+ public RegistrationResponse() {
+ this(null);
+ }
+
+ @JsonUnwrapped
+ public UmaResourceRepresentation getResourceDescription() {
+ return this.resourceDescription;
+ }
+
+ public String getId() {
+ if (this.resourceDescription != null) {
+ return this.resourceDescription.getId();
+ }
+
+ return null;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java
new file mode 100644
index 0000000..0e9bafa
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java
@@ -0,0 +1,150 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.protection.resource.representation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * <p>One or more resources that the resource server manages as a set of protected resources.
+ *
+ * <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.2">OAuth-resource-reg</a>.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaResourceRepresentation {
+
+ @JsonProperty("_id")
+ private String id;
+
+ private String name;
+ private String uri;
+ private String type;
+ private Set<UmaScopeRepresentation> scopes;
+
+ @JsonProperty("icon_uri")
+ private String iconUri;
+ private String owner;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param name a human-readable string describing a set of one or more resources
+ * @param uri a {@link URI} that provides the network location for the resource set being registered
+ * @param type a string uniquely identifying the semantics of the resource set
+ * @param scopes the available scopes for this resource set
+ * @param iconUri a {@link URI} for a graphic icon representing the resource set
+ */
+ public UmaResourceRepresentation(String name, Set<UmaScopeRepresentation> scopes, String uri, String type, String iconUri) {
+ this.name = name;
+ this.scopes = scopes;
+ this.uri = uri;
+ this.type = type;
+ this.iconUri = iconUri;
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param name a human-readable string describing a set of one or more resources
+ * @param uri a {@link URI} that provides the network location for the resource set being registered
+ * @param type a string uniquely identifying the semantics of the resource set
+ * @param scopes the available scopes for this resource set
+ */
+ public UmaResourceRepresentation(String name, Set<UmaScopeRepresentation> scopes, String uri, String type) {
+ this(name, scopes, uri, type, null);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param name a human-readable string describing a set of one or more resources
+ * @param serverUri a {@link URI} that identifies this resource server
+ * @param scopes the available scopes for this resource set
+ */
+ public UmaResourceRepresentation(String name, Set<UmaScopeRepresentation> scopes) {
+ this(name, scopes, null, null, null);
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ */
+ public UmaResourceRepresentation() {
+ this(null, null, null, null, null);
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getUri() {
+ return this.uri;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public Set<UmaScopeRepresentation> getScopes() {
+ return Collections.unmodifiableSet(this.scopes);
+ }
+
+ public String getIconUri() {
+ return this.iconUri;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setUri(String uri) {
+ this.uri = uri;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public void setScopes(Set<UmaScopeRepresentation> scopes) {
+ this.scopes = scopes;
+ }
+
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ public void setOwner(String owner) {
+ this.owner = owner;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaScopeRepresentation.java b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaScopeRepresentation.java
new file mode 100644
index 0000000..4a184d9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaScopeRepresentation.java
@@ -0,0 +1,98 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.protection.resource.representation;
+
+import java.net.URI;
+import java.util.Objects;
+
+/**
+ * <p>A bounded extent of access that is possible to perform on a resource set. In authorization policy terminology,
+ * a scope is one of the potentially many "verbs" that can logically apply to a resource set ("object").
+ *
+ * <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.1">OAuth-resource-reg</a>.
+ *
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class UmaScopeRepresentation {
+
+ private String id;
+ private String name;
+ private String iconUri;
+
+ /**
+ * Creates an instance.
+ *
+ * @param name the a human-readable string describing some scope (extent) of access
+ * @param iconUri a {@link URI} for a graphic icon representing the scope
+ */
+ public UmaScopeRepresentation(String name, String iconUri) {
+ this.name = name;
+ this.iconUri = iconUri;
+ }
+
+ /**
+ * Creates an instance.
+ *
+ * @param name the a human-readable string describing some scope (extent) of access
+ */
+ public UmaScopeRepresentation(String name) {
+ this(name, null);
+ }
+
+ /**
+ * Creates an instance.
+ */
+ public UmaScopeRepresentation() {
+ this(null, null);
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getIconUri() {
+ return this.iconUri;
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UmaScopeRepresentation scope = (UmaScopeRepresentation) o;
+ return Objects.equals(getName(), scope.getName());
+ }
+
+ public int hashCode() {
+ return Objects.hash(getName());
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setIconUri(String iconUri) {
+ this.iconUri = iconUri;
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
new file mode 100644
index 0000000..f4aaac5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
@@ -0,0 +1,228 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.protection.resource;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.admin.ResourceSetService;
+import org.keycloak.authorization.admin.representation.ResourceOwnerRepresentation;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.admin.util.Models;
+import org.keycloak.authorization.identity.Identity;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.protection.resource.representation.UmaResourceRepresentation;
+import org.keycloak.authorization.protection.resource.representation.UmaScopeRepresentation;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.services.ErrorResponseException;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceService {
+
+ private final ResourceServer resourceServer;
+ private final ResourceSetService resourceManager;
+ private final Identity identity;
+ private final AuthorizationProvider authorization;
+
+ public ResourceService(ResourceServer resourceServer, Identity identity, ResourceSetService resourceManager, AuthorizationProvider authorization) {
+ this.identity = identity;
+ this.resourceServer = resourceServer;
+ this.resourceManager = resourceManager;
+ this.authorization = authorization;
+ }
+
+ @POST
+ @Consumes("application/json")
+ @Produces("application/json")
+ public Response create(UmaResourceRepresentation umaResource) {
+ checkResourceServerSettings();
+ ResourceRepresentation resource = toResourceRepresentation(umaResource);
+ Response response = this.resourceManager.create(resource);
+
+ if (response.getEntity() instanceof ResourceRepresentation) {
+ return Response.status(Status.CREATED).entity(toUmaRepresentation((ResourceRepresentation) response.getEntity())).build();
+ }
+
+ return response;
+ }
+
+ @Path("/{id}")
+ @DELETE
+ public Response delete(@PathParam("id") String id) {
+ checkResourceServerSettings();
+ return this.resourceManager.delete(id);
+ }
+
+ @Path("/{id}")
+ @GET
+ @Produces("application/json")
+ public RegistrationResponse findById(@PathParam("id") String id) {
+ Response response = this.resourceManager.findById(id);
+ UmaResourceRepresentation resource = toUmaRepresentation((ResourceRepresentation) response.getEntity());
+
+ if (resource == null) {
+ throw new ErrorResponseException("not_found", "Resource with id [" + id + "] not found.", Status.NOT_FOUND);
+ }
+
+ return new RegistrationResponse(resource);
+ }
+
+ @GET
+ @Produces("application/json")
+ public Set<String> find(@QueryParam("filter") String filter) {
+ if (filter == null) {
+ return findAll();
+ } else {
+ return findByFilter(filter);
+ }
+ }
+
+ private Set<String> findAll() {
+ Response response = this.resourceManager.findAll();
+ List<ResourceRepresentation> resources = (List<ResourceRepresentation>) response.getEntity();
+ return resources.stream().map(ResourceRepresentation::getId).collect(Collectors.toSet());
+ }
+
+ private Set<String> findByFilter(String filter) {
+ Set<ResourceRepresentation> resources = new HashSet<>();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+
+ if (filter != null) {
+ for (String currentFilter : filter.split("&")) {
+ String[] parts = currentFilter.split("=");
+ String filterType = parts[0];
+ final String filterValue;
+
+ if (parts.length > 1) {
+ filterValue = parts[1];
+ } else {
+ filterValue = null;
+ }
+
+
+ if ("name".equals(filterType)) {
+ resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getName())).collect(Collectors.toSet()).stream()
+ .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization))
+ .collect(Collectors.toList()));
+ } else if ("type".equals(filterType)) {
+ resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getType())).collect(Collectors.toSet()).stream()
+ .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization))
+ .collect(Collectors.toList()));
+ } else if ("uri".equals(filterType)) {
+ resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getUri())).collect(Collectors.toSet()).stream()
+ .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization))
+ .collect(Collectors.toList()));
+ } else if ("owner".equals(filterType)) {
+ resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getOwner())).collect(Collectors.toSet()).stream()
+ .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization))
+ .collect(Collectors.toList()));
+ }
+ }
+ } else {
+ resources = storeFactory.getResourceStore().findByOwner(identity.getId()).stream()
+ .map(resource -> Models.toRepresentation(resource, this.resourceServer, authorization))
+ .collect(Collectors.toSet());
+ }
+
+ return resources.stream()
+ .map(ResourceRepresentation::getId)
+ .collect(Collectors.toSet());
+ }
+
+ private ResourceRepresentation toResourceRepresentation(UmaResourceRepresentation umaResource) {
+ ResourceRepresentation resource = new ResourceRepresentation();
+
+ resource.setId(umaResource.getId());
+ resource.setIconUri(umaResource.getIconUri());
+ resource.setName(umaResource.getName());
+ resource.setUri(umaResource.getUri());
+ resource.setType(umaResource.getType());
+
+ ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation();
+ String ownerId = umaResource.getOwner();
+
+ if (ownerId == null) {
+ ownerId = this.identity.getId();
+ }
+
+ owner.setId(ownerId);
+ resource.setOwner(owner);
+
+ resource.setScopes(umaResource.getScopes().stream().map(representation -> {
+ ScopeRepresentation scopeRepresentation = new ScopeRepresentation();
+
+ scopeRepresentation.setId(representation.getId());
+ scopeRepresentation.setName(representation.getName());
+ scopeRepresentation.setIconUri(representation.getIconUri());
+
+ return scopeRepresentation;
+ }).collect(Collectors.toSet()));
+
+ return resource;
+ }
+
+ private UmaResourceRepresentation toUmaRepresentation(ResourceRepresentation representation) {
+ if (representation == null) {
+ return null;
+ }
+
+ UmaResourceRepresentation resource = new UmaResourceRepresentation();
+
+ resource.setId(representation.getId());
+ resource.setIconUri(representation.getIconUri());
+ resource.setName(representation.getName());
+ resource.setUri(representation.getUri());
+ resource.setType(representation.getType());
+
+ if (representation.getOwner() != null) {
+ resource.setOwner(representation.getOwner().getId());
+ }
+
+ resource.setScopes(representation.getScopes().stream().map(scopeRepresentation -> {
+ UmaScopeRepresentation umaScopeRep = new UmaScopeRepresentation();
+ umaScopeRep.setId(scopeRepresentation.getId());
+ umaScopeRep.setName(scopeRepresentation.getName());
+ umaScopeRep.setIconUri(scopeRepresentation.getIconUri());
+ return umaScopeRep;
+ }).collect(Collectors.toSet()));
+
+ return resource;
+ }
+
+ private void checkResourceServerSettings() {
+ if (!this.resourceServer.isAllowRemoteResourceManagement()) {
+ throw new ErrorResponseException("not_supported", "Remote management is disabled.", Status.BAD_REQUEST);
+ }
+ }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authorization/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
new file mode 100644
index 0000000..240fafa
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
@@ -0,0 +1,138 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.util;
+
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.Decision.Effect;
+import org.keycloak.authorization.identity.Identity;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.model.Scope;
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.Result;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.representations.authorization.Permission;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public final class Permissions {
+
+ /**
+ * Returns a list of permissions for all resources and scopes that belong to the given <code>resourceServer</code> and
+ * <code>identity</code>.
+ *
+ * TODO: review once we support caches
+ *
+ * @param resourceServer
+ * @param identity
+ * @param authorization
+ * @return
+ */
+ public static List<ResourcePermission> all(ResourceServer resourceServer, Identity identity, AuthorizationProvider authorization) {
+ List<ResourcePermission> permissions = new ArrayList<>();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+
+ storeFactory.getResourceStore().findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource)));
+ storeFactory.getResourceStore().findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource)));
+
+ return permissions;
+ }
+
+ public static List<ResourcePermission> createResourcePermissions(Resource resource) {
+ List<ResourcePermission> permissions = new ArrayList<>();
+ List<Scope> scopes = resource.getScopes();
+
+ permissions.add(new ResourcePermission(resource, Collections.emptyList(), resource.getResourceServer()));
+
+ for (Scope scope : scopes) {
+ permissions.add(new ResourcePermission(resource, Arrays.asList(scope), resource.getResourceServer()));
+ }
+
+ return permissions;
+ }
+
+ public static List<Permission> allPermits(List<Result> evaluation) {
+ List<Permission> permissions = evaluation.stream()
+ .filter(evaluationResult -> evaluationResult.getEffect().equals(Effect.PERMIT))
+ .map(evaluationResult -> {
+ ResourcePermission permission = evaluationResult.getPermission();
+ String resourceId = null;
+ String resourceName = null;
+
+ Resource resource = permission.getResource();
+
+ if (resource != null) {
+ resourceId = resource.getId();
+ resourceName = resource.getName();
+ }
+
+ Set<String> scopes = null;
+
+ if (!permission.getScopes().isEmpty()) {
+ scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet());
+ }
+
+ return new Permission(resourceId, resourceName, scopes);
+ }).collect(Collectors.toList());
+
+ Map<String, Permission> perms = new HashMap<>();
+
+ permissions.forEach(permission -> {
+ Permission evalPermission = perms.get(permission.getResourceSetId());
+
+ if (evalPermission == null) {
+ evalPermission = permission;
+ if (evalPermission.getScopes() != null && evalPermission.getScopes().isEmpty()) {
+ evalPermission.setScopes(null);
+ }
+ perms.put(permission.getResourceSetId(), evalPermission);
+ }
+
+ Set<String> permissionScopes = permission.getScopes();
+
+ if (permissionScopes != null && !permissionScopes.isEmpty()) {
+ Set<String> scopes = evalPermission.getScopes();
+
+ if (scopes == null) {
+ scopes = new HashSet();
+ evalPermission.setScopes(scopes);
+ }
+
+ for (String scopeName : permissionScopes) {
+ if (!scopes.contains(scopeName)) {
+ scopes.add(scopeName);
+ }
+ }
+ }
+ });
+
+ return perms.values().stream().collect(Collectors.toList());
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authorization/util/Tokens.java b/services/src/main/java/org/keycloak/authorization/util/Tokens.java
new file mode 100644
index 0000000..0deeef5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/util/Tokens.java
@@ -0,0 +1,65 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.util;
+
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.RSAProvider;
+import org.keycloak.models.KeycloakContext;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.services.managers.AppAuthManager;
+import org.keycloak.services.managers.AuthenticationManager.AuthResult;
+
+import javax.ws.rs.core.Response.Status;
+import java.security.PublicKey;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class Tokens {
+
+ public static AccessToken getAccessToken(KeycloakSession keycloakSession) {
+ AppAuthManager authManager = new AppAuthManager();
+ KeycloakContext context = keycloakSession.getContext();
+ AuthResult authResult = authManager.authenticateBearerToken(keycloakSession, context.getRealm(), context.getUri(), context.getConnection(), context.getRequestHeaders());
+
+ if (authResult != null) {
+ return authResult.getToken();
+ }
+
+ return null;
+ }
+
+ public static String getAccessTokenAsString(KeycloakSession keycloakSession) {
+ AppAuthManager authManager = new AppAuthManager();
+
+ return authManager.extractAuthorizationHeaderToken(keycloakSession.getContext().getRequestHeaders());
+ }
+
+ public static boolean verifySignature(String token, PublicKey publicKey) {
+ try {
+ JWSInput jws = new JWSInput(token);
+
+ return RSAProvider.verify(jws, publicKey);
+ } catch (Exception e) {
+ throw new ErrorResponseException("invalid_signature", "Unexpected error while validating signature.", Status.INTERNAL_SERVER_ERROR);
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
new file mode 100644
index 0000000..0378765
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.protocol.oidc;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.keycloak.RSATokenVerifier;
+import org.keycloak.common.VerificationException;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.services.ErrorResponseException;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvider {
+
+ private final KeycloakSession session;
+ private final TokenManager tokenManager;
+ private final RealmModel realm;
+
+ public AccessTokenIntrospectionProvider(KeycloakSession session) {
+ this.session = session;
+ this.realm = session.getContext().getRealm();
+ this.tokenManager = new TokenManager();
+ }
+
+ public Response introspect(String token) {
+ try {
+ AccessToken toIntrospect = toAccessToken(token);
+ RealmModel realm = this.session.getContext().getRealm();
+ ObjectNode tokenMetadata;
+
+ boolean active = tokenManager.isTokenValid(session, realm, toIntrospect);
+
+ if (active) {
+ tokenMetadata = JsonSerialization.createObjectNode(toIntrospect);
+ tokenMetadata.put("client_id", toIntrospect.getIssuedFor());
+ tokenMetadata.put("username", toIntrospect.getPreferredUsername());
+ } else {
+ tokenMetadata = JsonSerialization.createObjectNode();
+ }
+
+ tokenMetadata.put("active", active);
+
+ return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).type(MediaType.APPLICATION_JSON_TYPE).build();
+ } catch (Exception e) {
+ throw new RuntimeException("Error creating token introspection response.", e);
+ }
+ }
+
+ protected AccessToken toAccessToken(String token) {
+ try {
+ return RSATokenVerifier.toAccessToken(token, realm.getPublicKey());
+ } catch (VerificationException e) {
+ throw new ErrorResponseException("invalid_request", "Invalid token.", Response.Status.UNAUTHORIZED);
+ }
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProviderFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProviderFactory.java
new file mode 100644
index 0000000..19c9f18
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProviderFactory.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.protocol.oidc;
+
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AccessTokenIntrospectionProviderFactory implements TokenIntrospectionProviderFactory {
+
+ public static final String ACCESS_TOKEN_TYPE = "access_token";
+
+ @Override
+ public TokenIntrospectionProvider create(KeycloakSession session) {
+ return new AccessTokenIntrospectionProvider(session);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+
+ }
+
+ @Override
+ public void close() {
+
+ }
+
+ @Override
+ public String getId() {
+ return ACCESS_TOKEN_TYPE;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 1ff3da9..4f17cdd 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -132,7 +132,7 @@ public class TokenEndpoint {
@Path("introspect")
public Object introspect() {
- TokenIntrospectionEndpoint tokenIntrospectionEndpoint = new TokenIntrospectionEndpoint(this.realm, this.tokenManager, this.event);
+ TokenIntrospectionEndpoint tokenIntrospectionEndpoint = new TokenIntrospectionEndpoint(this.realm, this.event);
ResteasyProviderFactory.getInstance().injectProperties(tokenIntrospectionEndpoint);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenIntrospectionEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenIntrospectionEndpoint.java
index 323a6ff..f6c59c8 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenIntrospectionEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenIntrospectionEndpoint.java
@@ -16,26 +16,19 @@
*/
package org.keycloak.protocol.oidc.endpoints;
-import com.fasterxml.jackson.databind.node.ObjectNode;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
-import org.keycloak.OAuthErrorException;
-import org.keycloak.RSATokenVerifier;
import org.keycloak.common.ClientConnection;
-import org.keycloak.common.VerificationException;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
-import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
-import org.keycloak.protocol.oidc.TokenManager;
-import org.keycloak.protocol.oidc.TokenManager.TokenValidation;
+import org.keycloak.protocol.oidc.AccessTokenIntrospectionProviderFactory;
+import org.keycloak.protocol.oidc.TokenIntrospectionProvider;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
-import org.keycloak.representations.AccessToken;
import org.keycloak.services.ErrorResponseException;
-import org.keycloak.util.JsonSerialization;
import javax.ws.rs.POST;
import javax.ws.rs.core.Context;
@@ -52,8 +45,6 @@ import javax.ws.rs.core.UriInfo;
*/
public class TokenIntrospectionEndpoint {
- private static final String TOKEN_TYPE_ACCESS_TOKEN = "access_token";
- private static final String TOKEN_TYPE_REFRESH_TOKEN = "refresh_token";
private static final String PARAM_TOKEN_TYPE_HINT = "token_type_hint";
private static final String PARAM_TOKEN = "token";
@@ -72,12 +63,10 @@ public class TokenIntrospectionEndpoint {
private ClientConnection clientConnection;
private final RealmModel realm;
- private final TokenManager tokenManager;
private final EventBuilder event;
- public TokenIntrospectionEndpoint(RealmModel realm, TokenManager tokenManager, EventBuilder event) {
+ public TokenIntrospectionEndpoint(RealmModel realm, EventBuilder event) {
this.realm = realm;
- this.tokenManager = tokenManager;
this.event = event;
}
@@ -94,7 +83,7 @@ public class TokenIntrospectionEndpoint {
String tokenTypeHint = formParams.getFirst(PARAM_TOKEN_TYPE_HINT);
if (tokenTypeHint == null) {
- tokenTypeHint = TOKEN_TYPE_ACCESS_TOKEN;
+ tokenTypeHint = AccessTokenIntrospectionProviderFactory.ACCESS_TOKEN_TYPE;
}
String token = formParams.getFirst(PARAM_TOKEN);
@@ -103,39 +92,26 @@ public class TokenIntrospectionEndpoint {
throw throwErrorResponseException(Errors.INVALID_REQUEST, "Token not provided.", Status.BAD_REQUEST);
}
+ TokenIntrospectionProvider provider = this.session.getProvider(TokenIntrospectionProvider.class, tokenTypeHint);
+
+ if (provider == null) {
+ throw throwErrorResponseException(Errors.INVALID_REQUEST, "Unsupported token type [" + tokenTypeHint + "].", Status.BAD_REQUEST);
+ }
+
try {
- AccessToken toIntrospect = toAccessToken(tokenTypeHint, token);
- ObjectNode tokenMetadata;
-
- boolean active = tokenManager.isTokenValid(session, realm, toIntrospect);
- if (active) {
- tokenMetadata = JsonSerialization.createObjectNode(toIntrospect);
- tokenMetadata.put("client_id", toIntrospect.getIssuedFor());
- tokenMetadata.put("username", toIntrospect.getPreferredUsername());
- } else {
- tokenMetadata = JsonSerialization.createObjectNode();
- }
- tokenMetadata.put("active", active);
+ Response response = provider.introspect(token);
this.event.success();
- return Response.ok(JsonSerialization.writeValueAsBytes(tokenMetadata)).build();
+ return response;
+ } catch (ErrorResponseException ere) {
+ throw ere;
} catch (Exception e) {
throw throwErrorResponseException(Errors.INVALID_REQUEST, "Failed to introspect token.", Status.BAD_REQUEST);
}
}
- private AccessToken toAccessToken(String tokenTypeHint, String token) throws JWSInputException, OAuthErrorException {
- if (TOKEN_TYPE_ACCESS_TOKEN.equals(tokenTypeHint)) {
- return toAccessToken(token);
- } else if (TOKEN_TYPE_REFRESH_TOKEN.equals(tokenTypeHint)) {
- return this.tokenManager.toRefreshToken(this.realm, token);
- } else {
- throw throwErrorResponseException(Errors.INVALID_REQUEST, "Unsupported token type [" + tokenTypeHint + "].", Status.BAD_REQUEST);
- }
- }
-
private void authorizeClient() {
try {
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient();
@@ -153,14 +129,6 @@ public class TokenIntrospectionEndpoint {
}
}
- private AccessToken toAccessToken(String tokenString) {
- try {
- return RSATokenVerifier.toAccessToken(tokenString, realm.getPublicKey());
- } catch (VerificationException e) {
- throw new ErrorResponseException("invalid_request", "Invalid token.", Status.UNAUTHORIZED);
- }
- }
-
private void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
throw new ErrorResponseException("invalid_request", "HTTPS required", Status.FORBIDDEN);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
index f3f502d..a7781de 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java
@@ -20,12 +20,16 @@ package org.keycloak.protocol.oidc.installation;
import org.keycloak.Config;
import org.keycloak.authentication.ClientAuthenticator;
import org.keycloak.authentication.ClientAuthenticatorFactory;
+import org.keycloak.authorization.admin.AuthorizationService;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
import org.keycloak.protocol.ClientInstallationProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.util.JsonSerialization;
@@ -34,6 +38,7 @@ import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
+import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -59,6 +64,9 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide
Map<String, Object> adapterConfig = getClientCredentialsAdapterConfig(session, client);
rep.setCredentials(adapterConfig);
}
+
+ configureAuthorizationSettings(session, client, rep);
+
String json = null;
try {
json = JsonSerialization.writeValueAsPrettyString(rep);
@@ -143,4 +151,22 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide
return MediaType.APPLICATION_JSON;
}
+ private void configureAuthorizationSettings(KeycloakSession session, ClientModel client, ClientManager.InstallationAdapterConfig rep) {
+ if (new AuthorizationService(session, client, null).isEnabled()) {
+ PolicyEnforcerConfig enforcerConfig = new PolicyEnforcerConfig();
+
+ enforcerConfig.setEnforcementMode(null);
+ enforcerConfig.setPaths(null);
+
+ rep.setEnforcerConfig(enforcerConfig);
+
+ Set<RoleModel> clientRoles = client.getRoles();
+
+ if (clientRoles.size() == 1) {
+ if (clientRoles.iterator().next().getName().equals(Constants.AUTHZ_UMA_PROTECTION)) {
+ rep.setUseResourceRoleMappings(null);
+ }
+ }
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProvider.java
new file mode 100644
index 0000000..23c675b
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProvider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.protocol.oidc;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RefreshTokenIntrospectionProvider extends AccessTokenIntrospectionProvider {
+
+ public RefreshTokenIntrospectionProvider(KeycloakSession session) {
+ super(session);
+ }
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProviderFactory.java b/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProviderFactory.java
new file mode 100644
index 0000000..9f5ef76
--- /dev/null
+++ b/services/src/main/java/org/keycloak/protocol/oidc/RefreshTokenIntrospectionProviderFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package org.keycloak.protocol.oidc;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class RefreshTokenIntrospectionProviderFactory extends AccessTokenIntrospectionProviderFactory {
+
+ private static final String REFRESH_TOKEN_TYPE = "refresh_token";
+
+ @Override
+ public TokenIntrospectionProvider create(KeycloakSession session) {
+ return new RefreshTokenIntrospectionProvider(session);
+ }
+
+ @Override
+ public String getId() {
+ return REFRESH_TOKEN_TYPE;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java
index 282fd31..40c1ac6 100755
--- a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java
+++ b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java
@@ -23,6 +23,8 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakTransaction;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
@@ -87,15 +89,46 @@ public class KeycloakSessionServletFilter implements Filter {
try {
filterChain.doFilter(servletRequest, servletResponse);
} finally {
- // KeycloakTransactionCommitter is responsible for committing the transaction, but if an exception is thrown it's not invoked and transaction
- // should be rolled back
- if (session.getTransaction() != null && session.getTransaction().isActive()) {
- session.getTransaction().rollback();
+ if (servletRequest.isAsyncStarted()) {
+ servletRequest.getAsyncContext().addListener(createAsyncLifeCycleListener(session));
+ } else {
+ closeSession(session);
+ }
+ }
+ }
+
+ private AsyncListener createAsyncLifeCycleListener(final KeycloakSession session) {
+ return new AsyncListener() {
+ @Override
+ public void onComplete(AsyncEvent event) {
+ closeSession(session);
}
- session.close();
- ResteasyProviderFactory.clearContextData();
+ @Override
+ public void onTimeout(AsyncEvent event) {
+ closeSession(session);
+ }
+
+ @Override
+ public void onError(AsyncEvent event) {
+ closeSession(session);
+ }
+
+ @Override
+ public void onStartAsync(AsyncEvent event) {
+ }
+ };
+ }
+
+ private void closeSession(KeycloakSession session) {
+ // KeycloakTransactionCommitter is responsible for committing the transaction, but if an exception is thrown it's not invoked and transaction
+ // should be rolled back
+ if (session.getTransaction() != null && session.getTransaction().isActive()) {
+ session.getTransaction().rollback();
}
+
+ session.close();
+ ResteasyProviderFactory.clearContextData();
}
@Override
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
index a8120e9..607123e 100644
--- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java
@@ -35,6 +35,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.adapters.config.BaseRealmConfig;
import org.keycloak.common.util.Time;
+import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ServicesLogger;
@@ -207,6 +208,8 @@ public class ClientManager {
protected Boolean publicClient;
@JsonProperty("credentials")
protected Map<String, Object> credentials;
+ @JsonProperty("policy-enforcer")
+ protected PolicyEnforcerConfig enforcerConfig;
public Boolean isUseResourceRoleMappings() {
return useResourceRoleMappings;
@@ -247,6 +250,14 @@ public class ClientManager {
public void setBearerOnly(Boolean bearerOnly) {
this.bearerOnly = bearerOnly;
}
+
+ public PolicyEnforcerConfig getEnforcerConfig() {
+ return this.enforcerConfig;
+ }
+
+ public void setEnforcerConfig(PolicyEnforcerConfig enforcerConfig) {
+ this.enforcerConfig = enforcerConfig;
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 4ff5588..3926447 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -47,7 +47,6 @@ import org.keycloak.representations.idm.OAuthClientRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
-import org.keycloak.timer.TimerProvider;
import java.util.Collections;
import java.util.HashSet;
@@ -120,6 +119,7 @@ public class RealmManager implements RealmImporter {
setupAuthenticationFlows(realm);
setupRequiredActions(realm);
setupOfflineTokens(realm);
+ setupAuthorizationServices(realm);
return realm;
}
@@ -489,6 +489,9 @@ public class RealmManager implements RealmImporter {
for (final UserFederationProviderModel fedProvider : federationProviders) {
usersSyncManager.notifyToRefreshPeriodicSync(session, realm, fedProvider, false);
}
+
+ setupAuthorizationServices(realm);
+
return realm;
}
@@ -581,4 +584,14 @@ public class RealmManager implements RealmImporter {
return session.users().searchForUser(searchString.trim(), realmModel);
}
+ private void setupAuthorizationServices(RealmModel realm) {
+ for (String roleName : Constants.AUTHZ_DEFAULT_AUTHORIZATION_ROLES) {
+ if (realm.getRole(roleName) == null) {
+ RoleModel role = realm.addRole(roleName);
+ role.setDescription("${role_" + roleName + "}");
+ role.setScopeParamRequired(false);
+ realm.addDefaultRole(roleName);
+ }
+ }
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 08177b9..18023e8 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -20,6 +20,7 @@ import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.authorization.admin.AuthorizationService;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
@@ -133,12 +134,18 @@ public class ClientResource {
}
}
- public static void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException {
+ public void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException {
if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) {
new ClientManager(new RealmManager(session)).enableServiceAccount(client);
}
RepresentationToModel.updateClient(rep, client);
+
+ if (TRUE.equals(rep.getAuthorizationServicesEnabled())) {
+ authorization().enable();
+ } else {
+ authorization().disable();
+ }
}
/**
@@ -156,7 +163,11 @@ public class ClientResource {
throw new NotFoundException("Could not find client");
}
- return ModelToRepresentation.toRepresentation(client);
+ ClientRepresentation representation = ModelToRepresentation.toRepresentation(client);
+
+ representation.setAuthorizationServicesEnabled(authorization().isEnabled());
+
+ return representation;
}
/**
@@ -537,4 +548,12 @@ public class ClientResource {
return result;
}
+ @Path("/authz")
+ public AuthorizationService authorization() {
+ AuthorizationService resource = new AuthorizationService(this.session, this.client, this.auth);
+
+ ResteasyProviderFactory.getInstance().injectProperties(resource);
+
+ return resource;
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAuth.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAuth.java
index 440241d..176c480 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAuth.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAuth.java
@@ -31,7 +31,7 @@ public class RealmAuth {
private Resource resource;
public enum Resource {
- CLIENT, USER, REALM, EVENTS, IDENTITY_PROVIDER, IMPERSONATION
+ CLIENT, USER, REALM, EVENTS, IDENTITY_PROVIDER, IMPERSONATION, AUTHORIZATION
}
private AdminAuth auth;
@@ -89,6 +89,8 @@ public class RealmAuth {
return AdminRoles.VIEW_EVENTS;
case IDENTITY_PROVIDER:
return AdminRoles.VIEW_IDENTITY_PROVIDERS;
+ case AUTHORIZATION:
+ return AdminRoles.VIEW_AUTHORIZATION;
default:
throw new IllegalStateException();
}
@@ -108,6 +110,8 @@ public class RealmAuth {
return AdminRoles.MANAGE_IDENTITY_PROVIDERS;
case IMPERSONATION:
return ImpersonationConstants.IMPERSONATION_ROLE;
+ case AUTHORIZATION:
+ return AdminRoles.MANAGE_AUTHORIZATION;
default:
throw new IllegalStateException();
}
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 4f1c5f7..4161763 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -18,6 +18,8 @@ package org.keycloak.services.resources;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.AuthorizationService;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.events.EventBuilder;
@@ -40,8 +42,12 @@ import javax.ws.rs.NotFoundException;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
-import javax.ws.rs.core.*;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
import java.net.URI;
/**
@@ -242,6 +248,17 @@ public class RealmsResource {
return Cors.add(request, responseBuilder).allowedOrigins("*").build();
}
+ @Path("{realm}/authz")
+ public Object getAuthorizationService(@PathParam("realm") String name) {
+ init(name);
+ AuthorizationProvider authorization = this.session.getProvider(AuthorizationProvider.class);
+ AuthorizationService service = new AuthorizationService(authorization);
+
+ ResteasyProviderFactory.getInstance().injectProperties(service);
+
+ return service;
+ }
+
/**
* A JAX-RS sub-resource locator that uses the {@link org.keycloak.services.resource.RealmResourceSPI} to resolve sub-resources instances given an <code>unknownPath</code>.
*
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authorization.AuthorizationProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.authorization.AuthorizationProviderFactory
new file mode 100644
index 0000000..3ffe34c
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authorization.AuthorizationProviderFactory
@@ -0,0 +1,19 @@
+#
+# JBoss, Home of Professional Open Source.
+# Copyright 2016 Red Hat, Inc., and individual contributors
+# as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.authorization.DefaultAuthorizationProviderFactory
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.TokenIntrospectionProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.TokenIntrospectionProviderFactory
new file mode 100644
index 0000000..ae45923
--- /dev/null
+++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.oidc.TokenIntrospectionProviderFactory
@@ -0,0 +1,21 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+
+org.keycloak.protocol.oidc.AccessTokenIntrospectionProviderFactory
+org.keycloak.protocol.oidc.RefreshTokenIntrospectionProviderFactory
+org.keycloak.authorization.protection.introspect.RPTIntrospectionProviderFactory
\ No newline at end of file
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory
index afc490e..df3dd7a 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.wellknown.WellKnownProviderFactory
@@ -15,4 +15,5 @@
# limitations under the License.
#
-org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory
\ No newline at end of file
+org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory
+org.keycloak.authorization.config.UmaWellKnownProviderFactory
\ No newline at end of file
testsuite/integration/pom.xml 7(+7 -0)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index 362d749..d394e2a 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -29,6 +29,11 @@
<name>Keycloak Integration TestSuite</name>
<description />
+ <properties>
+ <maven.compiler.target>1.8</maven.compiler.target>
+ <maven.compiler.source>1.8</maven.compiler.source>
+ </properties>
+
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
@@ -400,6 +405,7 @@
<keycloak.user.provider>jpa</keycloak.user.provider>
<keycloak.userSessionPersister.provider>jpa</keycloak.userSessionPersister.provider>
<keycloak.eventsStore.provider>jpa</keycloak.eventsStore.provider>
+ <keycloak.authorization.provider>jpa</keycloak.authorization.provider>
<keycloak.liquibase.logging.level>debug</keycloak.liquibase.logging.level>
</systemPropertyVariables>
@@ -439,6 +445,7 @@
<keycloak.user.provider>mongo</keycloak.user.provider>
<keycloak.userSessionPersister.provider>mongo</keycloak.userSessionPersister.provider>
<keycloak.eventsStore.provider>mongo</keycloak.eventsStore.provider>
+ <keycloak.authorization.provider>mongo</keycloak.authorization.provider>
<keycloak.connectionsMongo.host>${keycloak.connectionsMongo.host}</keycloak.connectionsMongo.host>
<keycloak.connectionsMongo.port>${keycloak.connectionsMongo.port}</keycloak.connectionsMongo.port>
<keycloak.connectionsMongo.db>${keycloak.connectionsMongo.db}</keycloak.connectionsMongo.db>
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java
index 19da666..6c10322 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adduser/AddUserTest.java
@@ -107,7 +107,7 @@ public class AddUserTest {
List<RoleRepresentation> realmRoles = userResource.roles().realmLevel().listAll();
- assertRoles(realmRoles, "admin", "offline_access");
+ assertRoles(realmRoles, "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
List<ClientRepresentation> clients = realm.clients().findAll();
String accountId = null;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractAuthorizationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractAuthorizationTest.java
new file mode 100644
index 0000000..301adca
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractAuthorizationTest.java
@@ -0,0 +1,127 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.authorization;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakTransactionManager;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.AccessTokenResponse;
+
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.Invocation;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MASTER;
+import static org.keycloak.models.AdminRoles.ADMIN;
+import static org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractAuthorizationTest {
+
+ protected static final String TEST_REALM_NAME = "photoz";
+
+ @Rule
+ public KeycloakAuthorizationServerRule keycloak = new KeycloakAuthorizationServerRule(TEST_REALM_NAME);
+
+ private Keycloak adminClient;
+
+ @Before
+ public void onBefore() {
+ adminClient = Keycloak.getInstance(AUTH_SERVER_ROOT, MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
+ }
+
+ protected <R> R onAuthorizationSession(Function<AuthorizationProvider, R> function) {
+ KeycloakSession keycloakSession = startKeycloakSession();
+ KeycloakTransactionManager transaction = keycloakSession.getTransaction();
+
+ try {
+ AuthorizationProvider authorizationProvider = keycloakSession.getProvider(AuthorizationProvider.class);
+
+ R result = function.apply(authorizationProvider);
+
+ transaction.commit();
+
+ return result;
+ } catch (Exception e) {
+ transaction.rollback();
+ throw new RuntimeException(e);
+ } finally {
+ if (keycloakSession != null) {
+ keycloakSession.close();
+ }
+ }
+ }
+
+ protected void onAuthorizationSession(Consumer<AuthorizationProvider> consumer) {
+ KeycloakSession keycloakSession = startKeycloakSession();
+ KeycloakTransactionManager transaction = keycloakSession.getTransaction();
+
+ try {
+ AuthorizationProvider authorizationProvider = keycloakSession.getProvider(AuthorizationProvider.class);
+
+ consumer.accept(authorizationProvider);
+
+ transaction.commit();
+ } catch (Exception e) {
+ transaction.rollback();
+ throw new RuntimeException(e);
+ } finally {
+ if (keycloakSession != null) {
+ keycloakSession.close();
+ }
+ }
+ }
+
+ protected Invocation.Builder newClient(ClientModel client, String authzRelativePath) {
+ return ClientBuilder.newClient()
+ .register((ClientRequestFilter) requestContext -> {
+ AccessTokenResponse accessToken = adminClient.tokenManager().getAccessToken();
+ requestContext.getHeaders().add("Authorization", "Bearer " + accessToken.getToken());
+ }).target(AUTH_SERVER_ROOT + "/admin/realms/" + TEST_REALM_NAME + "/clients/" + client.getId() + "/authz" + authzRelativePath).request();
+ }
+
+ protected ClientModel getClientByClientId(String clientId) {
+ KeycloakSession session = this.keycloak.startSession();
+
+ try {
+ RealmModel realm = session.realms().getRealmByName(TEST_REALM_NAME);
+ return realm.getClientByClientId(clientId);
+ } finally {
+ session.close();
+ }
+ }
+
+ private KeycloakSession startKeycloakSession() {
+ KeycloakSession keycloakSession = this.keycloak.startSession();
+
+ keycloakSession.getContext().setRealm(keycloakSession.realms().getRealmByName(TEST_REALM_NAME));
+
+ return keycloakSession;
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java
new file mode 100644
index 0000000..8876f30
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java
@@ -0,0 +1,370 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.authorization;
+
+import org.apache.commons.collections.map.HashedMap;
+import org.jboss.resteasy.spi.ResteasyProviderFactory;
+import org.junit.Before;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.Decision;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.common.KeycloakEvaluationContext;
+import org.keycloak.authorization.common.KeycloakIdentity;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.model.ResourceServer;
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
+import org.keycloak.authorization.store.PolicyStore;
+import org.keycloak.authorization.store.ResourceServerStore;
+import org.keycloak.authorization.store.ResourceStore;
+import org.keycloak.authorization.store.ScopeStore;
+import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.common.ClientConnection;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.core.Cookie;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public abstract class AbstractPhotozAdminTest extends AbstractAuthorizationTest {
+
+ protected ResourceServer resourceServer;
+ protected Resource adminResource;
+ protected Policy anyAdminPolicy;
+ protected Policy onlyFromSpecificAddressPolicy;
+ protected Policy administrationPolicy;
+
+ protected Resource albumResource;
+ protected Policy anyUserPolicy;
+
+ @Before
+ public void onBefore() {
+ super.onBefore();
+ this.resourceServer = createResourceServer();
+ this.adminResource = createAdminAlbumResource();
+ this.anyAdminPolicy = createAnyAdminPolicy();
+ this.onlyFromSpecificAddressPolicy = createOnlyFromSpecificAddressPolicy();
+ this.administrationPolicy = createAdministrationPolicy();
+
+ this.albumResource = createAlbumResource();
+ this.anyUserPolicy = createAnyUserPolicy();
+ }
+
+ protected ResourceServer createResourceServer() {
+ return onAuthorizationSession(authorizationProvider -> {
+ StoreFactory storeFactory = authorizationProvider.getStoreFactory();
+ ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore();
+
+ return resourceServerStore.create(getClientByClientId("photoz-restful-api").getId());
+ });
+ }
+
+ protected Map<String, DefaultEvaluation> performEvaluation(List<ResourcePermission> permissions, AccessToken accessToken, ClientConnection clientConnection) {
+ Map<String, DefaultEvaluation> evaluations = new HashedMap();
+
+ onAuthorizationSession(authorizationProvider -> {
+ StoreFactory storeFactory = authorizationProvider.getStoreFactory();
+
+ // during tests we create resource instances, but we need to reload them to get their collections updated
+ List<ResourcePermission> updatedPermissions = permissions.stream().map(permission -> {
+ Resource resource = storeFactory.getResourceStore().findById(permission.getResource().getId());
+ return new ResourcePermission(resource, permission.getScopes(), permission.getResourceServer());
+ }).collect(Collectors.toList());
+
+ authorizationProvider.evaluators().from(updatedPermissions, createEvaluationContext(accessToken, clientConnection, authorizationProvider)).evaluate(new Decision<DefaultEvaluation>() {
+ @Override
+ public void onDecision(DefaultEvaluation evaluation) {
+ evaluations.put(evaluation.getPolicy().getId(), evaluation);
+ }
+
+ @Override
+ public void onError(Throwable cause) {
+ throw new RuntimeException("Permission evaluation failed.", cause);
+ }
+ });
+ });
+
+ return evaluations;
+ }
+
+ private KeycloakEvaluationContext createEvaluationContext(AccessToken accessToken, ClientConnection clientConnection, AuthorizationProvider authorizationProvider) {
+ KeycloakSession keycloakSession = authorizationProvider.getKeycloakSession();
+
+ keycloakSession.getContext().setConnection(clientConnection);
+
+ keycloakSession.getContext().setClient(getClientByClientId("photoz-html5-client"));
+
+ ResteasyProviderFactory.pushContext(HttpHeaders.class, createHttpHeaders());
+
+ KeycloakIdentity identity = new KeycloakIdentity(accessToken, keycloakSession);
+
+ return new KeycloakEvaluationContext(identity, keycloakSession);
+ }
+
+ protected AccessToken createAccessToken(Set<String> roles) {
+ AccessToken accessToken = new AccessToken();
+
+ accessToken.setRealmAccess(new AccessToken.Access());
+ accessToken.getRealmAccess().roles(roles);
+
+ return accessToken;
+ }
+
+
+ private HttpHeaders createHttpHeaders() {
+ return new HttpHeaders() {
+ @Override
+ public List<String> getRequestHeader(String name) {
+ return null;
+ }
+
+ @Override
+ public String getHeaderString(String name) {
+ return null;
+ }
+
+ @Override
+ public MultivaluedMap<String, String> getRequestHeaders() {
+ return null;
+ }
+
+ @Override
+ public List<MediaType> getAcceptableMediaTypes() {
+ return null;
+ }
+
+ @Override
+ public List<Locale> getAcceptableLanguages() {
+ return null;
+ }
+
+ @Override
+ public MediaType getMediaType() {
+ return null;
+ }
+
+ @Override
+ public Locale getLanguage() {
+ return null;
+ }
+
+ @Override
+ public Map<String, Cookie> getCookies() {
+ return null;
+ }
+
+ @Override
+ public Date getDate() {
+ return null;
+ }
+
+ @Override
+ public int getLength() {
+ return 0;
+ }
+ };
+ }
+
+ protected ClientConnection createClientConnection(String remoteAddr) {
+ return new ClientConnection() {
+ @Override
+ public String getRemoteAddr() {
+ return remoteAddr;
+ }
+
+ @Override
+ public String getRemoteHost() {
+ return "localhost";
+ }
+
+ @Override
+ public int getRemotePort() {
+ return 0;
+ }
+
+ @Override
+ public String getLocalAddr() {
+ return null;
+ }
+
+ @Override
+ public int getLocalPort() {
+ return 0;
+ }
+ };
+ }
+
+ protected Invocation.Builder newPermissionRequest(String... id) {
+ String idPathParam = "";
+
+ if (id.length != 0) {
+ idPathParam = "/" + id[0];
+ }
+
+ return newClient(getClientByClientId("photoz-restful-api"), "/resource-server/policy" + idPathParam);
+ }
+
+ private Policy createAdministrationPolicy() {
+ return onAuthorizationSession(authorizationProvider -> {
+ StoreFactory storeFactory = authorizationProvider.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ Policy policy = policyStore.create("Administration Policy", "aggregate", resourceServer);
+
+ policy.addAssociatedPolicy(anyAdminPolicy);
+ policy.addAssociatedPolicy(onlyFromSpecificAddressPolicy);
+
+ return policy;
+ });
+ }
+
+ private Policy createOnlyFromSpecificAddressPolicy() {
+ return onAuthorizationSession(authorizationProvider -> {
+ StoreFactory storeFactory = authorizationProvider.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ Policy policy = policyStore.create("Only From a Specific Client Address", "js", resourceServer);
+ HashedMap config = new HashedMap();
+
+ config.put("code",
+ "var contextAttributes = $evaluation.getContext().getAttributes();" +
+ "var networkAddress = contextAttributes.getValue('kc.authz.context.client.network.ip_address');" +
+ "if ('127.0.0.1'.equals(networkAddress.asInetAddress(0).getHostAddress())) {" +
+ "$evaluation.grant();" +
+ "}");
+
+ policy.setConfig(config);
+
+ return policy;
+ });
+ }
+
+ private Policy createAnyAdminPolicy() {
+ return onAuthorizationSession(authorizationProvider -> {
+ StoreFactory storeFactory = authorizationProvider.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ Policy policy = policyStore.create("Any Admin Policy", "role", resourceServer);
+ HashedMap config = new HashedMap();
+ RealmModel realm = authorizationProvider.getKeycloakSession().realms().getRealmByName(TEST_REALM_NAME);
+ RoleModel adminRole = realm.getRole("admin");
+
+ try {
+ config.put("roles", JsonSerialization.writeValueAsString(new String[] {adminRole.getId()}));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ policy.setConfig(config);
+
+ return policy;
+ });
+ }
+
+ private Resource createAdminAlbumResource() {
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setName("Admin Resources");
+ representation.setType("http://photoz.com/admin");
+ representation.setUri("/admin/*");
+
+ HashSet<ScopeRepresentation> scopes = new HashSet<>();
+
+ scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:admin:manage"));
+
+ representation.setScopes(scopes);
+
+ return createResource(representation);
+ }
+
+ private Resource createAlbumResource() {
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setName("Album Resource");
+ representation.setType("http://photoz.com/album");
+ representation.setUri("/album/*");
+
+ HashSet<ScopeRepresentation> scopes = new HashSet<>();
+
+ scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:view"));
+ scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:create"));
+ scopes.add(new ScopeRepresentation("urn:photoz.com:scopes:album:delete"));
+
+ representation.setScopes(scopes);
+
+ return createResource(representation);
+ }
+
+ protected Resource createResource(ResourceRepresentation representation) {
+ return onAuthorizationSession(authorizationProvider -> {
+ StoreFactory storeFactory = authorizationProvider.getStoreFactory();
+ ScopeStore scopeStore = storeFactory.getScopeStore();
+
+ representation.getScopes().forEach(scopeRepresentation -> {
+ scopeStore.create(scopeRepresentation.getName(), resourceServer);
+ });
+
+ ResourceStore resourceStore = storeFactory.getResourceStore();
+ Resource albumResource = resourceStore.create(representation.getName(), resourceServer, resourceServer.getId());
+
+ albumResource.setType(representation.getType());
+ albumResource.setUri(representation.getUri());
+ albumResource.setIconUri(representation.getIconUri());
+
+ return albumResource;
+ });
+ }
+
+ private Policy createAnyUserPolicy() {
+ return onAuthorizationSession(authorizationProvider -> {
+ StoreFactory storeFactory = authorizationProvider.getStoreFactory();
+ PolicyStore policyStore = storeFactory.getPolicyStore();
+ Policy policy = policyStore.create("Any User Policy", "role", resourceServer);
+ HashedMap config = new HashedMap();
+ RealmModel realm = authorizationProvider.getKeycloakSession().realms().getRealmByName(TEST_REALM_NAME);
+ RoleModel adminRole = realm.getRole("user");
+
+ try {
+ config.put("roles", JsonSerialization.writeValueAsString(new String[] {adminRole.getId()}));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ policy.setConfig(config);
+
+ return policy;
+ });
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AttributeTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AttributeTest.java
new file mode 100644
index 0000000..86e1ba0
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AttributeTest.java
@@ -0,0 +1,73 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.authorization;
+
+import org.apache.commons.collections.map.HashedMap;
+import org.junit.Test;
+import org.keycloak.authorization.attribute.Attributes;
+
+import java.net.InetAddress;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class AttributeTest {
+
+ @Test
+ public void testManageAttributes() throws ParseException {
+ Map<String, Collection<String>> map = new HashedMap();
+
+ map.put("integer", asList("1"));
+ map.put("long", asList("" + Long.MAX_VALUE));
+ map.put("string", asList("some string"));
+ map.put("date", asList("12/12/2016"));
+ map.put("ip_network_address", asList("127.0.0.1"));
+ map.put("host_network_address", asList("localhost"));
+ map.put("multi_valued", asList("1", "2", "3", "4"));
+
+ Attributes attributes = Attributes.from(map);
+
+ map.keySet().forEach(new Consumer<String>() {
+ @Override
+ public void accept(String name) {
+ assertTrue(attributes.exists(name));
+ }
+ });
+
+ assertFalse(attributes.exists("not_found"));
+ assertTrue(attributes.containsValue("integer", "1"));
+ assertTrue(attributes.containsValue("multi_valued", "3"));
+
+ assertEquals(1, attributes.getValue("multi_valued").asInt(0));
+ assertEquals(4, attributes.getValue("multi_valued").asInt(3));
+
+ assertEquals(new SimpleDateFormat("dd/MM/yyyy").parse("12/12/2016"), attributes.getValue("date").asDate(0, "dd/MM/yyyy"));
+
+ assertEquals(InetAddress.getLoopbackAddress(), attributes.getValue("ip_network_address").asInetAddress(0));
+ assertEquals(InetAddress.getLoopbackAddress(), attributes.getValue("host_network_address").asInetAddress(0));
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/KeycloakAuthorizationServerRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/KeycloakAuthorizationServerRule.java
new file mode 100644
index 0000000..1df0b61
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/KeycloakAuthorizationServerRule.java
@@ -0,0 +1,47 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.authorization;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class KeycloakAuthorizationServerRule extends AbstractKeycloakRule {
+
+ private final String realmName;
+
+ KeycloakAuthorizationServerRule(String realmName) {
+ this.realmName = realmName;
+ }
+
+ @Override
+ protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
+ server.importRealm(getClass().getResourceAsStream("/authorization-test/test-" + realmName + "-realm.json"));
+ }
+
+ @Override
+ protected String[] getTestRealms() {
+ return new String[] {this.realmName};
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java
new file mode 100644
index 0000000..f323265
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourceManagementTest.java
@@ -0,0 +1,163 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.authorization;
+
+import org.junit.Test;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.authorization.model.Resource;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation.Builder;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourceManagementTest extends AbstractPhotozAdminTest {
+
+ @Test
+ public void testCreate() throws Exception {
+ ResourceRepresentation newResource = new ResourceRepresentation();
+
+ newResource.setName("New Resource");
+ newResource.setType("Resource Type");
+ newResource.setIconUri("Resource Icon URI");
+ newResource.setUri("Resource URI");
+
+ Response response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class);
+
+ onAuthorizationSession(authorizationProvider -> {
+ Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId());
+
+ assertNotNull(resourceModel);
+ assertEquals(resource.getId(), resourceModel.getId());
+ assertEquals("New Resource", resourceModel.getName());
+ assertEquals("Resource Type", resourceModel.getType());
+ assertEquals("Resource Icon URI", resourceModel.getIconUri());
+ assertEquals("Resource URI", resourceModel.getUri());
+ assertEquals(resourceServer.getClientId(), resourceModel.getOwner());
+ assertEquals(resourceServer.getId(), resourceModel.getResourceServer().getId());
+ });
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ ResourceRepresentation newResource = new ResourceRepresentation();
+
+ newResource.setName("New Resource");
+ newResource.setType("Resource Type");
+ newResource.setIconUri("Resource Icon URI");
+ newResource.setUri("Resource URI");
+
+ Response response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ newResource.setName("New Resource Changed");
+ newResource.setType("Resource Type Changed");
+ newResource.setIconUri("Resource Icon URI Changed");
+ newResource.setUri("Resource URI Changed");
+
+ response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE));
+
+ ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class);
+
+ onAuthorizationSession(authorizationProvider -> {
+ Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId());
+
+ assertNotNull(resourceModel);
+ assertEquals(resource.getId(), resourceModel.getId());
+ assertEquals("New Resource Changed", resourceModel.getName());
+ assertEquals("Resource Type Changed", resourceModel.getType());
+ assertEquals("Resource Icon URI Changed", resourceModel.getIconUri());
+ assertEquals("Resource URI Changed", resourceModel.getUri());
+ assertEquals(resourceServer.getId(), resourceModel.getResourceServer().getId());
+ });
+ }
+
+ @Test
+ public void testFindById() throws Exception {
+ ResourceRepresentation newResource = new ResourceRepresentation();
+
+ newResource.setName("New Resource");
+ newResource.setType("Resource Type");
+ newResource.setIconUri("Resource Icon URI");
+ newResource.setUri("Resource URI");
+
+ Response response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class);
+
+ response = newResourceRequest(resource.getId()).get();
+
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+ resource = response.readEntity(ResourceRepresentation.class);
+
+ assertEquals("New Resource", resource.getName());
+ assertEquals("Resource Type", resource.getType());
+ assertEquals("Resource Icon URI", resource.getIconUri());
+ assertEquals("Resource URI", resource.getUri());
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ ResourceRepresentation newResource = new ResourceRepresentation();
+
+ newResource.setName("New Resource");
+
+ Response response = newResourceRequest().post(Entity.entity(newResource, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ ResourceRepresentation resource = response.readEntity(ResourceRepresentation.class);
+
+ assertNotNull(resource.getId());
+
+ response = newResourceRequest(resource.getId()).delete();
+
+ assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
+
+ onAuthorizationSession(authorizationProvider -> {
+ Resource resourceModel = authorizationProvider.getStoreFactory().getResourceStore().findById(resource.getId());
+
+ assertNull(resourceModel);
+ });
+ }
+
+ private Builder newResourceRequest(String... id) {
+ String idPathParam = "";
+
+ if (id.length != 0) {
+ idPathParam = "/" + id[0];
+ }
+
+ return newClient(getClientByClientId("photoz-restful-api"), "/resource-server/resource" + idPathParam);
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
new file mode 100644
index 0000000..a4cc551
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ResourcePermissionManagementTest.java
@@ -0,0 +1,364 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.authorization;
+
+import org.apache.commons.collections.map.HashedMap;
+import org.junit.Test;
+import org.keycloak.authorization.Decision.Effect;
+import org.keycloak.authorization.admin.representation.PolicyRepresentation;
+import org.keycloak.authorization.admin.representation.ResourceRepresentation;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
+import org.keycloak.authorization.permission.ResourcePermission;
+import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ResourcePermissionManagementTest extends AbstractPhotozAdminTest {
+
+ @Test
+ public void testCreateForTypeWithSinglePolicy() throws Exception {
+ PolicyRepresentation newPermission = new PolicyRepresentation();
+
+ newPermission.setName("Admin Resource Policy");
+ newPermission.setType("resource");
+
+ HashedMap config = new HashedMap();
+
+ config.put("defaultResourceType", "http://photoz.com/admin");
+ config.put("applyPolicies", JsonSerialization.writeValueAsString(new String[] {this.administrationPolicy.getId()}));
+
+ newPermission.setConfig(config);
+
+ Response response = newPermissionRequest().post(Entity.entity(newPermission, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class);
+
+ onAuthorizationSession(authorizationProvider -> {
+ Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId());
+
+ assertNotNull(policyModel);
+ assertEquals(permission.getId(), policyModel.getId());
+ assertEquals(newPermission.getName(), policyModel.getName());
+ assertEquals(newPermission.getType(), policyModel.getType());
+ assertEquals(resourceServer.getId(), policyModel.getResourceServer().getId());
+ });
+
+ Set<String> roles = new HashSet<>();
+
+ roles.add("admin");
+
+ Map<String, DefaultEvaluation> evaluationsAdminRole = performEvaluation(
+ Arrays.asList(new ResourcePermission(adminResource, Collections.emptyList(), resourceServer)),
+ createAccessToken(roles),
+ createClientConnection("127.0.0.1"));
+
+ assertEquals(1, evaluationsAdminRole.size());
+ assertTrue(evaluationsAdminRole.containsKey(this.administrationPolicy.getId()));
+ assertEquals(Effect.PERMIT, evaluationsAdminRole.get(this.administrationPolicy.getId()).getEffect());
+
+ evaluationsAdminRole = performEvaluation(
+ Arrays.asList(new ResourcePermission(adminResource, Collections.emptyList(), resourceServer)),
+ createAccessToken(roles),
+ createClientConnection("127.0.0.10"));
+
+ assertEquals(1, evaluationsAdminRole.size());
+ assertTrue(evaluationsAdminRole.containsKey(this.administrationPolicy.getId()));
+ assertEquals(Effect.DENY, evaluationsAdminRole.get(this.administrationPolicy.getId()).getEffect());
+
+ roles.clear();
+ roles.add("user");
+
+ Map<String, DefaultEvaluation> evaluationsUserRole = performEvaluation(
+ Arrays.asList(new ResourcePermission(adminResource, Collections.emptyList(), resourceServer)),
+ createAccessToken(roles),
+ createClientConnection("127.0.0.1"));
+
+ assertEquals(1, evaluationsUserRole.size());
+ assertTrue(evaluationsUserRole.containsKey(this.administrationPolicy.getId()));
+ assertEquals(Effect.DENY, evaluationsUserRole.get(this.administrationPolicy.getId()).getEffect());
+ }
+
+ @Test
+ public void testCreateForTypeWithMultiplePolicies() throws Exception {
+ createAlbumResourceTypePermission();
+
+ HashSet<String> roles = new HashSet<>();
+
+ roles.add("admin");
+
+ Map<String, DefaultEvaluation> evaluationsAdminRole = performEvaluation(
+ Arrays.asList(new ResourcePermission(albumResource, Collections.emptyList(), resourceServer)),
+ createAccessToken(roles),
+ createClientConnection("127.0.0.1"));
+
+ assertEquals(2, evaluationsAdminRole.size());
+ assertTrue(evaluationsAdminRole.containsKey(this.administrationPolicy.getId()));
+ assertTrue(evaluationsAdminRole.containsKey(this.anyUserPolicy.getId()));
+ assertEquals(Effect.DENY, evaluationsAdminRole.get(this.anyUserPolicy.getId()).getEffect());
+ assertEquals(Effect.PERMIT, evaluationsAdminRole.get(this.administrationPolicy.getId()).getEffect());
+
+ evaluationsAdminRole = performEvaluation(
+ Arrays.asList(new ResourcePermission(albumResource, Collections.emptyList(), resourceServer)),
+ createAccessToken(roles),
+ createClientConnection("127.0.0.10"));
+
+ assertEquals(2, evaluationsAdminRole.size());
+ assertTrue(evaluationsAdminRole.containsKey(this.administrationPolicy.getId()));
+ assertTrue(evaluationsAdminRole.containsKey(this.anyUserPolicy.getId()));
+ assertEquals(Effect.DENY, evaluationsAdminRole.get(this.anyUserPolicy.getId()).getEffect());
+ assertEquals(Effect.DENY, evaluationsAdminRole.get(this.administrationPolicy.getId()).getEffect());
+
+ roles.clear();
+ roles.add("user");
+
+ Map<String, DefaultEvaluation> evaluationsUserRole = performEvaluation(
+ Arrays.asList(new ResourcePermission(albumResource, Collections.emptyList(), resourceServer)),
+ createAccessToken(roles),
+ createClientConnection("127.0.0.1"));
+
+ assertEquals(2, evaluationsUserRole.size());
+ assertTrue(evaluationsUserRole.containsKey(this.administrationPolicy.getId()));
+ assertTrue(evaluationsUserRole.containsKey(this.anyUserPolicy.getId()));
+ assertEquals(Effect.PERMIT, evaluationsUserRole.get(this.anyUserPolicy.getId()).getEffect());
+ assertEquals(Effect.DENY, evaluationsUserRole.get(this.administrationPolicy.getId()).getEffect());
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ PolicyRepresentation permission = createAlbumResourceTypePermission();
+ Map<String, String> config = permission.getConfig();
+
+ config.put("applyPolicies", JsonSerialization.writeValueAsString(new String[] {this.anyUserPolicy.getId()}));
+
+ permission.setConfig(config);
+
+ newPermissionRequest(permission.getId()).put(Entity.entity(permission, MediaType.APPLICATION_JSON_TYPE));
+
+ HashSet<String> roles = new HashSet<>();
+
+ roles.add("admin");
+
+ Map<String, DefaultEvaluation> evaluationsAdminRole = performEvaluation(
+ Arrays.asList(new ResourcePermission(albumResource, Collections.emptyList(), resourceServer)),
+ createAccessToken(roles),
+ createClientConnection("127.0.0.1"));
+
+ assertEquals(1, evaluationsAdminRole.size());
+ assertTrue(evaluationsAdminRole.containsKey(this.anyUserPolicy.getId()));
+ assertEquals(Effect.DENY, evaluationsAdminRole.get(this.anyUserPolicy.getId()).getEffect());
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ PolicyRepresentation newPermission = createAlbumResourceTypePermission();
+
+ Response delete = newPermissionRequest(newPermission.getId()).delete();
+
+ assertEquals(Status.NO_CONTENT.getStatusCode(), delete.getStatus());
+ }
+
+ @Test
+ public void testFindById() throws Exception {
+ PolicyRepresentation newPermission = createAlbumResourceTypePermission();
+
+ Response response = newPermissionRequest(newPermission.getId()).get();
+
+ PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class);
+
+ assertEquals(newPermission.getId(), permission.getId());
+ assertEquals(newPermission.getName(), permission.getName());
+ assertEquals(newPermission.getType(), permission.getType());
+ }
+
+ @Test
+ public void testCreatePolicyForResource() throws Exception {
+ PolicyRepresentation newPermission = new PolicyRepresentation();
+
+ newPermission.setName("Multiple Resource Policy");
+ newPermission.setType("resource");
+
+ HashedMap config = new HashedMap();
+
+ config.put("resources", JsonSerialization.writeValueAsString(new String[] {this.albumResource.getId(), this.adminResource.getId()}));
+ config.put("applyPolicies", JsonSerialization.writeValueAsString(new String[] {this.onlyFromSpecificAddressPolicy.getId()}));
+
+ newPermission.setConfig(config);
+
+ Response response = newPermissionRequest().post(Entity.entity(newPermission, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ List<ResourcePermission> permissions = new ArrayList<>();
+
+ permissions.add(new ResourcePermission(this.albumResource, Collections.emptyList(), this.resourceServer));
+
+ Map<String, DefaultEvaluation> evaluations = performEvaluation(
+ permissions,
+ createAccessToken(Collections.emptySet()),
+ createClientConnection("127.0.0.1"));
+
+ assertEquals(1, evaluations.size());
+ assertTrue(evaluations.containsKey(this.onlyFromSpecificAddressPolicy.getId()));
+ assertEquals(Effect.PERMIT, evaluations.get(this.onlyFromSpecificAddressPolicy.getId()).getEffect());
+
+ permissions = new ArrayList<>();
+
+ permissions.add(new ResourcePermission(this.adminResource, Collections.emptyList(), this.resourceServer));
+
+ evaluations = performEvaluation(
+ permissions,
+ createAccessToken(Collections.emptySet()),
+ createClientConnection("127.0.0.1"));
+
+ assertEquals(1, evaluations.size());
+ assertTrue(evaluations.containsKey(this.onlyFromSpecificAddressPolicy.getId()));
+ assertEquals(Effect.PERMIT, evaluations.get(this.onlyFromSpecificAddressPolicy.getId()).getEffect());
+
+ permissions = new ArrayList<>();
+
+ permissions.add(new ResourcePermission(this.adminResource, Collections.emptyList(), this.resourceServer));
+ permissions.add(new ResourcePermission(this.albumResource, Collections.emptyList(), this.resourceServer));
+
+ evaluations = performEvaluation(
+ permissions,
+ createAccessToken(Collections.emptySet()),
+ createClientConnection("127.0.0.1"));
+
+ assertEquals(1, evaluations.size());
+ assertTrue(evaluations.containsKey(this.onlyFromSpecificAddressPolicy.getId()));
+ assertEquals(Effect.PERMIT, evaluations.get(this.onlyFromSpecificAddressPolicy.getId()).getEffect());
+
+ permissions = new ArrayList<>();
+
+ permissions.add(new ResourcePermission(this.adminResource, Collections.emptyList(), this.resourceServer));
+ permissions.add(new ResourcePermission(this.albumResource, Collections.emptyList(), this.resourceServer));
+
+ evaluations = performEvaluation(
+ permissions,
+ createAccessToken(Collections.emptySet()),
+ createClientConnection("127.0.0.10"));
+
+ assertEquals(1, evaluations.size());
+ assertTrue(evaluations.containsKey(this.onlyFromSpecificAddressPolicy.getId()));
+ assertEquals(Effect.DENY, evaluations.get(this.onlyFromSpecificAddressPolicy.getId()).getEffect());
+ }
+
+ /**
+ * Tests if a resource can inherit the policies defined for another resource based on its type
+ *
+ * @throws Exception
+ */
+ @Test
+ public void testInheritPoliciesBasedOnResourceType() throws Exception {
+ createAlbumResourceTypePermission();
+ ResourceRepresentation representation = new ResourceRepresentation();
+
+ representation.setName("Alice Family Album");
+ representation.setType(this.albumResource.getType());
+
+ Resource resource = createResource(representation);
+
+ Set<String> roles = new HashSet<>();
+
+ roles.add("user");
+
+ Map<String, DefaultEvaluation> evaluationsUserRole = performEvaluation(
+ Arrays.asList(new ResourcePermission(resource, Collections.emptyList(), resourceServer)),
+ createAccessToken(roles),
+ createClientConnection("127.0.0.1"));
+
+ assertEquals(2, evaluationsUserRole.size());
+ assertTrue(evaluationsUserRole.containsKey(this.administrationPolicy.getId()));
+ assertTrue(evaluationsUserRole.containsKey(this.anyUserPolicy.getId()));
+ assertEquals(Effect.PERMIT, evaluationsUserRole.get(this.anyUserPolicy.getId()).getEffect());
+ assertEquals(Effect.DENY, evaluationsUserRole.get(this.administrationPolicy.getId()).getEffect());
+
+ ResourceRepresentation someResourceRep = new ResourceRepresentation();
+
+ someResourceRep.setName("Some Resource");
+ someResourceRep.setType("Some non-existent type");
+
+ Resource someResource = createResource(someResourceRep);
+
+ evaluationsUserRole = performEvaluation(
+ Arrays.asList(new ResourcePermission(someResource, Collections.emptyList(), resourceServer)),
+ createAccessToken(roles),
+ createClientConnection("127.0.0.1"));
+
+ // no policies can be applied given that there is no policy defined for this resource or its type
+ assertEquals(0, evaluationsUserRole.size());
+ }
+
+ private PolicyRepresentation createAlbumResourceTypePermission() throws Exception {
+ PolicyRepresentation newPermission = new PolicyRepresentation();
+
+ newPermission.setName("Album Resource Policy");
+ newPermission.setType("resource");
+ newPermission.setDecisionStrategy(Policy.DecisionStrategy.AFFIRMATIVE);
+
+ HashedMap config = new HashedMap();
+
+ config.put("defaultResourceType", albumResource.getType());
+
+ String applyPolicies = JsonSerialization.writeValueAsString(new String[]{this.anyUserPolicy.getId(), this.administrationPolicy.getId()});
+
+ config.put("applyPolicies", applyPolicies);
+
+ newPermission.setConfig(config);
+
+ Response response = newPermissionRequest().post(Entity.entity(newPermission, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ PolicyRepresentation permission = response.readEntity(PolicyRepresentation.class);
+
+ onAuthorizationSession(authorizationProvider -> {
+ Policy policyModel = authorizationProvider.getStoreFactory().getPolicyStore().findById(permission.getId());
+
+ assertNotNull(policyModel);
+ assertEquals(permission.getId(), policyModel.getId());
+ assertEquals(permission.getName(), policyModel.getName());
+ assertEquals(permission.getType(), policyModel.getType());
+ assertTrue(permission.getConfig().containsValue(albumResource.getType()));
+ assertTrue(permission.getConfig().containsValue(applyPolicies));
+ assertEquals(resourceServer.getId(), policyModel.getResourceServer().getId());
+ });
+
+ return permission;
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
new file mode 100644
index 0000000..839a813
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/ScopeManagementTest.java
@@ -0,0 +1,148 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.authorization;
+
+import org.junit.Test;
+import org.keycloak.authorization.admin.representation.ScopeRepresentation;
+import org.keycloak.authorization.model.Scope;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.Invocation.Builder;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class ScopeManagementTest extends AbstractPhotozAdminTest {
+
+ @Test
+ public void testCreate() throws Exception {
+ ScopeRepresentation newScope = new ScopeRepresentation();
+
+ newScope.setName("New Scope");
+ newScope.setIconUri("Icon URI");
+
+ Response response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class);
+
+ onAuthorizationSession(authorizationProvider -> {
+ Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId());
+
+ assertNotNull(scopeModel);
+ assertEquals(scope.getId(), scopeModel.getId());
+ assertEquals("New Scope", scopeModel.getName());
+ assertEquals("Icon URI", scopeModel.getIconUri());
+ assertEquals(resourceServer.getId(), scopeModel.getResourceServer().getId());
+ });
+ }
+
+ @Test
+ public void testUpdate() throws Exception {
+ ScopeRepresentation newScope = new ScopeRepresentation();
+
+ newScope.setName("New Scope");
+ newScope.setIconUri("Icon URI");
+
+ Response response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ newScope.setName("New Scope Changed");
+ newScope.setIconUri("Icon URI Changed");
+
+ response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE));
+
+ ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class);
+
+ onAuthorizationSession(authorizationProvider -> {
+ Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId());
+
+ assertNotNull(scopeModel);
+ assertEquals(scope.getId(), scopeModel.getId());
+ assertEquals("New Scope Changed", scopeModel.getName());
+ assertEquals("Icon URI Changed", scopeModel.getIconUri());
+ assertEquals(resourceServer.getId(), scopeModel.getResourceServer().getId());
+ });
+ }
+
+ @Test
+ public void testFindById() throws Exception {
+ ScopeRepresentation newScope = new ScopeRepresentation();
+
+ newScope.setName("New Scope");
+ newScope.setIconUri("Icon URI");
+
+ Response response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class);
+
+ response = newScopeRequest(scope.getId()).get();
+
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+ scope = response.readEntity(ScopeRepresentation.class);
+
+ assertEquals("New Scope", scope.getName());
+ assertEquals("Icon URI", scope.getIconUri());
+ }
+
+ @Test
+ public void testDelete() throws Exception {
+ ScopeRepresentation newScope = new ScopeRepresentation();
+
+ newScope.setName("New Scope");
+
+ Response response = newScopeRequest().post(Entity.entity(newScope, MediaType.APPLICATION_JSON_TYPE));
+
+ assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+ ScopeRepresentation scope = response.readEntity(ScopeRepresentation.class);
+
+ assertNotNull(scope.getId());
+
+ response = newScopeRequest(scope.getId()).delete();
+
+ assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
+
+ onAuthorizationSession(authorizationProvider -> {
+ Scope scopeModel = authorizationProvider.getStoreFactory().getScopeStore().findById(scope.getId());
+
+ assertNull(scopeModel);
+ });
+ }
+
+ private Builder newScopeRequest(String... id) {
+ String idPathParam = "";
+
+ if (id.length != 0) {
+ idPathParam = "/" + id[0];
+ }
+
+ return newClient(getClientByClientId("photoz-restful-api"), "/resource-server/scope" + idPathParam);
+ }
+}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
index 27b550a..f340379 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/AdapterTest.java
@@ -86,7 +86,7 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertEquals(realmModel.getName(), "JUGGLER");
Assert.assertArrayEquals(realmModel.getPrivateKey().getEncoded(), keyPair.getPrivate().getEncoded());
Assert.assertArrayEquals(realmModel.getPublicKey().getEncoded(), keyPair.getPublic().getEncoded());
- Assert.assertEquals(2, realmModel.getDefaultRoles().size());
+ Assert.assertEquals(3, realmModel.getDefaultRoles().size());
Assert.assertTrue(realmModel.getDefaultRoles().contains("foo"));
}
@@ -112,7 +112,7 @@ public class AdapterTest extends AbstractModelTest {
Assert.assertEquals(realmModel.getName(), "JUGGLER");
Assert.assertArrayEquals(realmModel.getPrivateKey().getEncoded(), keyPair.getPrivate().getEncoded());
Assert.assertArrayEquals(realmModel.getPublicKey().getEncoded(), keyPair.getPublic().getEncoded());
- Assert.assertEquals(2, realmModel.getDefaultRoles().size());
+ Assert.assertEquals(3, realmModel.getDefaultRoles().size());
Assert.assertTrue(realmModel.getDefaultRoles().contains("foo"));
realmModel.getId();
@@ -462,7 +462,7 @@ public class AdapterTest extends AbstractModelTest {
realmModel.addRole("admin");
realmModel.addRole("user");
Set<RoleModel> roles = realmModel.getRoles();
- Assert.assertEquals(4, roles.size());
+ Assert.assertEquals(5, roles.size());
UserModel user = realmManager.getSession().users().addUser(realmModel, "bburke");
RoleModel realmUserRole = realmModel.getRole("user");
user.grantRole(realmUserRole);
@@ -488,7 +488,7 @@ public class AdapterTest extends AbstractModelTest {
user.grantRole(application.getRole("user"));
roles = user.getRealmRoleMappings();
- Assert.assertEquals(roles.size(), 3);
+ Assert.assertEquals(4, roles.size());
assertRolesContains(realmUserRole, roles);
Assert.assertTrue(user.hasRole(realmUserRole));
// Role "foo" is default realm role
@@ -503,13 +503,13 @@ public class AdapterTest extends AbstractModelTest {
// Test that application role 'user' don't clash with realm role 'user'
Assert.assertNotEquals(realmModel.getRole("user").getId(), application.getRole("user").getId());
- Assert.assertEquals(7, user.getRoleMappings().size());
+ Assert.assertEquals(8, user.getRoleMappings().size());
// Revoke some roles
user.deleteRoleMapping(realmModel.getRole("foo"));
user.deleteRoleMapping(appBarRole);
roles = user.getRoleMappings();
- Assert.assertEquals(5, roles.size());
+ Assert.assertEquals(6, roles.size());
assertRolesContains(realmUserRole, roles);
assertRolesContains(application.getRole("user"), roles);
Assert.assertFalse(user.hasRole(appBarRole));
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index 136fc69..547b583 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -99,7 +99,7 @@ public class ImportTest extends AbstractModelTest {
Assert.assertEquals(1, creds.size());
RequiredCredentialModel cred = creds.get(0);
Assert.assertEquals("password", cred.getFormLabel());
- Assert.assertEquals(3, realm.getDefaultRoles().size());
+ Assert.assertEquals(4, realm.getDefaultRoles().size());
Assert.assertNotNull(realm.getRole("foo"));
Assert.assertNotNull(realm.getRole("bar"));
diff --git a/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json b/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json
new file mode 100644
index 0000000..e23c9a4
--- /dev/null
+++ b/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json
@@ -0,0 +1,161 @@
+{
+ "realm": "photoz",
+ "enabled": true,
+ "accessTokenLifespan": 60,
+ "accessCodeLifespan": 60,
+ "accessCodeLifespanUserAction": 300,
+ "ssoSessionIdleTimeout": 600,
+ "ssoSessionMaxLifespan": 36000,
+ "sslRequired": "external",
+ "registrationAllowed": false,
+ "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
+ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+ "requiredCredentials": [
+ "password"
+ ],
+ "users": [
+ {
+ "username": "alice",
+ "enabled": true,
+ "email": "alice@keycloak.org",
+ "firstName": "Alice",
+ "lastName": "In Chains",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "alice"
+ }
+ ],
+ "realmRoles": [
+ "user"
+ ],
+ "clientRoles": {
+ "photoz-html5-client": [
+ "uma_authorization",
+ "kc_entitlement"
+ ]
+ }
+ },
+ {
+ "username": "jdoe",
+ "enabled": true,
+ "email": "jdoe@keycloak.org",
+ "firstName": "John",
+ "lastName": "Doe",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "jdoe"
+ }
+ ],
+ "realmRoles": [
+ "user"
+ ],
+ "clientRoles": {
+ "photoz-html5-client": [
+ "uma_authorization",
+ "kc_entitlement"
+ ]
+ }
+ },
+ {
+ "username": "admin",
+ "enabled": true,
+ "email": "admin@admin.com",
+ "firstName": "Admin",
+ "lastName": "Istrator",
+ "credentials": [
+ {
+ "type": "password",
+ "value": "admin"
+ }
+ ],
+ "realmRoles": [
+ "user",
+ "admin"
+ ],
+ "clientRoles": {
+ "realm-management": [
+ "realm-admin"
+ ],
+ "photoz-html5-client": [
+ "uma_authorization",
+ "kc_entitlement"
+ ]
+ }
+ },
+ {
+ "username": "service-account-photoz-restful-api",
+ "enabled": true,
+ "email": "service-account-photoz-restful-api@placeholder.org",
+ "serviceAccountClientId": "photoz-restful-api",
+ "realmRoles": [
+ "uma_protection"
+ ]
+ }
+ ],
+ "roles": {
+ "realm": [
+ {
+ "name": "user",
+ "description": "User privileges"
+ },
+ {
+ "name": "admin",
+ "description": "Administrator privileges"
+ },
+ {
+ "name": "uma_protection",
+ "description": "Allows access to the Protection API"
+ }
+ ]
+ },
+ "clients": [
+ {
+ "clientId": "photoz-html5-client",
+ "enabled": true,
+ "adminUrl": "/photoz-html5-client",
+ "baseUrl": "/photoz-html5-client",
+ "publicClient": true,
+ "redirectUris": [
+ "/photoz-html5-client/*"
+ ],
+ "webOrigins": [
+ ""
+ ]
+ },
+ {
+ "clientId": "photoz-restful-api",
+ "enabled": true,
+ "publicClient": false,
+ "baseUrl": "/photoz-restful-api",
+ "bearerOnly": false,
+ "serviceAccountsEnabled": true,
+ "redirectUris": [
+ "/photoz-restful-api/*"
+ ],
+ "secret": "secret"
+ },
+ {
+ "clientId": "public-client-01",
+ "enabled": true,
+ "publicClient": true,
+ "baseUrl": "public-client-01",
+ "redirectUris": [
+ "/public-client-01/*"
+ ]
+ },
+ {
+ "clientId": "confidential-no-service-account",
+ "enabled": true,
+ "publicClient": false,
+ "baseUrl": "/confidential-no-service-account",
+ "bearerOnly": false,
+ "serviceAccountsEnabled": false,
+ "redirectUris": [
+ "/confidential-no-service-account/*"
+ ],
+ "secret": "secret"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index 1c63a35..16ded24 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -26,6 +26,10 @@
"provider": "${keycloak.userSessionPersister.provider:jpa}"
},
+ "authorizationPersister": {
+ "provider": "${keycloak.authorization.provider:jpa}"
+ },
+
"userCache": {
"default" : {
"enabled": true
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
index 68a8f8d..2a9c643 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ClientTest.java
@@ -359,7 +359,7 @@ public class ClientTest extends AbstractAdminTest {
Assert.assertNames(scopesResource.realmLevel().listAll(), "role1");
Assert.assertNames(scopesResource.realmLevel().listEffective(), "role1", "role2");
- Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access");
+ Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll(), AccountRoles.VIEW_PROFILE);
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective(), AccountRoles.VIEW_PROFILE);
@@ -376,7 +376,7 @@ public class ClientTest extends AbstractAdminTest {
Assert.assertNames(scopesResource.realmLevel().listAll());
Assert.assertNames(scopesResource.realmLevel().listEffective());
- Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", "role1", "role2");
+ Assert.assertNames(scopesResource.realmLevel().listAvailable(), "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "role1", "role2");
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAll());
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listAvailable(), AccountRoles.VIEW_PROFILE, AccountRoles.MANAGE_ACCOUNT);
Assert.assertNames(scopesResource.clientLevel(accountMgmtId).listEffective());
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
index 988ecaf..9ac45d0 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java
@@ -23,6 +23,7 @@ import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleMappingResource;
import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.Constants;
import org.keycloak.models.RoleModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
@@ -430,7 +431,7 @@ public class GroupTest extends AbstractGroupTest {
// List realm roles
assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite");
- assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", "user");
+ assertNames(roles.realmLevel().listAvailable(), "admin", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION, "user");
assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child");
// List client roles
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 19645a8..bb23116 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -29,6 +29,7 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleMappingResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.Constants;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.ErrorRepresentation;
@@ -789,7 +790,7 @@ public class UserTest extends AbstractAdminTest {
assertAdminEvents.clear();
RoleMappingResource roles = realm.users().get(userId).roles();
- assertNames(roles.realmLevel().listAll(), "user", "offline_access");
+ assertNames(roles.realmLevel().listAll(), "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
// Add realm roles
List<RoleRepresentation> l = new LinkedList<>();
@@ -808,9 +809,9 @@ public class UserTest extends AbstractAdminTest {
assertAdminEvents.assertEvent("test", OperationType.CREATE, AdminEventPaths.userClientRoleMappingsPath(userId, clientUuid), list);
// List realm roles
- assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite", "user", "offline_access");
+ assertNames(roles.realmLevel().listAll(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
assertNames(roles.realmLevel().listAvailable(), "admin");
- assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child", "user", "offline_access");
+ assertNames(roles.realmLevel().listEffective(), "realm-role", "realm-composite", "realm-child", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
// List client roles
assertNames(roles.clientLevel(clientUuid).listAll(), "client-role", "client-composite");
@@ -819,7 +820,7 @@ public class UserTest extends AbstractAdminTest {
// Get mapping representation
MappingsRepresentation all = roles.getAll();
- assertNames(all.getRealmMappings(), "realm-role", "realm-composite", "user", "offline_access");
+ assertNames(all.getRealmMappings(), "realm-role", "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
assertEquals(2, all.getClientMappings().size());
assertNames(all.getClientMappings().get("myclient").getMappings(), "client-role", "client-composite");
assertNames(all.getClientMappings().get("account").getMappings(), "manage-account", "view-profile");
@@ -829,7 +830,7 @@ public class UserTest extends AbstractAdminTest {
roles.realmLevel().remove(Collections.singletonList(realmRoleRep));
assertAdminEvents.assertEvent("test", OperationType.DELETE, AdminEventPaths.userRealmRoleMappingsPath(userId), Collections.singletonList(realmRoleRep));
- assertNames(roles.realmLevel().listAll(), "realm-composite", "user", "offline_access");
+ assertNames(roles.realmLevel().listAll(), "realm-composite", "user", "offline_access", Constants.AUTHZ_UMA_AUTHORIZATION);
// Remove client role
RoleRepresentation clientRoleRep = realm.clients().get(clientUuid).roles().get("client-role").toRepresentation();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
index 74b0516..900767f 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java
@@ -122,7 +122,7 @@ public class TokenIntrospectionTest extends TestRealmKeycloakTest {
EventRepresentation loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password");
- String tokenResponse = oauth.introspectAccessTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken());
+ String tokenResponse = oauth.introspectRefreshTokenWithClientCredential("confidential-cli", "secret1", accessTokenResponse.getAccessToken());
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(tokenResponse);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index 9208927..78479d7 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -27,6 +27,10 @@
"provider": "${keycloak.userSessionPersister.provider:jpa}"
},
+ "authorizationPersister": {
+ "provider": "${keycloak.authorization.provider:jpa}"
+ },
+
"userCache": {
"provider": "${keycloak.user.cache.provider:default}",
"default" : {
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
index 8c0727f..6c5ac3c 100755
--- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties
+++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties
@@ -53,6 +53,7 @@ role_view-profile=View profile
role_manage-account=Manage account
role_read-token=Read token
role_offline-access=Offline access
+role_uma_authorization=Obtain permissions
client_account=Account
client_security-admin-console=Security Admin Console
client_admin-cli=Admin CLI
diff --git a/themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties b/themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties
index e2e8743..8b07e7b 100644
--- a/themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties
+++ b/themes/src/main/resources/theme/base/account/messages/messages_pt_BR.properties
@@ -53,6 +53,7 @@ role_view-profile=Visualiza perfil
role_manage-account=Gerencia conta
role_read-token=L\u00EA token
role_offline-access=Acesso Offline
+role_uma_authorization=Obter permiss\u00F5es
client_account=Conta
client_security-admin-console=Console de Administra\u00E7\u00E3o de Seguran\u00E7a
client_admin-cli=Admin CLI
diff --git a/themes/src/main/resources/theme/base/admin/index.ftl b/themes/src/main/resources/theme/base/admin/index.ftl
index 9f5f94a..782e089 100755
--- a/themes/src/main/resources/theme/base/admin/index.ftl
+++ b/themes/src/main/resources/theme/base/admin/index.ftl
@@ -50,6 +50,14 @@
<script src="${resourceUrl}/js/controllers/groups.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/loaders.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/services.js" type="text/javascript"></script>
+
+ <!-- Authorization -->
+ <script src="${resourceUrl}/js/authz/lib/ace/ace.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/js/authz/lib/ace/mode-javascript.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/js/authz/lib/ace/ui-ace.min.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/js/authz/authz-app.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/js/authz/authz-controller.js" type="text/javascript"></script>
+ <script src="${resourceUrl}/js/authz/authz-services.js" type="text/javascript"></script>
</head>
<body data-ng-controller="GlobalCtrl" data-ng-cloak data-ng-show="auth.user">
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
new file mode 100644
index 0000000..467038c
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
@@ -0,0 +1,412 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2016 Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+module.requires.push('ui.ace');
+
+module.config(['$routeProvider', function ($routeProvider) {
+ $routeProvider
+ .when('/realms/:realm/authz', {
+ templateUrl: resourceUrl + '/partials/authz/resource-server-list.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ }
+ },
+ controller: 'ResourceServerCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/create', {
+ templateUrl: resourceUrl + '/partials/authz/resource-server-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ },
+ clients: function (ClientListLoader) {
+ return ClientListLoader();
+ }
+ },
+ controller: 'ResourceServerDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server', {
+ templateUrl: resourceUrl + '/partials/authz/resource-server-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ },
+ clients: function (ClientListLoader) {
+ return ClientListLoader();
+ },
+ serverInfo: function (ServerInfoLoader) {
+ return ServerInfoLoader();
+ }
+ },
+ controller: 'ResourceServerDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/evaluate', {
+ templateUrl: resourceUrl + '/partials/authz/policy/resource-server-policy-evaluate.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ },
+ clients: function (ClientListLoader) {
+ return ClientListLoader();
+ },
+ roles: function (RoleListLoader) {
+ return new RoleListLoader();
+ }
+ },
+ controller: 'PolicyEvaluateCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/evaluate/result', {
+ templateUrl: resourceUrl + '/partials/authz/policy/resource-server-policy-evaluate-result.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ },
+ },
+ controller: 'PolicyEvaluateCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/resource', {
+ templateUrl: resourceUrl + '/partials/authz/resource-server-resource-list.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerResourceCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/resource/create', {
+ templateUrl: resourceUrl + '/partials/authz/resource-server-resource-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerResourceDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/resource/:rsrid', {
+ templateUrl: resourceUrl + '/partials/authz/resource-server-resource-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerResourceDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/scope', {
+ templateUrl: resourceUrl + '/partials/authz/resource-server-scope-list.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerScopeCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/scope/create', {
+ templateUrl: resourceUrl + '/partials/authz/resource-server-scope-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerScopeDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/scope/:id', {
+ templateUrl: resourceUrl + '/partials/authz/resource-server-scope-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerScopeDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/permission', {
+ templateUrl: resourceUrl + '/partials/authz/permission/resource-server-permission-list.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPermissionCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy', {
+ templateUrl: resourceUrl + '/partials/authz/policy/resource-server-policy-list.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/drools/create', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-drools-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyDroolsDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/drools/:id', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-drools-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyDroolsDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/permission/resource/create', {
+ templateUrl: resourceUrl + '/partials/authz/permission/provider/resource-server-policy-resource-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyResourceDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/permission/resource/:id', {
+ templateUrl: resourceUrl + '/partials/authz/permission/provider/resource-server-policy-resource-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyResourceDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/permission/scope/create', {
+ templateUrl: resourceUrl + '/partials/authz/permission/provider/resource-server-policy-scope-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyScopeDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/permission/scope/:id', {
+ templateUrl: resourceUrl + '/partials/authz/permission/provider/resource-server-policy-scope-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyScopeDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/user/create', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-user-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyUserDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/user/:id', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-user-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyUserDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/role/create', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-role-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyRoleDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/role/:id', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-role-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyRoleDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-js-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyJSDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/:id', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-js-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyJSDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/time/create', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-time-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyTimeDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/time/:id', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-time-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyTimeDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/aggregate/create', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyAggregateDetailCtrl'
+ }).when('/realms/:realm/clients/:client/authz/resource-server/policy/aggregate/:id', {
+ templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html',
+ resolve: {
+ realm: function (RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ }
+ },
+ controller: 'ResourceServerPolicyAggregateDetailCtrl'
+ });
+}]);
+
+module.directive('kcTabsResourceServer', function () {
+ return {
+ scope: true,
+ restrict: 'E',
+ replace: true,
+ templateUrl: resourceUrl + '/templates/authz/kc-tabs-resource-server.html'
+ }
+});
+
+module.filter('unique', function () {
+
+ return function (items, filterOn) {
+
+ if (filterOn === false) {
+ return items;
+ }
+
+ if ((filterOn || angular.isUndefined(filterOn)) && angular.isArray(items)) {
+ var hashCheck = {}, newItems = [];
+
+ var extractValueToCompare = function (item) {
+ if (angular.isObject(item) && angular.isString(filterOn)) {
+ return item[filterOn];
+ } else {
+ return item;
+ }
+ };
+
+ angular.forEach(items, function (item) {
+ var valueToCheck, isDuplicate = false;
+
+ for (var i = 0; i < newItems.length; i++) {
+ if (angular.equals(extractValueToCompare(newItems[i]), extractValueToCompare(item))) {
+ isDuplicate = true;
+ break;
+ }
+ }
+ if (!isDuplicate) {
+ newItems.push(item);
+ }
+
+ });
+ items = newItems;
+ }
+ return items;
+ };
+});
+
+module.filter('toCamelCase', function () {
+ return function (input) {
+ input = input || '';
+ return input.replace(/\w\S*/g, function (txt) {
+ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+ });
+ };
+})
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
new file mode 100644
index 0000000..e07ed1a
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -0,0 +1,1205 @@
+module.controller('ResourceServerCtrl', function($scope, realm, ResourceServer) {
+ $scope.realm = realm;
+
+ ResourceServer.query({realm : realm.realm}, function (data) {
+ $scope.servers = data;
+ });
+});
+
+module.controller('ResourceServerDetailCtrl', function($scope, $http, $route, $location, $upload, realm, ResourceServer, client, AuthzDialog, Notifications) {
+ $scope.realm = realm;
+ $scope.client = client;
+
+ ResourceServer.get({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ $scope.server = angular.copy(data);
+ $scope.changed = false;
+
+ $scope.$watch('server', function() {
+ if (!angular.equals($scope.server, data)) {
+ $scope.changed = true;
+ }
+ }, true);
+
+ $scope.save = function() {
+ ResourceServer.update({realm : realm.realm, client : $scope.server.clientId}, $scope.server, function() {
+ $route.reload();
+ Notifications.success("The resource server has been created.");
+ });
+ }
+
+ $scope.reset = function() {
+ $scope.server = angular.copy(data);
+ $scope.changed = false;
+ }
+
+ $scope.export = function() {
+ $scope.exportSettings = true;
+ ResourceServer.settings({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ var tmp = angular.fromJson(data);
+ $scope.settings = angular.toJson(tmp, true);
+ })
+ }
+
+ $scope.downloadSettings = function() {
+ saveAs(new Blob([$scope.settings], { type: 'application/json' }), $scope.server.name + "-authz-config.json");
+ }
+
+ $scope.cancelExport = function() {
+ delete $scope.settings
+ }
+
+ $scope.onFileSelect = function($files) {
+ $scope.files = $files;
+ };
+
+ $scope.clearFileSelect = function() {
+ $scope.files = null;
+ }
+
+ $scope.uploadFile = function() {
+ //$files: an array of files selected, each file has name, size, and type.
+ for (var i = 0; i < $scope.files.length; i++) {
+ var $file = $scope.files[i];
+ $scope.upload = $upload.upload({
+ url: authUrl + '/admin/realms/' + $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server', //upload.php script, node.js route, or servlet url
+ // method: POST or PUT,
+ // headers: {'headerKey': 'headerValue'}, withCredential: true,
+ data: {myObj: ""},
+ file: $file
+ /* set file formData name for 'Content-Desposition' header. Default: 'file' */
+ //fileFormDataName: myFile,
+ /* customize how data is added to formData. See #40#issuecomment-28612000 for example */
+ //formDataAppender: function(formData, key, val){}
+ }).progress(function(evt) {
+ console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total));
+ }).success(function(data, status, headers) {
+ $route.reload();
+ Notifications.success("The resource server has been updated.");
+ }).error(function() {
+ Notifications.error("The resource server can not be uploaded. Please verify the file.");
+ });
+ }
+ };
+ });
+});
+
+module.controller('ResourceServerResourceCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerResource, client) {
+ $scope.realm = realm;
+ $scope.client = client;
+
+ ResourceServer.get({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ $scope.server = data;
+
+ $scope.createPolicy = function(resource) {
+ $location.path('/realms/' + $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/permission/resource/create').search({rsrid: resource._id});
+ }
+
+ ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.resources = data;
+ });
+ });
+});
+
+module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $route, $location, realm, ResourceServer, client, ResourceServerResource, ResourceServerScope, AuthzDialog, Notifications) {
+ $scope.realm = realm;
+ $scope.client = client;
+
+ ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.scopes = data;
+ });
+
+ ResourceServer.get({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ $scope.server = data;
+
+ var resourceId = $route.current.params.rsrid;
+
+ if (!resourceId) {
+ $scope.create = true;
+ $scope.changed = false;
+
+ var resource = {};
+ resource.scopes = [];
+
+ $scope.resource = angular.copy(resource);
+
+ $scope.$watch('resource', function() {
+ if (!angular.equals($scope.resource, resource)) {
+ $scope.changed = true;
+ }
+ }, true);
+
+ $scope.save = function() {
+ ResourceServerResource.save({realm : realm.realm, client : $scope.client.id}, $scope.resource, function(data) {
+ $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource/" + data._id);
+ Notifications.success("The resource has been created.");
+ });
+ }
+
+ $scope.cancel = function() {
+ $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource/");
+ }
+ } else {
+ ResourceServerResource.get({
+ realm : $route.current.params.realm,
+ client : client.id,
+ rsrid : $route.current.params.rsrid,
+ }, function(data) {
+ $scope.resource = angular.copy(data);
+ $scope.changed = false;
+
+ for (i = 0; i < $scope.resource.scopes.length; i++) {
+ $scope.resource.scopes[i] = $scope.resource.scopes[i].name;
+ }
+
+ $scope.$watch('resource', function() {
+ if (!angular.equals($scope.resource, data)) {
+ $scope.changed = true;
+ }
+ }, true);
+
+ $scope.save = function() {
+ ResourceServerResource.update({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, $scope.resource, function() {
+ $route.reload();
+ Notifications.success("The resource has been updated.");
+ });
+ }
+
+ $scope.remove = function() {
+ var msg = "";
+
+ if ($scope.resource.policies.length > 0) {
+ msg = "<p>This resource is referenced in some policies:</p>";
+ msg += "<ul>";
+ for (i = 0; i < $scope.resource.policies.length; i++) {
+ msg+= "<li><strong>" + $scope.resource.policies[i].name + "</strong></li>";
+ }
+ msg += "</ul>";
+ msg += "<p>If you remove this resource, the policies above will be affected and will not be associated with this resource anymore.</p>";
+ }
+
+ AuthzDialog.confirmDeleteWithMsg($scope.resource.name, "Resource", msg, function() {
+ ResourceServerResource.delete({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, null, function() {
+ $location.url("/realms/" + realm.realm + "/clients/" + $scope.client.id + "/authz/resource-server/resource");
+ Notifications.success("The resource has been deleted.");
+ });
+ });
+ }
+
+ $scope.reset = function() {
+ $scope.resource = angular.copy(data);
+ $scope.changed = false;
+ }
+ });
+ }
+ });
+});
+
+module.controller('ResourceServerScopeCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerScope, client) {
+ $scope.realm = realm;
+ $scope.client = client;
+
+ ResourceServer.get({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ $scope.server = data;
+
+ ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.scopes = data;
+ });
+ });
+});
+
+module.controller('ResourceServerScopeDetailCtrl', function($scope, $http, $route, $location, realm, ResourceServer, client, ResourceServerScope, AuthzDialog, Notifications) {
+ $scope.realm = realm;
+ $scope.client = client;
+
+ ResourceServer.get({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ $scope.server = data;
+
+ var scopeId = $route.current.params.id;
+
+ if (!scopeId) {
+ $scope.create = true;
+ $scope.changed = false;
+
+ var scope = {};
+
+ $scope.resource = angular.copy(scope);
+
+ $scope.$watch('scope', function() {
+ if (!angular.equals($scope.scope, scope)) {
+ $scope.changed = true;
+ }
+ }, true);
+
+ $scope.save = function() {
+ ResourceServerScope.save({realm : realm.realm, client : $scope.client.id}, $scope.scope, function(data) {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/scope/" + data.id);
+ Notifications.success("The scope has been created.");
+ });
+ }
+ } else {
+ ResourceServerScope.get({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : $route.current.params.id,
+ }, function(data) {
+ $scope.scope = angular.copy(data);
+ $scope.changed = false;
+
+ $scope.$watch('scope', function() {
+ if (!angular.equals($scope.scope, data)) {
+ $scope.changed = true;
+ }
+ }, true);
+
+ $scope.save = function() {
+ ResourceServerScope.update({realm : realm.realm, client : $scope.client.id, id : $scope.scope.id}, $scope.scope, function() {
+ $scope.changed = false;
+ Notifications.success("The scope has been updated.");
+ });
+ }
+
+ $scope.remove = function() {
+ var msg = "";
+
+ if ($scope.scope.policies.length > 0) {
+ msg = "<p>This resource is referenced in some policies:</p>";
+ msg += "<ul>";
+ for (i = 0; i < $scope.scope.policies.length; i++) {
+ msg+= "<li><strong>" + $scope.scope.policies[i].name + "</strong></li>";
+ }
+ msg += "</ul>";
+ msg += "<p>If you remove this resource, the policies above will be affected and will not be associated with this resource anymore.</p>";
+ }
+
+ AuthzDialog.confirmDeleteWithMsg($scope.scope.name, "Scope", msg, function() {
+ ResourceServerScope.delete({realm : realm.realm, client : $scope.client.id, id : $scope.scope.id}, null, function() {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/scope");
+ Notifications.success("The scope has been deleted.");
+ });
+ });
+ }
+
+ $scope.reset = function() {
+ $scope.scope = angular.copy(data);
+ $scope.changed = false;
+ }
+ });
+ }
+ });
+});
+
+module.controller('ResourceServerPolicyCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPolicy, PolicyProvider, client) {
+ $scope.realm = realm;
+ $scope.client = client;
+ $scope.policyProviders = [];
+
+ PolicyProvider.query({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function (data) {
+ for (i = 0; i < data.length; i++) {
+ if (data[i].type != 'resource' && data[i].type != 'scope') {
+ $scope.policyProviders.push(data[i]);
+ }
+ }
+ });
+
+ ResourceServer.get({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ $scope.server = data;
+
+ ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.policies = [];
+
+ for (i = 0; i < data.length; i++) {
+ if (data[i].type != 'resource' && data[i].type != 'scope') {
+ $scope.policies.push(data[i]);
+ }
+ }
+ });
+ });
+
+ $scope.addPolicy = function(policyType) {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy/" + policyType.type + "/create");
+ }
+});
+
+module.controller('ResourceServerPermissionCtrl', function($scope, $http, $route, $location, realm, ResourceServer, ResourceServerPolicy, PolicyProvider, client) {
+ $scope.realm = realm;
+ $scope.client = client;
+ $scope.policyProviders = [];
+
+ PolicyProvider.query({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function (data) {
+ for (i = 0; i < data.length; i++) {
+ if (data[i].type == 'resource' || data[i].type == 'scope') {
+ $scope.policyProviders.push(data[i]);
+ }
+ }
+ });
+
+ ResourceServer.get({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ $scope.server = data;
+
+ ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.policies = [];
+
+ for (i = 0; i < data.length; i++) {
+ if (data[i].type == 'resource' || data[i].type == 'scope') {
+ $scope.policies.push(data[i]);
+ }
+ }
+ });
+ });
+
+ $scope.addPolicy = function(policyType) {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission/" + policyType.type + "/create");
+ }
+});
+
+module.controller('ResourceServerPolicyDroolsDetailCtrl', function($scope, $http, $route, realm, client, PolicyController) {
+ PolicyController.onInit({
+ getPolicyType : function() {
+ return "drools";
+ },
+
+ onInit : function() {
+ $scope.drools = {};
+
+ $scope.resolveModules = function(policy) {
+ if (!policy) {
+ policy = $scope.policy;
+ }
+
+ $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/drools/resolveModules'
+ , policy).success(function(data) {
+ $scope.drools.moduleNames = data;
+ $scope.resolveSessions();
+ });
+ }
+
+ $scope.resolveSessions = function() {
+ $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/drools/resolveSessions'
+ , $scope.policy).success(function(data) {
+ $scope.drools.moduleSessions = data;
+ });
+ }
+ },
+
+ onInitUpdate : function(policy) {
+ policy.config.scannerPeriod = parseInt(policy.config.scannerPeriod);
+ $scope.resolveModules(policy);
+ },
+
+ onUpdate : function() {
+ $scope.policy.config.resources = JSON.stringify($scope.policy.config.resources);
+ },
+
+ onInitCreate : function(newPolicy) {
+ newPolicy.config.scannerPeriod = 1;
+ newPolicy.config.scannerPeriodUnit = 'Hours';
+ }
+ }, realm, client, $scope);
+});
+
+module.controller('ResourceServerPolicyResourceDetailCtrl', function($scope, $route, $location, realm, client, PolicyController, ResourceServerPolicy, ResourceServerResource) {
+ PolicyController.onInit({
+ getPolicyType : function() {
+ return "resource";
+ },
+
+ isPermission : function() {
+ return true;
+ },
+
+ onInit : function() {
+ ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.resources = data;
+ });
+
+ ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.policies = [];
+
+ for (i = 0; i < data.length; i++) {
+ if (data[i].type != 'resource' && data[i].type != 'scope') {
+ $scope.policies.push(data[i]);
+ }
+ }
+ });
+
+ $scope.applyToResourceType = function() {
+ if ($scope.policy.config.default) {
+ $scope.policy.config.resources = [];
+ } else {
+ $scope.policy.config.defaultResourceType = null;
+ }
+ }
+ },
+
+ onInitUpdate : function(policy) {
+ policy.config.default = eval(policy.config.default);
+ policy.config.resources = eval(policy.config.resources);
+ policy.config.applyPolicies = eval(policy.config.applyPolicies);
+ },
+
+ onUpdate : function() {
+ $scope.policy.config.resources = JSON.stringify($scope.policy.config.resources);
+ $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ },
+
+ onInitCreate : function(newPolicy) {
+ newPolicy.decisionStrategy = 'UNANIMOUS';
+ newPolicy.config = {};
+ newPolicy.config.resources = '';
+
+ var resourceId = $location.search()['rsrid'];
+
+ if (resourceId) {
+ newPolicy.config.resources = [resourceId];
+ }
+ },
+
+ onCreate : function() {
+ $scope.policy.config.resources = JSON.stringify($scope.policy.config.resources);
+ $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ }
+ }, realm, client, $scope);
+});
+
+module.controller('ResourceServerPolicyScopeDetailCtrl', function($scope, $route, realm, client, PolicyController, ResourceServerPolicy, ResourceServerResource, ResourceServerScope) {
+ PolicyController.onInit({
+ getPolicyType : function() {
+ return "scope";
+ },
+
+ isPermission : function() {
+ return true;
+ },
+
+ onInit : function() {
+ ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.scopes = data;
+ });
+
+ ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.resources = data;
+ });
+
+ ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.policies = [];
+
+ for (i = 0; i < data.length; i++) {
+ if (data[i].type != 'resource' && data[i].type != 'scope') {
+ $scope.policies.push(data[i]);
+ }
+ }
+ });
+
+ $scope.resolveScopes = function(policy, keepScopes) {
+ if (!keepScopes) {
+ policy.config.scopes = [];
+ }
+
+ if (!policy) {
+ policy = $scope.policy;
+ }
+
+ if (policy.config.resources != null) {
+ ResourceServerResource.get({
+ realm : $route.current.params.realm,
+ client : client.id,
+ rsrid : policy.config.resources
+ }, function(data) {
+ $scope.scopes = data.scopes;
+ });
+ } else {
+ ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.scopes = data;
+ });
+ }
+ }
+ },
+
+ onInitUpdate : function(policy) {
+ if (policy.config.resources) {
+ policy.config.resources = eval(policy.config.resources);
+
+ if (policy.config.resources.length > 0) {
+ policy.config.resources = policy.config.resources[0];
+ } else {
+ policy.config.resources = null;
+ }
+ }
+
+ $scope.resolveScopes(policy, true);
+
+ policy.config.applyPolicies = eval(policy.config.applyPolicies);
+ policy.config.scopes = eval(policy.config.scopes);
+ },
+
+ onUpdate : function() {
+ if ($scope.policy.config.resources != null) {
+ var resources = undefined;
+
+ if ($scope.policy.config.resources.length != 0) {
+ resources = JSON.stringify([$scope.policy.config.resources])
+ }
+
+ $scope.policy.config.resources = resources;
+ }
+
+ $scope.policy.config.scopes = JSON.stringify($scope.policy.config.scopes);
+ $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ },
+
+ onInitCreate : function(newPolicy) {
+ newPolicy.decisionStrategy = 'UNANIMOUS';
+ newPolicy.config = {};
+ newPolicy.config.resources = '';
+ },
+
+ onCreate : function() {
+ if ($scope.policy.config.resources != null) {
+ var resources = undefined;
+
+ if ($scope.policy.config.resources.length != 0) {
+ resources = JSON.stringify([$scope.policy.config.resources])
+ }
+
+ $scope.policy.config.resources = resources;
+ }
+ $scope.policy.config.scopes = JSON.stringify($scope.policy.config.scopes);
+ $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ }
+ }, realm, client, $scope);
+});
+
+module.controller('ResourceServerPolicyUserDetailCtrl', function($scope, $route, realm, client, PolicyController, User) {
+ PolicyController.onInit({
+ getPolicyType : function() {
+ return "user";
+ },
+
+ onInit : function() {
+ User.query({realm: $route.current.params.realm}, function(data) {
+ $scope.users = data;
+ });
+
+ $scope.selectedUsers = [];
+
+ $scope.selectUser = function(user) {
+ if (!user || !user.id) {
+ return;
+ }
+
+ $scope.selectedUser = {};
+ $scope.selectedUsers.push(user);
+ }
+
+ $scope.removeFromList = function(list, index) {
+ list.splice(index, 1);
+ }
+ },
+
+ onInitUpdate : function(policy) {
+ var selectedUsers = [];
+
+ if (policy.config.users) {
+ var users = eval(policy.config.users);
+
+ for (i = 0; i < users.length; i++) {
+ User.get({realm: $route.current.params.realm, userId: users[i]}, function(data) {
+ selectedUsers.push(data);
+ $scope.selectedUsers = angular.copy(selectedUsers);
+ });
+ }
+ }
+
+ $scope.$watch('selectedUsers', function() {
+ if (!angular.equals($scope.selectedUsers, selectedUsers)) {
+ $scope.changed = true;
+ }
+ }, true);
+ },
+
+ onUpdate : function() {
+ var users = [];
+
+ for (i = 0; i < $scope.selectedUsers.length; i++) {
+ users.push($scope.selectedUsers[i].id);
+ }
+
+ $scope.policy.config.users = JSON.stringify(users);
+ },
+
+ onCreate : function() {
+ var users = [];
+
+ for (i = 0; i < $scope.selectedUsers.length; i++) {
+ users.push($scope.selectedUsers[i].id);
+ }
+
+ $scope.policy.config.users = JSON.stringify(users);
+ }
+ }, realm, client, $scope);
+});
+
+module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route, realm, client, PolicyController, Role, RoleById) {
+ PolicyController.onInit({
+ getPolicyType : function() {
+ return "role";
+ },
+
+ onInit : function() {
+ Role.query({realm: $route.current.params.realm}, function(data) {
+ $scope.roles = data;
+ });
+
+ $scope.selectedRoles = [];
+
+ $scope.selectRole = function(role) {
+ if (!role || !role.id) {
+ return;
+ }
+
+ $scope.selectedRole = {};
+ $scope.selectedRoles.push(role);
+ }
+
+ $scope.removeFromList = function(list, index) {
+ list.splice(index, 1);
+ }
+ },
+
+ onInitUpdate : function(policy) {
+ var selectedRoles = [];
+
+ if (policy.config.roles) {
+ var roles = eval(policy.config.roles);
+
+ for (i = 0; i < roles.length; i++) {
+ RoleById.get({realm: $route.current.params.realm, role: roles[i]}, function(data) {
+ selectedRoles.push(data);
+ $scope.selectedRoles = angular.copy(selectedRoles);
+ });
+ }
+ }
+
+ $scope.$watch('selectedRoles', function() {
+ if (!angular.equals($scope.selectedRoles, selectedRoles)) {
+ $scope.changed = true;
+ }
+ }, true);
+ },
+
+ onUpdate : function() {
+ var roles = [];
+
+ for (i = 0; i < $scope.selectedRoles.length; i++) {
+ roles.push($scope.selectedRoles[i].id);
+ }
+
+ $scope.policy.config.roles = JSON.stringify(roles);
+ },
+
+ onCreate : function() {
+ var roles = [];
+
+ for (i = 0; i < $scope.selectedRoles.length; i++) {
+ roles.push($scope.selectedRoles[i].id);
+ }
+
+ $scope.policy.config.roles = JSON.stringify(roles);
+ }
+ }, realm, client, $scope);
+});
+
+module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) {
+ PolicyController.onInit({
+ getPolicyType : function() {
+ return "js";
+ },
+
+ onInit : function() {
+ $scope.initEditor = function(editor){
+ var session = editor.getSession();
+
+ session.setMode('ace/mode/javascript');
+ };
+ },
+
+ onInitUpdate : function(policy) {
+
+ },
+
+ onUpdate : function() {
+
+ },
+
+ onInitCreate : function(newPolicy) {
+ newPolicy.config = {};
+ },
+
+ onCreate : function() {
+
+ }
+ }, realm, client, $scope);
+});
+
+module.controller('ResourceServerPolicyTimeDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) {
+ PolicyController.onInit({
+ getPolicyType : function() {
+ return "time";
+ },
+
+ onInit : function() {
+ },
+
+ onInitUpdate : function(policy) {
+
+ },
+
+ onUpdate : function() {
+
+ },
+
+ onInitCreate : function(newPolicy) {
+ newPolicy.config.expirationTime = 1;
+ newPolicy.config.expirationUnit = 'Minutes';
+ },
+
+ onCreate : function() {
+
+ }
+ }, realm, client, $scope);
+});
+
+module.controller('ResourceServerPolicyAggregateDetailCtrl', function($scope, $route, $location, realm, PolicyController, ResourceServerPolicy, client) {
+ PolicyController.onInit({
+ getPolicyType : function() {
+ return "aggregate";
+ },
+
+ onInit : function() {
+ ResourceServerPolicy.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.policies = [];
+
+ for (i = 0; i < data.length; i++) {
+ if (data[i].type != 'resource' && data[i].type != 'scope') {
+ $scope.policies.push(data[i]);
+ }
+ }
+ });
+ },
+
+ onInitUpdate : function(policy) {
+ policy.config.applyPolicies = eval(policy.config.applyPolicies);
+ },
+
+ onUpdate : function() {
+ $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ },
+
+ onInitCreate : function(newPolicy) {
+ newPolicy.config = {};
+ newPolicy.decisionStrategy = 'UNANIMOUS';
+ },
+
+ onCreate : function() {
+ $scope.policy.config.applyPolicies = JSON.stringify($scope.policy.config.applyPolicies);
+ }
+ }, realm, client, $scope);
+});
+
+module.service("PolicyController", function($http, $route, $location, ResourceServer, ResourceServerPolicy, AuthzDialog, Notifications) {
+
+ var PolicyController = {};
+
+ PolicyController.onInit = function(delegate, realm, client, $scope) {
+ if (!delegate.isPermission) {
+ delegate.isPermission = function () {
+ return false;
+ }
+ }
+
+ $scope.realm = realm;
+ $scope.client = client;
+
+ $scope.decisionStrategies = ['AFFIRMATIVE', 'UNANIMOUS', 'CONSENSUS'];
+ $scope.logics = ['POSITIVE', 'NEGATIVE'];
+
+ delegate.onInit();
+
+ ResourceServer.get({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ $scope.server = data;
+
+ var policyId = $route.current.params.id;
+
+ if (!policyId) {
+ $scope.create = true;
+ $scope.changed = false;
+
+ var policy = {};
+
+ policy.type = delegate.getPolicyType();
+ policy.config = {};
+ policy.logic = 'POSITIVE';
+
+ if (delegate.onInitCreate) {
+ delegate.onInitCreate(policy);
+ }
+
+ $scope.policy = angular.copy(policy);
+
+ $scope.$watch('policy', function() {
+ if (!angular.equals($scope.policy, policy)) {
+ $scope.changed = true;
+ }
+ }, true);
+
+ $scope.save = function() {
+ if (delegate.onCreate) {
+ delegate.onCreate();
+ }
+ ResourceServerPolicy.save({realm : realm.realm, client : client.id}, $scope.policy, function(data) {
+ if (delegate.isPermission()) {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission/" + $scope.policy.type + "/" + data.id);
+ Notifications.success("The permission has been created.");
+ } else {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy/" + $scope.policy.type + "/" + data.id);
+ Notifications.success("The policy has been created.");
+ }
+ });
+ }
+
+ $scope.cancel = function() {
+ if (delegate.isPermission()) {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission/");
+ } else {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy/");
+ }
+ }
+ } else {
+ ResourceServerPolicy.get({
+ realm : $route.current.params.realm,
+ client : client.id,
+ id : $route.current.params.id,
+ }, function(data) {
+ var policy = angular.copy(data);
+
+ if (delegate.onInitUpdate) {
+ delegate.onInitUpdate(policy);
+ }
+
+ $scope.policy = angular.copy(policy);
+ $scope.changed = false;
+
+ $scope.$watch('policy', function() {
+ if (!angular.equals($scope.policy, policy)) {
+ $scope.changed = true;
+ }
+ }, true);
+
+ $scope.save = function() {
+ if (delegate.onUpdate) {
+ delegate.onUpdate();
+ }
+ ResourceServerPolicy.update({realm : realm.realm, client : client.id, id : $scope.policy.id}, $scope.policy, function() {
+ $route.reload();
+ if (delegate.isPermission()) {
+ Notifications.success("The permission has been updated.");
+ } else {
+ Notifications.success("The policy has been updated.");
+ }
+ });
+ }
+
+ $scope.reset = function() {
+ var freshPolicy = angular.copy(data);
+
+ if (delegate.onInitUpdate) {
+ delegate.onInitUpdate(freshPolicy);
+ }
+
+ $scope.policy = angular.copy(freshPolicy);
+ $scope.changed = false;
+ }
+ });
+
+ $scope.remove = function() {
+ var msg = "";
+
+ if ($scope.policy.dependentPolicies.length > 0) {
+ msg = "<p>This policy is being used by other policies:</p>";
+ msg += "<ul>";
+ for (i = 0; i < $scope.policy.dependentPolicies.length; i++) {
+ msg+= "<li><strong>" + $scope.policy.dependentPolicies[i].name + "</strong></li>";
+ }
+ msg += "</ul>";
+ msg += "<p>If you remove this policy, the policies above will be affected and will not be associated with this policy anymore.</p>";
+ }
+
+ AuthzDialog.confirmDeleteWithMsg($scope.policy.name, "Policy", msg, function() {
+ ResourceServerPolicy.delete({realm : $scope.realm.realm, client : $scope.client.id, id : $scope.policy.id}, null, function() {
+ if (delegate.isPermission()) {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/permission");
+ Notifications.success("The permission has been deleted.");
+ } else {
+ $location.url("/realms/" + realm.realm + "/clients/" + client.id + "/authz/resource-server/policy");
+ Notifications.success("The policy has been deleted.");
+ }
+ });
+ });
+ }
+ }
+ });
+ }
+
+ return PolicyController;
+});
+
+module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $location, realm, clients, roles, ResourceServer, client, ResourceServerResource, ResourceServerScope, User, Notifications) {
+ $scope.realm = realm;
+ $scope.client = client;
+ $scope.clients = clients;
+ $scope.roles = roles;
+ $scope.authzRequest = {};
+ $scope.authzRequest.resources = [];
+ $scope.authzRequest.context = {};
+ $scope.authzRequest.context.attributes = {};
+ $scope.authzRequest.roleIds = [];
+ $scope.newResource = {};
+ $scope.resultUrl = resourceUrl + '/partials/authz/policy/resource-server-policy-evaluate-result.html';
+
+ ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.scopes = data;
+ });
+
+ $scope.addContextAttribute = function() {
+ if (!$scope.newContextAttribute.value || $scope.newContextAttribute.value == '') {
+ Notifications.error("You must provide a value to a context attribute.");
+ return;
+ }
+
+ $scope.authzRequest.context.attributes[$scope.newContextAttribute.key] = $scope.newContextAttribute.value;
+ delete $scope.newContextAttribute;
+ }
+
+ $scope.removeContextAttribute = function(key) {
+ delete $scope.authzRequest.context.attributes[key];
+ }
+
+ $scope.getContextAttribute = function(key) {
+ for (i = 0; i < $scope.defaultContextAttributes.length; i++) {
+ if ($scope.defaultContextAttributes[i].key == key) {
+ return $scope.defaultContextAttributes[i];
+ }
+ }
+
+ return $scope.authzRequest.context.attributes[key];
+ }
+
+ $scope.getContextAttributeName = function(key) {
+ var attribute = $scope.getContextAttribute(key);
+
+ if (!attribute.name) {
+ return key;
+ }
+
+ return attribute.name;
+ }
+
+ $scope.defaultContextAttributes = [
+ {
+ key : "custom",
+ name : "Custom Attribute...",
+ custom: true
+ },
+ {
+ key : "kc.authz.context.authc.method",
+ name : "Authentication Method",
+ values: [
+ {
+ key : "pwd",
+ name : "Password"
+ },
+ {
+ key : "otp",
+ name : "One-Time Password"
+ },
+ {
+ key : "kbr",
+ name : "Kerberos"
+ }
+ ]
+ },
+ {
+ key : "kc.authz.context.authc.realm",
+ name : "Realm"
+ },
+ {
+ key : "kc.authz.context.time.date_time",
+ name : "Date/Time (MM/dd/yyyy hh:mm:ss)"
+ },
+ {
+ key : "kc.authz.context.client.network.ip_address",
+ name : "Client IPv4 Address"
+ },
+ {
+ key : "kc.authz.context.client.network.host",
+ name : "Client Host"
+ },
+ {
+ key : "kc.authz.context.client.user_agent",
+ name : "Client/User Agent"
+ }
+ ];
+
+ $scope.isDefaultContextAttribute = function() {
+ if (!$scope.newContextAttribute) {
+ return true;
+ }
+
+ if ($scope.newContextAttribute.custom) {
+ return false;
+ }
+
+ if (!$scope.getContextAttribute($scope.newContextAttribute.key).custom) {
+ return true;
+ }
+
+ return false;
+ }
+
+ $scope.selectDefaultContextAttribute = function() {
+ $scope.newContextAttribute = angular.copy($scope.newContextAttribute);
+ }
+
+ $scope.setApplyToResourceType = function() {
+ if ($scope.applyResourceType) {
+ ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.scopes = data;
+ });
+ }
+
+ delete $scope.newResource;
+ $scope.authzRequest.resources = [];
+ }
+
+ $scope.addResource = function() {
+ var resource = {};
+
+ resource.id = $scope.newResource._id;
+
+ for (i = 0; i < $scope.resources.length; i++) {
+ if ($scope.resources[i]._id == resource.id) {
+ resource.name = $scope.resources[i].name;
+ break;
+ }
+ }
+
+ resource.scopes = $scope.newResource.scopes;
+
+ $scope.authzRequest.resources.push(resource);
+
+ delete $scope.newResource;
+ }
+
+ $scope.removeResource = function(index) {
+ $scope.authzRequest.resources.splice(index, 1);
+ }
+
+ $scope.resolveScopes = function() {
+ if ($scope.newResource._id) {
+ $scope.newResource.scopes = [];
+ $scope.scopes = [];
+ ResourceServerResource.get({
+ realm: $route.current.params.realm,
+ client : client.id,
+ rsrid: $scope.newResource._id
+ }, function (data) {
+ $scope.scopes = data.scopes;
+ });
+ } else {
+ ResourceServerScope.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.scopes = data;
+ });
+ }
+ }
+
+ $scope.save = function() {
+ $scope.authzRequest.entitlements = false;
+ if ($scope.applyResourceType) {
+ if (!$scope.newResource) {
+ $scope.newResource = {};
+ }
+ $scope.authzRequest.resources[0].scopes = $scope.newResource.scopes;
+ }
+
+ $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/evaluate'
+ , $scope.authzRequest).success(function(data) {
+ $scope.evaluationResult = data;
+ $scope.showResultTab();
+ });
+ }
+
+ $scope.entitlements = function() {
+ $scope.authzRequest.entitlements = true;
+ $http.post(authUrl + '/admin/realms/'+ $route.current.params.realm + '/clients/' + client.id + '/authz/resource-server/policy/evaluate'
+ , $scope.authzRequest).success(function(data) {
+ $scope.evaluationResult = data;
+ $scope.showResultTab();
+ });
+ }
+
+ $scope.showResultTab = function() {
+ $scope.showResult = true;
+ }
+
+ $scope.showRequestTab = function() {
+ $scope.showResult = false;
+ }
+
+ User.query({realm: $route.current.params.realm}, function(data) {
+ $scope.users = data;
+ });
+
+ ResourceServerResource.query({realm : realm.realm, client : client.id}, function (data) {
+ $scope.resources = data;
+ });
+
+ ResourceServer.get({
+ realm : $route.current.params.realm,
+ client : client.id
+ }, function(data) {
+ $scope.server = data;
+ });
+});
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
new file mode 100644
index 0000000..c74db28
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-services.js
@@ -0,0 +1,117 @@
+module.factory('ResourceServer', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server', {
+ realm : '@realm',
+ client: '@client'
+ }, {
+ 'update' : {method : 'PUT'},
+ 'settings' : {url: authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/settings', method : 'GET'}
+ });
+});
+
+module.factory('ResourceServerResource', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/resource/:rsrid', {
+ realm : '@realm',
+ client: '@client',
+ rsrid : '@rsrid'
+ }, {
+ 'update' : {method : 'PUT'}
+ });
+});
+
+module.factory('ResourceServerScope', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/scope/:id', {
+ realm : '@realm',
+ client: '@client',
+ id : '@id'
+ }, {
+ 'update' : {method : 'PUT'}
+ });
+});
+
+module.factory('ResourceServerPolicy', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/:id', {
+ realm : '@realm',
+ client: '@client',
+ id : '@id'
+ }, {
+ 'update' : {method : 'PUT'}
+ });
+});
+
+module.factory('PolicyProvider', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/clients/:client/authz/resource-server/policy/providers', {
+ realm : '@realm',
+ client: '@client'
+ });
+});
+
+module.service('AuthzDialog', function($modal) {
+ var dialog = {};
+
+ var openDialog = function(title, message, btns, template) {
+ var controller = function($scope, $modalInstance, $sce, title, message, btns) {
+ $scope.title = title;
+ $scope.message = $sce.trustAsHtml(message);
+ $scope.btns = btns;
+
+ $scope.ok = function () {
+ $modalInstance.close();
+ };
+ $scope.cancel = function () {
+ $modalInstance.dismiss('cancel');
+ };
+ };
+
+ return $modal.open({
+ templateUrl: resourceUrl + template,
+ controller: controller,
+ resolve: {
+ title: function() {
+ return title;
+ },
+ message: function() {
+ return message;
+ },
+ btns: function() {
+ return btns;
+ }
+ }
+ }).result;
+ }
+
+ dialog.confirmDeleteWithMsg = function(name, type, msg, success) {
+ var title = 'Delete ' + type;
+ msg += 'Are you sure you want to permanently delete the ' + type + ' <strong>' + name + '</strong> ?';
+ var btns = {
+ ok: {
+ label: 'Delete',
+ cssClass: 'btn btn-danger'
+ },
+ cancel: {
+ label: 'Cancel',
+ cssClass: 'btn btn-default'
+ }
+ }
+
+ openDialog(title, msg, btns, '/templates/authz/kc-authz-modal.html').then(success);
+ };
+
+ dialog.confirmDelete = function(name, type, success) {
+ var title = 'Delete ' + type;
+ var msg = 'Are you sure you want to permanently delete the ' + type + ' <strong>' + name + '</strong> ?';
+ var btns = {
+ ok: {
+ label: 'Delete',
+ cssClass: 'btn btn-danger'
+ },
+ cancel: {
+ label: 'Cancel',
+ cssClass: 'btn btn-default'
+ }
+ }
+
+ openDialog(title, msg, btns, '/templates/authz/kc-authz-modal.html').then(success);
+ }
+
+ return dialog;
+});
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/ace.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/ace.js
new file mode 100644
index 0000000..e099651
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/ace.js
@@ -0,0 +1,11 @@
+(function(){function o(n){var i=e;n&&(e[n]||(e[n]={}),i=e[n]);if(!i.define||!i.define.packaged)t.original=i.define,i.define=t,i.define.packaged=!0;if(!i.require||!i.require.packaged)r.original=i.require,i.require=r,i.require.packaged=!0}var ACE_NAMESPACE="",e=function(){return this}();!e&&typeof window!="undefined"&&(e=window);if(!ACE_NAMESPACE&&typeof requirejs!="undefined")return;var t=function(e,n,r){if(typeof e!="string"){t.original?t.original.apply(this,arguments):(console.error("dropping module because define wasn't a string."),console.trace());return}arguments.length==2&&(r=n),t.modules[e]||(t.payloads[e]=r,t.modules[e]=null)};t.modules={},t.payloads={};var n=function(e,t,n){if(typeof t=="string"){var i=s(e,t);if(i!=undefined)return n&&n(),i}else if(Object.prototype.toString.call(t)==="[object Array]"){var o=[];for(var u=0,a=t.length;u<a;++u){var f=s(e,t[u]);if(f==undefined&&r.original)return;o.push(f)}return n&&n.apply(null,o)||!0}},r=function(e,t){var i=n("",e,t);return i==undefined&&r.original?r.original.apply(this,arguments):i},i=function(e,t){if(t.indexOf("!")!==-1){var n=t.split("!");return i(e,n[0])+"!"+i(e,n[1])}if(t.charAt(0)=="."){var r=e.split("/").slice(0,-1).join("/");t=r+"/"+t;while(t.indexOf(".")!==-1&&s!=t){var s=t;t=t.replace(/\/\.\//,"/").replace(/[^\/]+\/\.\.\//,"")}}return t},s=function(e,r){r=i(e,r);var s=t.modules[r];if(!s){s=t.payloads[r];if(typeof s=="function"){var o={},u={id:r,uri:"",exports:o,packaged:!0},a=function(e,t){return n(r,e,t)},f=s(a,o,u);o=f||u.exports,t.modules[r]=o,delete t.payloads[r]}s=t.modules[r]=o||s}return s};o(ACE_NAMESPACE)})(),define("ace/lib/regexp",["require","exports","module"],function(e,t,n){"use strict";function o(e){return(e.global?"g":"")+(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.extended?"x":"")+(e.sticky?"y":"")}function u(e,t,n){if(Array.prototype.indexOf)return e.indexOf(t,n);for(var r=n||0;r<e.length;r++)if(e[r]===t)return r;return-1}var r={exec:RegExp.prototype.exec,test:RegExp.prototype.test,match:String.prototype.match,replace:String.prototype.replace,split:String.prototype.split},i=r.exec.call(/()??/,"")[1]===undefined,s=function(){var e=/^/g;return r.test.call(e,""),!e.lastIndex}();if(s&&i)return;RegExp.prototype.exec=function(e){var t=r.exec.apply(this,arguments),n,a;if(typeof e=="string"&&t){!i&&t.length>1&&u(t,"")>-1&&(a=RegExp(this.source,r.replace.call(o(this),"g","")),r.replace.call(e.slice(t.index),a,function(){for(var e=1;e<arguments.length-2;e++)arguments[e]===undefined&&(t[e]=undefined)}));if(this._xregexp&&this._xregexp.captureNames)for(var f=1;f<t.length;f++)n=this._xregexp.captureNames[f-1],n&&(t[n]=t[f]);!s&&this.global&&!t[0].length&&this.lastIndex>t.index&&this.lastIndex--}return t},s||(RegExp.prototype.test=function(e){var t=r.exec.call(this,e);return t&&this.global&&!t[0].length&&this.lastIndex>t.index&&this.lastIndex--,!!t})}),define("ace/lib/es5-shim",["require","exports","module"],function(e,t,n){function r(){}function w(e){try{return Object.defineProperty(e,"sentinel",{}),"sentinel"in e}catch(t){}}function H(e){return e=+e,e!==e?e=0:e!==0&&e!==1/0&&e!==-1/0&&(e=(e>0||-1)*Math.floor(Math.abs(e))),e}function B(e){var t=typeof e;return e===null||t==="undefined"||t==="boolean"||t==="number"||t==="string"}function j(e){var t,n,r;if(B(e))return e;n=e.valueOf;if(typeof n=="function"){t=n.call(e);if(B(t))return t}r=e.toString;if(typeof r=="function"){t=r.call(e);if(B(t))return t}throw new TypeError}Function.prototype.bind||(Function.prototype.bind=function(t){var n=this;if(typeof n!="function")throw new TypeError("Function.prototype.bind called on incompatible "+n);var i=u.call(arguments,1),s=function(){if(this instanceof s){var e=n.apply(this,i.concat(u.call(arguments)));return Object(e)===e?e:this}return n.apply(t,i.concat(u.call(arguments)))};return n.prototype&&(r.prototype=n.prototype,s.prototype=new r,r.prototype=null),s});var i=Function.prototype.call,s=Array.prototype,o=Object.prototype,u=s.slice,a=i.bind(o.toString),f=i.bind(o.hasOwnProperty),l,c,h,p,d;if(d=f(o,"__defineGetter__"))l=i.bind(o.__defineGetter__),c=i.bind(o.__defineSetter__),h=i.bind(o.__lookupGetter__),p=i.bind(o.__lookupSetter__);if([1,2].splice(0).length!=2)if(!function(){function e(e){var t=new Array(e+2);return t[0]=t[1]=0,t}var t=[],n;t.splice.apply(t,e(20)),t.splice.apply(t,e(26)),n=t.length,t.splice(5,0,"XXX"),n+1==t.length;if(n+1==t.length)return!0}())Array.prototype.splice=function(e,t){var n=this.length;e>0?e>n&&(e=n):e==void 0?e=0:e<0&&(e=Math.max(n+e,0)),e+t<n||(t=n-e);var r=this.slice(e,e+t),i=u.call(arguments,2),s=i.length;if(e===n)s&&this.push.apply(this,i);else{var o=Math.min(t,n-e),a=e+o,f=a+s-o,l=n-a,c=n-o;if(f<a)for(var h=0;h<l;++h)this[f+h]=this[a+h];else if(f>a)for(h=l;h--;)this[f+h]=this[a+h];if(s&&e===c)this.length=c,this.push.apply(this,i);else{this.length=c+s;for(h=0;h<s;++h)this[e+h]=i[h]}}return r};else{var v=Array.prototype.splice;Array.prototype.splice=function(e,t){return arguments.length?v.apply(this,[e===void 0?0:e,t===void 0?this.length-e:t].concat(u.call(arguments,2))):[]}}Array.isArray||(Array.isArray=function(t){return a(t)=="[object Array]"});var m=Object("a"),g=m[0]!="a"||!(0 in m);Array.prototype.forEach||(Array.prototype.forEach=function(t){var n=F(this),r=g&&a(this)=="[object String]"?this.split(""):n,i=arguments[1],s=-1,o=r.length>>>0;if(a(t)!="[object Function]")throw new TypeError;while(++s<o)s in r&&t.call(i,r[s],s,n)}),Array.prototype.map||(Array.prototype.map=function(t){var n=F(this),r=g&&a(this)=="[object String]"?this.split(""):n,i=r.length>>>0,s=Array(i),o=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var u=0;u<i;u++)u in r&&(s[u]=t.call(o,r[u],u,n));return s}),Array.prototype.filter||(Array.prototype.filter=function(t){var n=F(this),r=g&&a(this)=="[object String]"?this.split(""):n,i=r.length>>>0,s=[],o,u=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var f=0;f<i;f++)f in r&&(o=r[f],t.call(u,o,f,n)&&s.push(o));return s}),Array.prototype.every||(Array.prototype.every=function(t){var n=F(this),r=g&&a(this)=="[object String]"?this.split(""):n,i=r.length>>>0,s=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var o=0;o<i;o++)if(o in r&&!t.call(s,r[o],o,n))return!1;return!0}),Array.prototype.some||(Array.prototype.some=function(t){var n=F(this),r=g&&a(this)=="[object String]"?this.split(""):n,i=r.length>>>0,s=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var o=0;o<i;o++)if(o in r&&t.call(s,r[o],o,n))return!0;return!1}),Array.prototype.reduce||(Array.prototype.reduce=function(t){var n=F(this),r=g&&a(this)=="[object String]"?this.split(""):n,i=r.length>>>0;if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");if(!i&&arguments.length==1)throw new TypeError("reduce of empty array with no initial value");var s=0,o;if(arguments.length>=2)o=arguments[1];else do{if(s in r){o=r[s++];break}if(++s>=i)throw new TypeError("reduce of empty array with no initial value")}while(!0);for(;s<i;s++)s in r&&(o=t.call(void 0,o,r[s],s,n));return o}),Array.prototype.reduceRight||(Array.prototype.reduceRight=function(t){var n=F(this),r=g&&a(this)=="[object String]"?this.split(""):n,i=r.length>>>0;if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");if(!i&&arguments.length==1)throw new TypeError("reduceRight of empty array with no initial value");var s,o=i-1;if(arguments.length>=2)s=arguments[1];else do{if(o in r){s=r[o--];break}if(--o<0)throw new TypeError("reduceRight of empty array with no initial value")}while(!0);do o in this&&(s=t.call(void 0,s,r[o],o,n));while(o--);return s});if(!Array.prototype.indexOf||[0,1].indexOf(1,2)!=-1)Array.prototype.indexOf=function(t){var n=g&&a(this)=="[object String]"?this.split(""):F(this),r=n.length>>>0;if(!r)return-1;var i=0;arguments.length>1&&(i=H(arguments[1])),i=i>=0?i:Math.max(0,r+i);for(;i<r;i++)if(i in n&&n[i]===t)return i;return-1};if(!Array.prototype.lastIndexOf||[0,1].lastIndexOf(0,-3)!=-1)Array.prototype.lastIndexOf=function(t){var n=g&&a(this)=="[object String]"?this.split(""):F(this),r=n.length>>>0;if(!r)return-1;var i=r-1;arguments.length>1&&(i=Math.min(i,H(arguments[1]))),i=i>=0?i:r-Math.abs(i);for(;i>=0;i--)if(i in n&&t===n[i])return i;return-1};Object.getPrototypeOf||(Object.getPrototypeOf=function(t){return t.__proto__||(t.constructor?t.constructor.prototype:o)});if(!Object.getOwnPropertyDescriptor){var y="Object.getOwnPropertyDescriptor called on a non-object: ";Object.getOwnPropertyDescriptor=function(t,n){if(typeof t!="object"&&typeof t!="function"||t===null)throw new TypeError(y+t);if(!f(t,n))return;var r,i,s;r={enumerable:!0,configurable:!0};if(d){var u=t.__proto__;t.__proto__=o;var i=h(t,n),s=p(t,n);t.__proto__=u;if(i||s)return i&&(r.get=i),s&&(r.set=s),r}return r.value=t[n],r}}Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(t){return Object.keys(t)});if(!Object.create){var b;Object.prototype.__proto__===null?b=function(){return{__proto__:null}}:b=function(){var e={};for(var t in e)e[t]=null;return e.constructor=e.hasOwnProperty=e.propertyIsEnumerable=e.isPrototypeOf=e.toLocaleString=e.toString=e.valueOf=e.__proto__=null,e},Object.create=function(t,n){var r;if(t===null)r=b();else{if(typeof t!="object")throw new TypeError("typeof prototype["+typeof t+"] != 'object'");var i=function(){};i.prototype=t,r=new i,r.__proto__=t}return n!==void 0&&Object.defineProperties(r,n),r}}if(Object.defineProperty){var E=w({}),S=typeof document=="undefined"||w(document.createElement("div"));if(!E||!S)var x=Object.defineProperty}if(!Object.defineProperty||x){var T="Property description must be an object: ",N="Object.defineProperty called on non-object: ",C="getters & setters can not be defined on this javascript engine";Object.defineProperty=function(t,n,r){if(typeof t!="object"&&typeof t!="function"||t===null)throw new TypeError(N+t);if(typeof r!="object"&&typeof r!="function"||r===null)throw new TypeError(T+r);if(x)try{return x.call(Object,t,n,r)}catch(i){}if(f(r,"value"))if(d&&(h(t,n)||p(t,n))){var s=t.__proto__;t.__proto__=o,delete t[n],t[n]=r.value,t.__proto__=s}else t[n]=r.value;else{if(!d)throw new TypeError(C);f(r,"get")&&l(t,n,r.get),f(r,"set")&&c(t,n,r.set)}return t}}Object.defineProperties||(Object.defineProperties=function(t,n){for(var r in n)f(n,r)&&Object.defineProperty(t,r,n[r]);return t}),Object.seal||(Object.seal=function(t){return t}),Object.freeze||(Object.freeze=function(t){return t});try{Object.freeze(function(){})}catch(k){Object.freeze=function(t){return function(n){return typeof n=="function"?n:t(n)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(t){return t}),Object.isSealed||(Object.isSealed=function(t){return!1}),Object.isFrozen||(Object.isFrozen=function(t){return!1}),Object.isExtensible||(Object.isExtensible=function(t){if(Object(t)===t)throw new TypeError;var n="";while(f(t,n))n+="?";t[n]=!0;var r=f(t,n);return delete t[n],r});if(!Object.keys){var L=!0,A=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],O=A.length;for(var M in{toString:null})L=!1;Object.keys=function I(e){if(typeof e!="object"&&typeof e!="function"||e===null)throw new TypeError("Object.keys called on a non-object");var I=[];for(var t in e)f(e,t)&&I.push(t);if(L)for(var n=0,r=O;n<r;n++){var i=A[n];f(e,i)&&I.push(i)}return I}}Date.now||(Date.now=function(){return(new Date).getTime()});var _=" \n\f\r \u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\ufeff";if(!String.prototype.trim||_.trim()){_="["+_+"]";var D=new RegExp("^"+_+_+"*"),P=new RegExp(_+_+"*$");String.prototype.trim=function(){return String(this).replace(D,"").replace(P,"")}}var F=function(e){if(e==null)throw new TypeError("can't convert "+e+" to object");return Object(e)}}),define("ace/lib/fixoldbrowsers",["require","exports","module","ace/lib/regexp","ace/lib/es5-shim"],function(e,t,n){"use strict";e("./regexp"),e("./es5-shim")}),define("ace/lib/dom",["require","exports","module"],function(e,t,n){"use strict";var r="http://www.w3.org/1999/xhtml";t.getDocumentHead=function(e){return e||(e=document),e.head||e.getElementsByTagName("head")[0]||e.documentElement},t.createElement=function(e,t){return document.createElementNS?document.createElementNS(t||r,e):document.createElement(e)},t.hasCssClass=function(e,t){var n=(e.className||"").split(/\s+/g);return n.indexOf(t)!==-1},t.addCssClass=function(e,n){t.hasCssClass(e,n)||(e.className+=" "+n)},t.removeCssClass=function(e,t){var n=e.className.split(/\s+/g);for(;;){var r=n.indexOf(t);if(r==-1)break;n.splice(r,1)}e.className=n.join(" ")},t.toggleCssClass=function(e,t){var n=e.className.split(/\s+/g),r=!0;for(;;){var i=n.indexOf(t);if(i==-1)break;r=!1,n.splice(i,1)}return r&&n.push(t),e.className=n.join(" "),r},t.setCssClass=function(e,n,r){r?t.addCssClass(e,n):t.removeCssClass(e,n)},t.hasCssString=function(e,t){var n=0,r;t=t||document;if(t.createStyleSheet&&(r=t.styleSheets)){while(n<r.length)if(r[n++].owningElement.id===e)return!0}else if(r=t.getElementsByTagName("style"))while(n<r.length)if(r[n++].id===e)return!0;return!1},t.importCssString=function(n,r,i){i=i||document;if(r&&t.hasCssString(r,i))return null;var s;r&&(n+="\n/*# sourceURL=ace/css/"+r+" */"),i.createStyleSheet?(s=i.createStyleSheet(),s.cssText=n,r&&(s.owningElement.id=r)):(s=t.createElement("style"),s.appendChild(i.createTextNode(n)),r&&(s.id=r),t.getDocumentHead(i).appendChild(s))},t.importCssStylsheet=function(e,n){if(n.createStyleSheet)n.createStyleSheet(e);else{var r=t.createElement("link");r.rel="stylesheet",r.href=e,t.getDocumentHead(n).appendChild(r)}},t.getInnerWidth=function(e){return parseInt(t.computedStyle(e,"paddingLeft"),10)+parseInt(t.computedStyle(e,"paddingRight"),10)+e.clientWidth},t.getInnerHeight=function(e){return parseInt(t.computedStyle(e,"paddingTop"),10)+parseInt(t.computedStyle(e,"paddingBottom"),10)+e.clientHeight},t.scrollbarWidth=function(e){var n=t.createElement("ace_inner");n.style.width="100%",n.style.minWidth="0px",n.style.height="200px",n.style.display="block";var r=t.createElement("ace_outer"),i=r.style;i.position="absolute",i.left="-10000px",i.overflow="hidden",i.width="200px",i.minWidth="0px",i.height="150px",i.display="block",r.appendChild(n);var s=e.documentElement;s.appendChild(r);var o=n.offsetWidth;i.overflow="scroll";var u=n.offsetWidth;return o==u&&(u=r.clientWidth),s.removeChild(r),o-u};if(typeof document=="undefined"){t.importCssString=function(){};return}window.pageYOffset!==undefined?(t.getPageScrollTop=function(){return window.pageYOffset},t.getPageScrollLeft=function(){return window.pageXOffset}):(t.getPageScrollTop=function(){return document.body.scrollTop},t.getPageScrollLeft=function(){return document.body.scrollLeft}),window.getComputedStyle?t.computedStyle=function(e,t){return t?(window.getComputedStyle(e,"")||{})[t]||"":window.getComputedStyle(e,"")||{}}:t.computedStyle=function(e,t){return t?e.currentStyle[t]:e.currentStyle},t.setInnerHtml=function(e,t){var n=e.cloneNode(!1);return n.innerHTML=t,e.parentNode.replaceChild(n,e),n},"textContent"in document.documentElement?(t.setInnerText=function(e,t){e.textContent=t},t.getInnerText=function(e){return e.textContent}):(t.setInnerText=function(e,t){e.innerText=t},t.getInnerText=function(e){return e.innerText}),t.getParentWindow=function(e){return e.defaultView||e.parentWindow}}),define("ace/lib/oop",["require","exports","module"],function(e,t,n){"use strict";t.inherits=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})},t.mixin=function(e,t){for(var n in t)e[n]=t[n];return e},t.implement=function(e,n){t.mixin(e,n)}}),define("ace/lib/keys",["require","exports","module","ace/lib/fixoldbrowsers","ace/lib/oop"],function(e,t,n){"use strict";e("./fixoldbrowsers");var r=e("./oop"),i=function(){var e={MODIFIER_KEYS:{16:"Shift",17:"Ctrl",18:"Alt",224:"Meta"},KEY_MODS:{ctrl:1,alt:2,option:2,shift:4,"super":8,meta:8,command:8,cmd:8},FUNCTION_KEYS:{8:"Backspace",9:"Tab",13:"Return",19:"Pause",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"Print",45:"Insert",46:"Delete",96:"Numpad0",97:"Numpad1",98:"Numpad2",99:"Numpad3",100:"Numpad4",101:"Numpad5",102:"Numpad6",103:"Numpad7",104:"Numpad8",105:"Numpad9","-13":"NumpadEnter",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"Numlock",145:"Scrolllock"},PRINTABLE_KEYS:{32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",61:"=",65:"a",66:"b",67:"c",68:"d",69:"e",70:"f",71:"g",72:"h",73:"i",74:"j",75:"k",76:"l",77:"m",78:"n",79:"o",80:"p",81:"q",82:"r",83:"s",84:"t",85:"u",86:"v",87:"w",88:"x",89:"y",90:"z",107:"+",109:"-",110:".",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",111:"/",106:"*"}},t,n;for(n in e.FUNCTION_KEYS)t=e.FUNCTION_KEYS[n].toLowerCase(),e[t]=parseInt(n,10);for(n in e.PRINTABLE_KEYS)t=e.PRINTABLE_KEYS[n].toLowerCase(),e[t]=parseInt(n,10);return r.mixin(e,e.MODIFIER_KEYS),r.mixin(e,e.PRINTABLE_KEYS),r.mixin(e,e.FUNCTION_KEYS),e.enter=e["return"],e.escape=e.esc,e.del=e["delete"],e[173]="-",function(){var t=["cmd","ctrl","alt","shift"];for(var n=Math.pow(2,t.length);n--;)e.KEY_MODS[n]=t.filter(function(t){return n&e.KEY_MODS[t]}).join("-")+"-"}(),e.KEY_MODS[0]="",e.KEY_MODS[-1]="input-",e}();r.mixin(t,i),t.keyCodeToString=function(e){var t=i[e];return typeof t!="string"&&(t=String.fromCharCode(e)),t.toLowerCase()}}),define("ace/lib/useragent",["require","exports","module"],function(e,t,n){"use strict";t.OS={LINUX:"LINUX",MAC:"MAC",WINDOWS:"WINDOWS"},t.getOS=function(){return t.isMac?t.OS.MAC:t.isLinux?t.OS.LINUX:t.OS.WINDOWS};if(typeof navigator!="object")return;var r=(navigator.platform.match(/mac|win|linux/i)||["other"])[0].toLowerCase(),i=navigator.userAgent;t.isWin=r=="win",t.isMac=r=="mac",t.isLinux=r=="linux",t.isIE=navigator.appName=="Microsoft Internet Explorer"||navigator.appName.indexOf("MSAppHost")>=0?parseFloat((i.match(/(?:MSIE |Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]):parseFloat((i.match(/(?:Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]),t.isOldIE=t.isIE&&t.isIE<9,t.isGecko=t.isMozilla=(window.Controllers||window.controllers)&&window.navigator.product==="Gecko",t.isOldGecko=t.isGecko&&parseInt((i.match(/rv\:(\d+)/)||[])[1],10)<4,t.isOpera=window.opera&&Object.prototype.toString.call(window.opera)=="[object Opera]",t.isWebKit=parseFloat(i.split("WebKit/")[1])||undefined,t.isChrome=parseFloat(i.split(" Chrome/")[1])||undefined,t.isAIR=i.indexOf("AdobeAIR")>=0,t.isIPad=i.indexOf("iPad")>=0,t.isTouchPad=i.indexOf("TouchPad")>=0,t.isChromeOS=i.indexOf(" CrOS ")>=0}),define("ace/lib/event",["require","exports","module","ace/lib/keys","ace/lib/useragent"],function(e,t,n){"use strict";function a(e,t,n){var a=u(t);if(!i.isMac&&s){s.OSKey&&(a|=8);if(s.altGr){if((3&a)==3)return;s.altGr=0}if(n===18||n===17){var f="location"in t?t.location:t.keyLocation;if(n===17&&f===1)s[n]==1&&(o=t.timeStamp);else if(n===18&&a===3&&f===2){var l=t.timeStamp-o;l<50&&(s.altGr=!0)}}}n in r.MODIFIER_KEYS&&(n=-1),a&8&&n>=91&&n<=93&&(n=-1);if(!a&&n===13){var f="location"in t?t.location:t.keyLocation;if(f===3){e(t,a,-n);if(t.defaultPrevented)return}}if(i.isChromeOS&&a&8){e(t,a,n);if(t.defaultPrevented)return;a&=-9}return!!a||n in r.FUNCTION_KEYS||n in r.PRINTABLE_KEYS?e(t,a,n):!1}function f(){s=Object.create(null),s.count=0,s.lastT=0}var r=e("./keys"),i=e("./useragent"),s=null,o=0;t.addListener=function(e,t,n){if(e.addEventListener)return e.addEventListener(t,n,!1);if(e.attachEvent){var r=function(){n.call(e,window.event)};n._wrapper=r,e.attachEvent("on"+t,r)}},t.removeListener=function(e,t,n){if(e.removeEventListener)return e.removeEventListener(t,n,!1);e.detachEvent&&e.detachEvent("on"+t,n._wrapper||n)},t.stopEvent=function(e){return t.stopPropagation(e),t.preventDefault(e),!1},t.stopPropagation=function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0},t.preventDefault=function(e){e.preventDefault?e.preventDefault():e.returnValue=!1},t.getButton=function(e){return e.type=="dblclick"?0:e.type=="contextmenu"||i.isMac&&e.ctrlKey&&!e.altKey&&!e.shiftKey?2:e.preventDefault?e.button:{1:0,2:2,4:1}[e.button]},t.capture=function(e,n,r){function i(e){n&&n(e),r&&r(e),t.removeListener(document,"mousemove",n,!0),t.removeListener(document,"mouseup",i,!0),t.removeListener(document,"dragstart",i,!0)}return t.addListener(document,"mousemove",n,!0),t.addListener(document,"mouseup",i,!0),t.addListener(document,"dragstart",i,!0),i},t.addTouchMoveListener=function(e,n){if("ontouchmove"in e){var r,i;t.addListener(e,"touchstart",function(e){var t=e.changedTouches[0];r=t.clientX,i=t.clientY}),t.addListener(e,"touchmove",function(e){var t=1,s=e.changedTouches[0];e.wheelX=-(s.clientX-r)/t,e.wheelY=-(s.clientY-i)/t,r=s.clientX,i=s.clientY,n(e)})}},t.addMouseWheelListener=function(e,n){"onmousewheel"in e?t.addListener(e,"mousewheel",function(e){var t=8;e.wheelDeltaX!==undefined?(e.wheelX=-e.wheelDeltaX/t,e.wheelY=-e.wheelDeltaY/t):(e.wheelX=0,e.wheelY=-e.wheelDelta/t),n(e)}):"onwheel"in e?t.addListener(e,"wheel",function(e){var t=.35;switch(e.deltaMode){case e.DOM_DELTA_PIXEL:e.wheelX=e.deltaX*t||0,e.wheelY=e.deltaY*t||0;break;case e.DOM_DELTA_LINE:case e.DOM_DELTA_PAGE:e.wheelX=(e.deltaX||0)*5,e.wheelY=(e.deltaY||0)*5}n(e)}):t.addListener(e,"DOMMouseScroll",function(e){e.axis&&e.axis==e.HORIZONTAL_AXIS?(e.wheelX=(e.detail||0)*5,e.wheelY=0):(e.wheelX=0,e.wheelY=(e.detail||0)*5),n(e)})},t.addMultiMouseDownListener=function(e,n,r,s){function c(e){t.getButton(e)!==0?o=0:e.detail>1?(o++,o>4&&(o=1)):o=1;if(i.isIE){var c=Math.abs(e.clientX-u)>5||Math.abs(e.clientY-a)>5;if(!f||c)o=1;f&&clearTimeout(f),f=setTimeout(function(){f=null},n[o-1]||600),o==1&&(u=e.clientX,a=e.clientY)}e._clicks=o,r[s]("mousedown",e);if(o>4)o=0;else if(o>1)return r[s](l[o],e)}function h(e){o=2,f&&clearTimeout(f),f=setTimeout(function(){f=null},n[o-1]||600),r[s]("mousedown",e),r[s](l[o],e)}var o=0,u,a,f,l={2:"dblclick",3:"tripleclick",4:"quadclick"};Array.isArray(e)||(e=[e]),e.forEach(function(e){t.addListener(e,"mousedown",c),i.isOldIE&&t.addListener(e,"dblclick",h)})};var u=!i.isMac||!i.isOpera||"KeyboardEvent"in window?function(e){return 0|(e.ctrlKey?1:0)|(e.altKey?2:0)|(e.shiftKey?4:0)|(e.metaKey?8:0)}:function(e){return 0|(e.metaKey?1:0)|(e.altKey?2:0)|(e.shiftKey?4:0)|(e.ctrlKey?8:0)};t.getModifierString=function(e){return r.KEY_MODS[u(e)]},t.addCommandKeyListener=function(e,n){var r=t.addListener;if(i.isOldGecko||i.isOpera&&!("KeyboardEvent"in window)){var o=null;r(e,"keydown",function(e){o=e.keyCode}),r(e,"keypress",function(e){return a(n,e,o)})}else{var u=null;r(e,"keydown",function(e){var t=e.keyCode;s[t]=(s[t]||0)+1,t==91||t==92?s.OSKey=!0:s.OSKey&&e.timeStamp-s.lastT>200&&s.count==1&&f(),s[t]==1&&s.count++,s.lastT=e.timeStamp;var r=a(n,e,t);return u=e.defaultPrevented,r}),r(e,"keypress",function(e){u&&(e.ctrlKey||e.altKey||e.shiftKey||e.metaKey)&&(t.stopEvent(e),u=null)}),r(e,"keyup",function(e){var t=e.keyCode;s[t]?s.count=Math.max(s.count-1,0):f();if(t==91||t==92)s.OSKey=!1;s[t]=null}),s||(f(),r(window,"focus",f))}};if(typeof window=="object"&&window.postMessage&&!i.isOldIE){var l=1;t.nextTick=function(e,n){n=n||window;var r="zero-timeout-message-"+l;t.addListener(n,"message",function i(s){s.data==r&&(t.stopPropagation(s),t.removeListener(n,"message",i),e())}),n.postMessage(r,"*")}}t.nextFrame=typeof window=="object"&&(window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame),t.nextFrame?t.nextFrame=t.nextFrame.bind(window):t.nextFrame=function(e){setTimeout(e,17)}}),define("ace/lib/lang",["require","exports","module"],function(e,t,n){"use strict";t.last=function(e){return e[e.length-1]},t.stringReverse=function(e){return e.split("").reverse().join("")},t.stringRepeat=function(e,t){var n="";while(t>0){t&1&&(n+=e);if(t>>=1)e+=e}return n};var r=/^\s\s*/,i=/\s\s*$/;t.stringTrimLeft=function(e){return e.replace(r,"")},t.stringTrimRight=function(e){return e.replace(i,"")},t.copyObject=function(e){var t={};for(var n in e)t[n]=e[n];return t},t.copyArray=function(e){var t=[];for(var n=0,r=e.length;n<r;n++)e[n]&&typeof e[n]=="object"?t[n]=this.copyObject(e[n]):t[n]=e[n];return t},t.deepCopy=function s(e){if(typeof e!="object"||!e)return e;var t;if(Array.isArray(e)){t=[];for(var n=0;n<e.length;n++)t[n]=s(e[n]);return t}var r=e.constructor;if(r===RegExp)return e;t=r();for(var n in e)t[n]=s(e[n]);return t},t.arrayToMap=function(e){var t={};for(var n=0;n<e.length;n++)t[e[n]]=1;return t},t.createMap=function(e){var t=Object.create(null);for(var n in e)t[n]=e[n];return t},t.arrayRemove=function(e,t){for(var n=0;n<=e.length;n++)t===e[n]&&e.splice(n,1)},t.escapeRegExp=function(e){return e.replace(/([.*+?^${}()|[\]\/\\])/g,"\\$1")},t.escapeHTML=function(e){return e.replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(/</g,"<")},t.getMatchOffsets=function(e,t){var n=[];return e.replace(t,function(e){n.push({offset:arguments[arguments.length-2],length:e.length})}),n},t.deferredCall=function(e){var t=null,n=function(){t=null,e()},r=function(e){return r.cancel(),t=setTimeout(n,e||0),r};return r.schedule=r,r.call=function(){return this.cancel(),e(),r},r.cancel=function(){return clearTimeout(t),t=null,r},r.isPending=function(){return t},r},t.delayedCall=function(e,t){var n=null,r=function(){n=null,e()},i=function(e){n==null&&(n=setTimeout(r,e||t))};return i.delay=function(e){n&&clearTimeout(n),n=setTimeout(r,e||t)},i.schedule=i,i.call=function(){this.cancel(),e()},i.cancel=function(){n&&clearTimeout(n),n=null},i.isPending=function(){return n},i}}),define("ace/keyboard/textinput",["require","exports","module","ace/lib/event","ace/lib/useragent","ace/lib/dom","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=e("../lib/dom"),o=e("../lib/lang"),u=i.isChrome<18,a=i.isIE,f=function(e,t){function b(e){if(h)return;h=!0;if(k)t=0,r=e?0:n.value.length-1;else var t=e?2:1,r=2;try{n.setSelectionRange(t,r)}catch(i){}h=!1}function w(){if(h)return;n.value=f,i.isWebKit&&y.schedule()}function R(){clearTimeout(q),q=setTimeout(function(){p&&(n.style.cssText=p,p=""),t.renderer.$keepTextAreaAtCursor==null&&(t.renderer.$keepTextAreaAtCursor=!0,t.renderer.$moveTextAreaToCursor())},i.isOldIE?200:0)}var n=s.createElement("textarea");n.className="ace_text-input",i.isTouchPad&&n.setAttribute("x-palm-disable-auto-cap",!0),n.setAttribute("wrap","off"),n.setAttribute("autocorrect","off"),n.setAttribute("autocapitalize","off"),n.setAttribute("spellcheck",!1),n.style.opacity="0",i.isOldIE&&(n.style.top="-1000px"),e.insertBefore(n,e.firstChild);var f="",l=!1,c=!1,h=!1,p="",d=!0;try{var v=document.activeElement===n}catch(m){}r.addListener(n,"blur",function(e){t.onBlur(e),v=!1}),r.addListener(n,"focus",function(e){v=!0,t.onFocus(e),b()}),this.focus=function(){if(p)return n.focus();var e=n.style.top;n.style.position="fixed",n.style.top="0px",n.focus(),setTimeout(function(){n.style.position="",n.style.top=="0px"&&(n.style.top=e)},0)},this.blur=function(){n.blur()},this.isFocused=function(){return v};var g=o.delayedCall(function(){v&&b(d)}),y=o.delayedCall(function(){h||(n.value=f,v&&b())});i.isWebKit||t.addEventListener("changeSelection",function(){t.selection.isEmpty()!=d&&(d=!d,g.schedule())}),w(),v&&t.onFocus();var E=function(e){return e.selectionStart===0&&e.selectionEnd===e.value.length};!n.setSelectionRange&&n.createTextRange&&(n.setSelectionRange=function(e,t){var n=this.createTextRange();n.collapse(!0),n.moveStart("character",e),n.moveEnd("character",t),n.select()},E=function(e){try{var t=e.ownerDocument.selection.createRange()}catch(n){}return!t||t.parentElement()!=e?!1:t.text==e.value});if(i.isOldIE){var S=!1,x=function(e){if(S)return;var t=n.value;if(h||!t||t==f)return;if(e&&t==f[0])return T.schedule();A(t),S=!0,w(),S=!1},T=o.delayedCall(x);r.addListener(n,"propertychange",x);var N={13:1,27:1};r.addListener(n,"keyup",function(e){h&&(!n.value||N[e.keyCode])&&setTimeout(F,0);if((n.value.charCodeAt(0)||0)<129)return T.call();h?j():B()}),r.addListener(n,"keydown",function(e){T.schedule(50)})}var C=function(e){l?l=!1:E(n)?(t.selectAll(),b()):k&&b(t.selection.isEmpty())},k=null;this.setInputHandler=function(e){k=e},this.getInputHandler=function(){return k};var L=!1,A=function(e){k&&(e=k(e),k=null),c?(b(),e&&t.onPaste(e),c=!1):e==f.charAt(0)?L?t.execCommand("del",{source:"ace"}):t.execCommand("backspace",{source:"ace"}):(e.substring(0,2)==f?e=e.substr(2):e.charAt(0)==f.charAt(0)?e=e.substr(1):e.charAt(e.length-1)==f.charAt(0)&&(e=e.slice(0,-1)),e.charAt(e.length-1)==f.charAt(0)&&(e=e.slice(0,-1)),e&&t.onTextInput(e)),L&&(L=!1)},O=function(e){if(h)return;var t=n.value;A(t),w()},M=function(e,t){var n=e.clipboardData||window.clipboardData;if(!n||u)return;var r=a?"Text":"text/plain";return t?n.setData(r,t)!==!1:n.getData(r)},_=function(e,i){var s=t.getCopyText();if(!s)return r.preventDefault(e);M(e,s)?(i?t.onCut():t.onCopy(),r.preventDefault(e)):(l=!0,n.value=s,n.select(),setTimeout(function(){l=!1,w(),b(),i?t.onCut():t.onCopy()}))},D=function(e){_(e,!0)},P=function(e){_(e,!1)},H=function(e){var s=M(e);typeof s=="string"?(s&&t.onPaste(s,e),i.isIE&&setTimeout(b),r.preventDefault(e)):(n.value="",c=!0)};r.addCommandKeyListener(n,t.onCommandKey.bind(t)),r.addListener(n,"select",C),r.addListener(n,"input",O),r.addListener(n,"cut",D),r.addListener(n,"copy",P),r.addListener(n,"paste",H),(!("oncut"in n)||!("oncopy"in n)||!("onpaste"in n))&&r.addListener(e,"keydown",function(e){if(i.isMac&&!e.metaKey||!e.ctrlKey)return;switch(e.keyCode){case 67:P(e);break;case 86:H(e);break;case 88:D(e)}});var B=function(e){if(h||!t.onCompositionStart||t.$readOnly)return;h={},t.onCompositionStart(),setTimeout(j,0),t.on("mousedown",F),t.selection.isEmpty()||(t.insert(""),t.session.markUndoGroup(),t.selection.clearSelection()),t.session.markUndoGroup()},j=function(){if(!h||!t.onCompositionUpdate||t.$readOnly)return;var e=n.value.replace(/\x01/g,"");if(h.lastValue===e)return;t.onCompositionUpdate(e),h.lastValue&&t.undo(),h.lastValue=e;if(h.lastValue){var r=t.selection.getRange();t.insert(h.lastValue),t.session.markUndoGroup(),h.range=t.selection.getRange(),t.selection.setRange(r),t.selection.clearSelection()}},F=function(e){if(!t.onCompositionEnd||t.$readOnly)return;var r=h;h=!1;var i=setTimeout(function(){i=null;var e=n.value.replace(/\x01/g,"");if(h)return;e==r.lastValue?w():!r.lastValue&&e&&(w(),A(e))});k=function(n){return i&&clearTimeout(i),n=n.replace(/\x01/g,""),n==r.lastValue?"":(r.lastValue&&i&&t.undo(),n)},t.onCompositionEnd(),t.removeListener("mousedown",F),e.type=="compositionend"&&r.range&&t.selection.setRange(r.range)},I=o.delayedCall(j,50);r.addListener(n,"compositionstart",B),i.isGecko?r.addListener(n,"text",function(){I.schedule()}):(r.addListener(n,"keyup",function(){I.schedule()}),r.addListener(n,"keydown",function(){I.schedule()})),r.addListener(n,"compositionend",F),this.getElement=function(){return n},this.setReadOnly=function(e){n.readOnly=e},this.onContextMenu=function(e){L=!0,b(t.selection.isEmpty()),t._emit("nativecontextmenu",{target:t,domEvent:e}),this.moveToMouse(e,!0)},this.moveToMouse=function(e,o){if(!o&&i.isOldIE)return;p||(p=n.style.cssText),n.style.cssText=(o?"z-index:100000;":"")+"height:"+n.style.height+";"+(i.isIE?"opacity:0.1;":"");var u=t.container.getBoundingClientRect(),a=s.computedStyle(t.container),f=u.top+(parseInt(a.borderTopWidth)||0),l=u.left+(parseInt(u.borderLeftWidth)||0),c=u.bottom-f-n.clientHeight-2,h=function(e){n.style.left=e.clientX-l-2+"px",n.style.top=Math.min(e.clientY-f-2,c)+"px"};h(e);if(e.type!="mousedown")return;t.renderer.$keepTextAreaAtCursor&&(t.renderer.$keepTextAreaAtCursor=null),clearTimeout(q),i.isWin&&!i.isOldIE&&r.capture(t.container,h,R)},this.onContextMenuClose=R;var q,U=function(e){t.textInput.onContextMenu(e),R()};r.addListener(n,"mouseup",U),r.addListener(n,"mousedown",function(e){e.preventDefault(),R()}),r.addListener(t.renderer.scroller,"contextmenu",U),r.addListener(n,"contextmenu",U)};t.TextInput=f}),define("ace/mouse/default_handlers",["require","exports","module","ace/lib/dom","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";function u(e){e.$clickSelection=null;var t=e.editor;t.setDefaultHandler("mousedown",this.onMouseDown.bind(e)),t.setDefaultHandler("dblclick",this.onDoubleClick.bind(e)),t.setDefaultHandler("tripleclick",this.onTripleClick.bind(e)),t.setDefaultHandler("quadclick",this.onQuadClick.bind(e)),t.setDefaultHandler("mousewheel",this.onMouseWheel.bind(e)),t.setDefaultHandler("touchmove",this.onTouchMove.bind(e));var n=["select","startSelect","selectEnd","selectAllEnd","selectByWordsEnd","selectByLinesEnd","dragWait","dragWaitEnd","focusWait"];n.forEach(function(t){e[t]=this[t]},this),e.selectByLines=this.extendSelectionBy.bind(e,"getLineRange"),e.selectByWords=this.extendSelectionBy.bind(e,"getWordRange")}function a(e,t,n,r){return Math.sqrt(Math.pow(n-e,2)+Math.pow(r-t,2))}function f(e,t){if(e.start.row==e.end.row)var n=2*t.column-e.start.column-e.end.column;else if(e.start.row==e.end.row-1&&!e.start.column&&!e.end.column)var n=t.column-4;else var n=2*t.row-e.start.row-e.end.row;return n<0?{cursor:e.start,anchor:e.end}:{cursor:e.end,anchor:e.start}}var r=e("../lib/dom"),i=e("../lib/event"),s=e("../lib/useragent"),o=0;(function(){this.onMouseDown=function(e){var t=e.inSelection(),n=e.getDocumentPosition();this.mousedownEvent=e;var r=this.editor,i=e.getButton();if(i!==0){var s=r.getSelectionRange(),o=s.isEmpty();r.$blockScrolling++,(o||i==1)&&r.selection.moveToPosition(n),r.$blockScrolling--,i==2&&r.textInput.onContextMenu(e.domEvent);return}this.mousedownEvent.time=Date.now();if(t&&!r.isFocused()){r.focus();if(this.$focusTimout&&!this.$clickSelection&&!r.inMultiSelectMode){this.setState("focusWait"),this.captureMouse(e);return}}return this.captureMouse(e),this.startSelect(n,e.domEvent._clicks>1),e.preventDefault()},this.startSelect=function(e,t){e=e||this.editor.renderer.screenToTextCoordinates(this.x,this.y);var n=this.editor;n.$blockScrolling++,this.mousedownEvent.getShiftKey()?n.selection.selectToPosition(e):t||n.selection.moveToPosition(e),t||this.select(),n.renderer.scroller.setCapture&&n.renderer.scroller.setCapture(),n.setStyle("ace_selecting"),this.setState("select"),n.$blockScrolling--},this.select=function(){var e,t=this.editor,n=t.renderer.screenToTextCoordinates(this.x,this.y);t.$blockScrolling++;if(this.$clickSelection){var r=this.$clickSelection.comparePoint(n);if(r==-1)e=this.$clickSelection.end;else if(r==1)e=this.$clickSelection.start;else{var i=f(this.$clickSelection,n);n=i.cursor,e=i.anchor}t.selection.setSelectionAnchor(e.row,e.column)}t.selection.selectToPosition(n),t.$blockScrolling--,t.renderer.scrollCursorIntoView()},this.extendSelectionBy=function(e){var t,n=this.editor,r=n.renderer.screenToTextCoordinates(this.x,this.y),i=n.selection[e](r.row,r.column);n.$blockScrolling++;if(this.$clickSelection){var s=this.$clickSelection.comparePoint(i.start),o=this.$clickSelection.comparePoint(i.end);if(s==-1&&o<=0){t=this.$clickSelection.end;if(i.end.row!=r.row||i.end.column!=r.column)r=i.start}else if(o==1&&s>=0){t=this.$clickSelection.start;if(i.start.row!=r.row||i.start.column!=r.column)r=i.end}else if(s==-1&&o==1)r=i.end,t=i.start;else{var u=f(this.$clickSelection,r);r=u.cursor,t=u.anchor}n.selection.setSelectionAnchor(t.row,t.column)}n.selection.selectToPosition(r),n.$blockScrolling--,n.renderer.scrollCursorIntoView()},this.selectEnd=this.selectAllEnd=this.selectByWordsEnd=this.selectByLinesEnd=function(){this.$clickSelection=null,this.editor.unsetStyle("ace_selecting"),this.editor.renderer.scroller.releaseCapture&&this.editor.renderer.scroller.releaseCapture()},this.focusWait=function(){var e=a(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y),t=Date.now();(e>o||t-this.mousedownEvent.time>this.$focusTimout)&&this.startSelect(this.mousedownEvent.getDocumentPosition())},this.onDoubleClick=function(e){var t=e.getDocumentPosition(),n=this.editor,r=n.session,i=r.getBracketRange(t);i?(i.isEmpty()&&(i.start.column--,i.end.column++),this.setState("select")):(i=n.selection.getWordRange(t.row,t.column),this.setState("selectByWords")),this.$clickSelection=i,this.select()},this.onTripleClick=function(e){var t=e.getDocumentPosition(),n=this.editor;this.setState("selectByLines");var r=n.getSelectionRange();r.isMultiLine()&&r.contains(t.row,t.column)?(this.$clickSelection=n.selection.getLineRange(r.start.row),this.$clickSelection.end=n.selection.getLineRange(r.end.row).end):this.$clickSelection=n.selection.getLineRange(t.row),this.select()},this.onQuadClick=function(e){var t=this.editor;t.selectAll(),this.$clickSelection=t.getSelectionRange(),this.setState("selectAll")},this.onMouseWheel=function(e){if(e.getAccelKey())return;e.getShiftKey()&&e.wheelY&&!e.wheelX&&(e.wheelX=e.wheelY,e.wheelY=0);var t=e.domEvent.timeStamp,n=t-(this.$lastScrollTime||0),r=this.editor,i=r.renderer.isScrollableBy(e.wheelX*e.speed,e.wheelY*e.speed);if(i||n<200)return this.$lastScrollTime=t,r.renderer.scrollBy(e.wheelX*e.speed,e.wheelY*e.speed),e.stop()},this.onTouchMove=function(e){var t=e.domEvent.timeStamp,n=t-(this.$lastScrollTime||0),r=this.editor,i=r.renderer.isScrollableBy(e.wheelX*e.speed,e.wheelY*e.speed);if(i||n<200)return this.$lastScrollTime=t,r.renderer.scrollBy(e.wheelX*e.speed,e.wheelY*e.speed),e.stop()}}).call(u.prototype),t.DefaultHandlers=u}),define("ace/tooltip",["require","exports","module","ace/lib/oop","ace/lib/dom"],function(e,t,n){"use strict";function s(e){this.isOpen=!1,this.$element=null,this.$parentNode=e}var r=e("./lib/oop"),i=e("./lib/dom");(function(){this.$init=function(){return this.$element=i.createElement("div"),this.$element.className="ace_tooltip",this.$element.style.display="none",this.$parentNode.appendChild(this.$element),this.$element},this.getElement=function(){return this.$element||this.$init()},this.setText=function(e){i.setInnerText(this.getElement(),e)},this.setHtml=function(e){this.getElement().innerHTML=e},this.setPosition=function(e,t){this.getElement().style.left=e+"px",this.getElement().style.top=t+"px"},this.setClassName=function(e){i.addCssClass(this.getElement(),e)},this.show=function(e,t,n){e!=null&&this.setText(e),t!=null&&n!=null&&this.setPosition(t,n),this.isOpen||(this.getElement().style.display="block",this.isOpen=!0)},this.hide=function(){this.isOpen&&(this.getElement().style.display="none",this.isOpen=!1)},this.getHeight=function(){return this.getElement().offsetHeight},this.getWidth=function(){return this.getElement().offsetWidth}}).call(s.prototype),t.Tooltip=s}),define("ace/mouse/default_gutter_handler",["require","exports","module","ace/lib/dom","ace/lib/oop","ace/lib/event","ace/tooltip"],function(e,t,n){"use strict";function u(e){function l(){var r=u.getDocumentPosition().row,s=n.$annotations[r];if(!s)return c();var o=t.session.getLength();if(r==o){var a=t.renderer.pixelToScreenCoordinates(0,u.y).row,l=u.$pos;if(a>t.session.documentToScreenRow(l.row,l.column))return c()}if(f==s)return;f=s.text.join("<br/>"),i.setHtml(f),i.show(),t.on("mousewheel",c);if(e.$tooltipFollowsMouse)h(u);else{var p=u.domEvent.target,d=p.getBoundingClientRect(),v=i.getElement().style;v.left=d.right+"px",v.top=d.bottom+"px"}}function c(){o&&(o=clearTimeout(o)),f&&(i.hide(),f=null,t.removeEventListener("mousewheel",c))}function h(e){i.setPosition(e.x,e.y)}var t=e.editor,n=t.renderer.$gutterLayer,i=new a(t.container);e.editor.setDefaultHandler("guttermousedown",function(r){if(!t.isFocused()||r.getButton()!=0)return;var i=n.getRegion(r);if(i=="foldWidgets")return;var s=r.getDocumentPosition().row,o=t.session.selection;if(r.getShiftKey())o.selectTo(s,0);else{if(r.domEvent.detail==2)return t.selectAll(),r.preventDefault();e.$clickSelection=t.selection.getLineRange(s)}return e.setState("selectByLines"),e.captureMouse(r),r.preventDefault()});var o,u,f;e.editor.setDefaultHandler("guttermousemove",function(t){var n=t.domEvent.target||t.domEvent.srcElement;if(r.hasCssClass(n,"ace_fold-widget"))return c();f&&e.$tooltipFollowsMouse&&h(t),u=t;if(o)return;o=setTimeout(function(){o=null,u&&!e.isMousePressed?l():c()},50)}),s.addListener(t.renderer.$gutter,"mouseout",function(e){u=null;if(!f||o)return;o=setTimeout(function(){o=null,c()},50)}),t.on("changeSession",c)}function a(e){o.call(this,e)}var r=e("../lib/dom"),i=e("../lib/oop"),s=e("../lib/event"),o=e("../tooltip").Tooltip;i.inherits(a,o),function(){this.setPosition=function(e,t){var n=window.innerWidth||document.documentElement.clientWidth,r=window.innerHeight||document.documentElement.clientHeight,i=this.getWidth(),s=this.getHeight();e+=15,t+=15,e+i>n&&(e-=e+i-n),t+s>r&&(t-=20+s),o.prototype.setPosition.call(this,e,t)}}.call(a.prototype),t.GutterHandler=u}),define("ace/mouse/mouse_event",["require","exports","module","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=t.MouseEvent=function(e,t){this.domEvent=e,this.editor=t,this.x=this.clientX=e.clientX,this.y=this.clientY=e.clientY,this.$pos=null,this.$inSelection=null,this.propagationStopped=!1,this.defaultPrevented=!1};(function(){this.stopPropagation=function(){r.stopPropagation(this.domEvent),this.propagationStopped=!0},this.preventDefault=function(){r.preventDefault(this.domEvent),this.defaultPrevented=!0},this.stop=function(){this.stopPropagation(),this.preventDefault()},this.getDocumentPosition=function(){return this.$pos?this.$pos:(this.$pos=this.editor.renderer.screenToTextCoordinates(this.clientX,this.clientY),this.$pos)},this.inSelection=function(){if(this.$inSelection!==null)return this.$inSelection;var e=this.editor,t=e.getSelectionRange();if(t.isEmpty())this.$inSelection=!1;else{var n=this.getDocumentPosition();this.$inSelection=t.contains(n.row,n.column)}return this.$inSelection},this.getButton=function(){return r.getButton(this.domEvent)},this.getShiftKey=function(){return this.domEvent.shiftKey},this.getAccelKey=i.isMac?function(){return this.domEvent.metaKey}:function(){return this.domEvent.ctrlKey}}).call(s.prototype)}),define("ace/mouse/dragdrop_handler",["require","exports","module","ace/lib/dom","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";function f(e){function T(e,n){var r=Date.now(),i=!n||e.row!=n.row,s=!n||e.column!=n.column;if(!S||i||s)t.$blockScrolling+=1,t.moveCursorToPosition(e),t.$blockScrolling-=1,S=r,x={x:p,y:d};else{var o=l(x.x,x.y,p,d);o>a?S=null:r-S>=u&&(t.renderer.scrollCursorIntoView(),S=null)}}function N(e,n){var r=Date.now(),i=t.renderer.layerConfig.lineHeight,s=t.renderer.layerConfig.characterWidth,u=t.renderer.scroller.getBoundingClientRect(),a={x:{left:p-u.left,right:u.right-p},y:{top:d-u.top,bottom:u.bottom-d}},f=Math.min(a.x.left,a.x.right),l=Math.min(a.y.top,a.y.bottom),c={row:e.row,column:e.column};f/s<=2&&(c.column+=a.x.left<a.x.right?-3:2),l/i<=1&&(c.row+=a.y.top<a.y.bottom?-1:1);var h=e.row!=c.row,v=e.column!=c.column,m=!n||e.row!=n.row;h||v&&!m?E?r-E>=o&&t.renderer.scrollCursorIntoView(c):E=r:E=null}function C(){var e=g;g=t.renderer.screenToTextCoordinates(p,d),T(g,e),N(g,e)}function k(){m=t.selection.toOrientedRange(),h=t.session.addMarker(m,"ace_selection",t.getSelectionStyle()),t.clearSelection(),t.isFocused()&&t.renderer.$cursorLayer.setBlinking(!1),clearInterval(v),C(),v=setInterval(C,20),y=0,i.addListener(document,"mousemove",O)}function L(){clearInterval(v),t.session.removeMarker(h),h=null,t.$blockScrolling+=1,t.selection.fromOrientedRange(m),t.$blockScrolling-=1,t.isFocused()&&!w&&t.renderer.$cursorLayer.setBlinking(!t.getReadOnly()),m=null,g=null,y=0,E=null,S=null,i.removeListener(document,"mousemove",O)}function O(){A==null&&(A=setTimeout(function(){A!=null&&h&&L()},20))}function M(e){var t=e.types;return!t||Array.prototype.some.call(t,function(e){return e=="text/plain"||e=="Text"})}function _(e){var t=["copy","copymove","all","uninitialized"],n=["move","copymove","linkmove","all","uninitialized"],r=s.isMac?e.altKey:e.ctrlKey,i="uninitialized";try{i=e.dataTransfer.effectAllowed.toLowerCase()}catch(e){}var o="none";return r&&t.indexOf(i)>=0?o="copy":n.indexOf(i)>=0?o="move":t.indexOf(i)>=0&&(o="copy"),o}var t=e.editor,n=r.createElement("img");n.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",s.isOpera&&(n.style.cssText="width:1px;height:1px;position:fixed;top:0;left:0;z-index:2147483647;opacity:0;");var f=["dragWait","dragWaitEnd","startDrag","dragReadyEnd","onMouseDrag"];f.forEach(function(t){e[t]=this[t]},this),t.addEventListener("mousedown",this.onMouseDown.bind(e));var c=t.container,h,p,d,v,m,g,y=0,b,w,E,S,x;this.onDragStart=function(e){if(this.cancelDrag||!c.draggable){var r=this;return setTimeout(function(){r.startSelect(),r.captureMouse(e)},0),e.preventDefault()}m=t.getSelectionRange();var i=e.dataTransfer;i.effectAllowed=t.getReadOnly()?"copy":"copyMove",s.isOpera&&(t.container.appendChild(n),n.scrollTop=0),i.setDragImage&&i.setDragImage(n,0,0),s.isOpera&&t.container.removeChild(n),i.clearData(),i.setData("Text",t.session.getTextRange()),w=!0,this.setState("drag")},this.onDragEnd=function(e){c.draggable=!1,w=!1,this.setState(null);if(!t.getReadOnly()){var n=e.dataTransfer.dropEffect;!b&&n=="move"&&t.session.remove(t.getSelectionRange()),t.renderer.$cursorLayer.setBlinking(!0)}this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle("")},this.onDragEnter=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||k(),y++,e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragOver=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||(k(),y++),A!==null&&(A=null),e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragLeave=function(e){y--;if(y<=0&&h)return L(),b=null,i.preventDefault(e)},this.onDrop=function(e){if(!g)return;var n=e.dataTransfer;if(w)switch(b){case"move":m.contains(g.row,g.column)?m={start:g,end:g}:m=t.moveText(m,g);break;case"copy":m=t.moveText(m,g,!0)}else{var r=n.getData("Text");m={start:g,end:t.session.insert(g,r)},t.focus(),b=null}return L(),i.preventDefault(e)},i.addListener(c,"dragstart",this.onDragStart.bind(e)),i.addListener(c,"dragend",this.onDragEnd.bind(e)),i.addListener(c,"dragenter",this.onDragEnter.bind(e)),i.addListener(c,"dragover",this.onDragOver.bind(e)),i.addListener(c,"dragleave",this.onDragLeave.bind(e)),i.addListener(c,"drop",this.onDrop.bind(e));var A=null}function l(e,t,n,r){return Math.sqrt(Math.pow(n-e,2)+Math.pow(r-t,2))}var r=e("../lib/dom"),i=e("../lib/event"),s=e("../lib/useragent"),o=200,u=200,a=5;(function(){this.dragWait=function(){var e=Date.now()-this.mousedownEvent.time;e>this.editor.getDragDelay()&&this.startDrag()},this.dragWaitEnd=function(){var e=this.editor.container;e.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()),this.selectEnd()},this.dragReadyEnd=function(e){this.editor.renderer.$cursorLayer.setBlinking(!this.editor.getReadOnly()),this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle(""),this.dragWaitEnd()},this.startDrag=function(){this.cancelDrag=!1;var e=this.editor,t=e.container;t.draggable=!0,e.renderer.$cursorLayer.setBlinking(!1),e.setStyle("ace_dragging");var n=s.isWin?"default":"move";e.renderer.setCursorStyle(n),this.setState("dragReady")},this.onMouseDrag=function(e){var t=this.editor.container;if(s.isIE&&this.state=="dragReady"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>3&&t.dragDrop()}if(this.state==="dragWait"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>0&&(t.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()))}},this.onMouseDown=function(e){if(!this.$dragEnabled)return;this.mousedownEvent=e;var t=this.editor,n=e.inSelection(),r=e.getButton(),i=e.domEvent.detail||1;if(i===1&&r===0&&n){if(e.editor.inMultiSelectMode&&(e.getAccelKey()||e.getShiftKey()))return;this.mousedownEvent.time=Date.now();var o=e.domEvent.target||e.domEvent.srcElement;"unselectable"in o&&(o.unselectable="on");if(t.getDragDelay()){if(s.isWebKit){this.cancelDrag=!0;var u=t.container;u.draggable=!0}this.setState("dragWait")}else this.startDrag();this.captureMouse(e,this.onMouseDrag.bind(this)),e.defaultPrevented=!0}}}).call(f.prototype),t.DragdropHandler=f}),define("ace/lib/net",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("./dom");t.get=function(e,t){var n=new XMLHttpRequest;n.open("GET",e,!0),n.onreadystatechange=function(){n.readyState===4&&t(n.responseText)},n.send(null)},t.loadScript=function(e,t){var n=r.getDocumentHead(),i=document.createElement("script");i.src=e,n.appendChild(i),i.onload=i.onreadystatechange=function(e,n){if(n||!i.readyState||i.readyState=="loaded"||i.readyState=="complete")i=i.onload=i.onreadystatechange=null,n||t()}},t.qualifyURL=function(e){var t=document.createElement("a");return t.href=e,t.href}}),define("ace/lib/event_emitter",["require","exports","module"],function(e,t,n){"use strict";var r={},i=function(){this.propagationStopped=!0},s=function(){this.defaultPrevented=!0};r._emit=r._dispatchEvent=function(e,t){this._eventRegistry||(this._eventRegistry={}),this._defaultHandlers||(this._defaultHandlers={});var n=this._eventRegistry[e]||[],r=this._defaultHandlers[e];if(!n.length&&!r)return;if(typeof t!="object"||!t)t={};t.type||(t.type=e),t.stopPropagation||(t.stopPropagation=i),t.preventDefault||(t.preventDefault=s),n=n.slice();for(var o=0;o<n.length;o++){n[o](t,this);if(t.propagationStopped)break}if(r&&!t.defaultPrevented)return r(t,this)},r._signal=function(e,t){var n=(this._eventRegistry||{})[e];if(!n)return;n=n.slice();for(var r=0;r<n.length;r++)n[r](t,this)},r.once=function(e,t){var n=this;t&&this.addEventListener(e,function r(){n.removeEventListener(e,r),t.apply(null,arguments)})},r.setDefaultHandler=function(e,t){var n=this._defaultHandlers;n||(n=this._defaultHandlers={_disabled_:{}});if(n[e]){var r=n[e],i=n._disabled_[e];i||(n._disabled_[e]=i=[]),i.push(r);var s=i.indexOf(t);s!=-1&&i.splice(s,1)}n[e]=t},r.removeDefaultHandler=function(e,t){var n=this._defaultHandlers;if(!n)return;var r=n._disabled_[e];if(n[e]==t){var i=n[e];r&&this.setDefaultHandler(e,r.pop())}else if(r){var s=r.indexOf(t);s!=-1&&r.splice(s,1)}},r.on=r.addEventListener=function(e,t,n){this._eventRegistry=this._eventRegistry||{};var r=this._eventRegistry[e];return r||(r=this._eventRegistry[e]=[]),r.indexOf(t)==-1&&r[n?"unshift":"push"](t),t},r.off=r.removeListener=r.removeEventListener=function(e,t){this._eventRegistry=this._eventRegistry||{};var n=this._eventRegistry[e];if(!n)return;var r=n.indexOf(t);r!==-1&&n.splice(r,1)},r.removeAllListeners=function(e){this._eventRegistry&&(this._eventRegistry[e]=[])},t.EventEmitter=r}),define("ace/lib/app_config",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"],function(e,t,n){"no use strict";function o(e){typeof console!="undefined"&&console.warn&&console.warn.apply(console,arguments)}function u(e,t){var n=new Error(e);n.data=t,typeof console=="object"&&console.error&&console.error(n),setTimeout(function(){throw n})}var r=e("./oop"),i=e("./event_emitter").EventEmitter,s={setOptions:function(e){Object.keys(e).forEach(function(t){this.setOption(t,e[t])},this)},getOptions:function(e){var t={};return e?Array.isArray(e)||(t=e,e=Object.keys(t)):e=Object.keys(this.$options),e.forEach(function(e){t[e]=this.getOption(e)},this),t},setOption:function(e,t){if(this["$"+e]===t)return;var n=this.$options[e];if(!n)return o('misspelled option "'+e+'"');if(n.forwardTo)return this[n.forwardTo]&&this[n.forwardTo].setOption(e,t);n.handlesSet||(this["$"+e]=t),n&&n.set&&n.set.call(this,t)},getOption:function(e){var t=this.$options[e];return t?t.forwardTo?this[t.forwardTo]&&this[t.forwardTo].getOption(e):t&&t.get?t.get.call(this):this["$"+e]:o('misspelled option "'+e+'"')}},a=function(){this.$defaultOptions={}};(function(){r.implement(this,i),this.defineOptions=function(e,t,n){return e.$options||(this.$defaultOptions[t]=e.$options={}),Object.keys(n).forEach(function(t){var r=n[t];typeof r=="string"&&(r={forwardTo:r}),r.name||(r.name=t),e.$options[r.name]=r,"initialValue"in r&&(e["$"+r.name]=r.initialValue)}),r.implement(e,s),this},this.resetOptions=function(e){Object.keys(e.$options).forEach(function(t){var n=e.$options[t];"value"in n&&e.setOption(t,n.value)})},this.setDefaultValue=function(e,t,n){var r=this.$defaultOptions[e]||(this.$defaultOptions[e]={});r[t]&&(r.forwardTo?this.setDefaultValue(r.forwardTo,t,n):r[t].value=n)},this.setDefaultValues=function(e,t){Object.keys(t).forEach(function(n){this.setDefaultValue(e,n,t[n])},this)},this.warn=o,this.reportError=u}).call(a.prototype),t.AppConfig=a}),define("ace/config",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/lib/net","ace/lib/app_config"],function(e,t,n){"no use strict";function f(r){if(!u||!u.document)return;a.packaged=r||e.packaged||n.packaged||u.define&&define.packaged;var i={},s="",o=document.currentScript||document._currentScript,f=o&&o.ownerDocument||document,c=f.getElementsByTagName("script");for(var h=0;h<c.length;h++){var p=c[h],d=p.src||p.getAttribute("src");if(!d)continue;var v=p.attributes;for(var m=0,g=v.length;m<g;m++){var y=v[m];y.name.indexOf("data-ace-")===0&&(i[l(y.name.replace(/^data-ace-/,""))]=y.value)}var b=d.match(/^(.*)\/ace(\-\w+)?\.js(\?|$)/);b&&(s=b[1])}s&&(i.base=i.base||s,i.packaged=!0),i.basePath=i.base,i.workerPath=i.workerPath||i.base,i.modePath=i.modePath||i.base,i.themePath=i.themePath||i.base,delete i.base;for(var w in i)typeof i[w]!="undefined"&&t.set(w,i[w])}function l(e){return e.replace(/-(.)/g,function(e,t){return t.toUpperCase()})}var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./lib/net"),o=e("./lib/app_config").AppConfig;n.exports=t=new o;var u=function(){return this||typeof window!="undefined"&&window}(),a={packaged:!1,workerPath:null,modePath:null,themePath:null,basePath:"",suffix:".js",$moduleUrls:{}};t.get=function(e){if(!a.hasOwnProperty(e))throw new Error("Unknown config key: "+e);return a[e]},t.set=function(e,t){if(!a.hasOwnProperty(e))throw new Error("Unknown config key: "+e);a[e]=t},t.all=function(){return r.copyObject(a)},t.moduleUrl=function(e,t){if(a.$moduleUrls[e])return a.$moduleUrls[e];var n=e.split("/");t=t||n[n.length-2]||"";var r=t=="snippets"?"/":"-",i=n[n.length-1];if(t=="worker"&&r=="-"){var s=new RegExp("^"+t+"[\\-_]|[\\-_]"+t+"$","g");i=i.replace(s,"")}(!i||i==t)&&n.length>1&&(i=n[n.length-2]);var o=a[t+"Path"];return o==null?o=a.basePath:r=="/"&&(t=r=""),o&&o.slice(-1)!="/"&&(o+="/"),o+t+r+i+this.get("suffix")},t.setModuleUrl=function(e,t){return a.$moduleUrls[e]=t},t.$loading={},t.loadModule=function(n,r){var i,o;Array.isArray(n)&&(o=n[0],n=n[1]);try{i=e(n)}catch(u){}if(i&&!t.$loading[n])return r&&r(i);t.$loading[n]||(t.$loading[n]=[]),t.$loading[n].push(r);if(t.$loading[n].length>1)return;var a=function(){e([n],function(e){t._emit("load.module",{name:n,module:e});var r=t.$loading[n];t.$loading[n]=null,r.forEach(function(t){t&&t(e)})})};if(!t.get("packaged"))return a();s.loadScript(t.moduleUrl(n,o),a)},t.init=f}),define("ace/mouse/mouse_handler",["require","exports","module","ace/lib/event","ace/lib/useragent","ace/mouse/default_handlers","ace/mouse/default_gutter_handler","ace/mouse/mouse_event","ace/mouse/dragdrop_handler","ace/config"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=e("./default_handlers").DefaultHandlers,o=e("./default_gutter_handler").GutterHandler,u=e("./mouse_event").MouseEvent,a=e("./dragdrop_handler").DragdropHandler,f=e("../config"),l=function(e){var t=this;this.editor=e,new s(this),new o(this),new a(this);var n=function(t){var n=!document.hasFocus||!document.hasFocus()||!e.isFocused()&&document.activeElement==(e.textInput&&e.textInput.getElement());n&&window.focus(),e.focus()},u=e.renderer.getMouseEventTarget();r.addListener(u,"click",this.onMouseEvent.bind(this,"click")),r.addListener(u,"mousemove",this.onMouseMove.bind(this,"mousemove")),r.addMultiMouseDownListener([u,e.renderer.scrollBarV&&e.renderer.scrollBarV.inner,e.renderer.scrollBarH&&e.renderer.scrollBarH.inner,e.textInput&&e.textInput.getElement()].filter(Boolean),[400,300,250],this,"onMouseEvent"),r.addMouseWheelListener(e.container,this.onMouseWheel.bind(this,"mousewheel")),r.addTouchMoveListener(e.container,this.onTouchMove.bind(this,"touchmove"));var f=e.renderer.$gutter;r.addListener(f,"mousedown",this.onMouseEvent.bind(this,"guttermousedown")),r.addListener(f,"click",this.onMouseEvent.bind(this,"gutterclick")),r.addListener(f,"dblclick",this.onMouseEvent.bind(this,"gutterdblclick")),r.addListener(f,"mousemove",this.onMouseEvent.bind(this,"guttermousemove")),r.addListener(u,"mousedown",n),r.addListener(f,"mousedown",n),i.isIE&&e.renderer.scrollBarV&&(r.addListener(e.renderer.scrollBarV.element,"mousedown",n),r.addListener(e.renderer.scrollBarH.element,"mousedown",n)),e.on("mousemove",function(n){if(t.state||t.$dragDelay||!t.$dragEnabled)return;var r=e.renderer.screenToTextCoordinates(n.x,n.y),i=e.session.selection.getRange(),s=e.renderer;!i.isEmpty()&&i.insideStart(r.row,r.column)?s.setCursorStyle("default"):s.setCursorStyle("")})};(function(){this.onMouseEvent=function(e,t){this.editor._emit(e,new u(t,this.editor))},this.onMouseMove=function(e,t){var n=this.editor._eventRegistry&&this.editor._eventRegistry.mousemove;if(!n||!n.length)return;this.editor._emit(e,new u(t,this.editor))},this.onMouseWheel=function(e,t){var n=new u(t,this.editor);n.speed=this.$scrollSpeed*2,n.wheelX=t.wheelX,n.wheelY=t.wheelY,this.editor._emit(e,n)},this.onTouchMove=function(e,t){var n=new u(t,this.editor);n.speed=1,n.wheelX=t.wheelX,n.wheelY=t.wheelY,this.editor._emit(e,n)},this.setState=function(e){this.state=e},this.captureMouse=function(e,t){this.x=e.x,this.y=e.y,this.isMousePressed=!0;var n=this.editor.renderer;n.$keepTextAreaAtCursor&&(n.$keepTextAreaAtCursor=null);var s=this,o=function(e){if(!e)return;if(i.isWebKit&&!e.which&&s.releaseMouse)return s.releaseMouse();s.x=e.clientX,s.y=e.clientY,t&&t(e),s.mouseEvent=new u(e,s.editor),s.$mouseMoved=!0},a=function(e){clearInterval(l),f(),s[s.state+"End"]&&s[s.state+"End"](e),s.state="",n.$keepTextAreaAtCursor==null&&(n.$keepTextAreaAtCursor=!0,n.$moveTextAreaToCursor()),s.isMousePressed=!1,s.$onCaptureMouseMove=s.releaseMouse=null,e&&s.onMouseEvent("mouseup",e)},f=function(){s[s.state]&&s[s.state](),s.$mouseMoved=!1};if(i.isOldIE&&e.domEvent.type=="dblclick")return setTimeout(function(){a(e)});s.$onCaptureMouseMove=o,s.releaseMouse=r.capture(this.editor.container,o,a);var l=setInterval(f,20)},this.releaseMouse=null,this.cancelContextMenu=function(){var e=function(t){if(t&&t.domEvent&&t.domEvent.type!="contextmenu")return;this.editor.off("nativecontextmenu",e),t&&t.domEvent&&r.stopEvent(t.domEvent)}.bind(this);setTimeout(e,10),this.editor.on("nativecontextmenu",e)}}).call(l.prototype),f.defineOptions(l.prototype,"mouseHandler",{scrollSpeed:{initialValue:2},dragDelay:{initialValue:i.isMac?150:0},dragEnabled:{initialValue:!0},focusTimout:{initialValue:0},tooltipFollowsMouse:{initialValue:!0}}),t.MouseHandler=l}),define("ace/mouse/fold_handler",["require","exports","module"],function(e,t,n){"use strict";function r(e){e.on("click",function(t){var n=t.getDocumentPosition(),r=e.session,i=r.getFoldAt(n.row,n.column,1);i&&(t.getAccelKey()?r.removeFold(i):r.expandFold(i),t.stop())}),e.on("gutterclick",function(t){var n=e.renderer.$gutterLayer.getRegion(t);if(n=="foldWidgets"){var r=t.getDocumentPosition().row,i=e.session;i.foldWidgets&&i.foldWidgets[r]&&e.session.onFoldWidgetClick(r,t),e.isFocused()||e.focus(),t.stop()}}),e.on("gutterdblclick",function(t){var n=e.renderer.$gutterLayer.getRegion(t);if(n=="foldWidgets"){var r=t.getDocumentPosition().row,i=e.session,s=i.getParentFoldRangeData(r,!0),o=s.range||s.firstRange;if(o){r=o.start.row;var u=i.getFoldAt(r,i.getLine(r).length,1);u?i.removeFold(u):(i.addFold("...",o),e.renderer.scrollCursorIntoView({row:o.start.row,column:0}))}t.stop()}})}t.FoldHandler=r}),define("ace/keyboard/keybinding",["require","exports","module","ace/lib/keys","ace/lib/event"],function(e,t,n){"use strict";var r=e("../lib/keys"),i=e("../lib/event"),s=function(e){this.$editor=e,this.$data={editor:e},this.$handlers=[],this.setDefaultHandler(e.commands)};(function(){this.setDefaultHandler=function(e){this.removeKeyboardHandler(this.$defaultHandler),this.$defaultHandler=e,this.addKeyboardHandler(e,0)},this.setKeyboardHandler=function(e){var t=this.$handlers;if(t[t.length-1]==e)return;while(t[t.length-1]&&t[t.length-1]!=this.$defaultHandler)this.removeKeyboardHandler(t[t.length-1]);this.addKeyboardHandler(e,1)},this.addKeyboardHandler=function(e,t){if(!e)return;typeof e=="function"&&!e.handleKeyboard&&(e.handleKeyboard=e);var n=this.$handlers.indexOf(e);n!=-1&&this.$handlers.splice(n,1),t==undefined?this.$handlers.push(e):this.$handlers.splice(t,0,e),n==-1&&e.attach&&e.attach(this.$editor)},this.removeKeyboardHandler=function(e){var t=this.$handlers.indexOf(e);return t==-1?!1:(this.$handlers.splice(t,1),e.detach&&e.detach(this.$editor),!0)},this.getKeyboardHandler=function(){return this.$handlers[this.$handlers.length-1]},this.getStatusText=function(){var e=this.$data,t=e.editor;return this.$handlers.map(function(n){return n.getStatusText&&n.getStatusText(t,e)||""}).filter(Boolean).join(" ")},this.$callKeyboardHandlers=function(e,t,n,r){var s,o=!1,u=this.$editor.commands;for(var a=this.$handlers.length;a--;){s=this.$handlers[a].handleKeyboard(this.$data,e,t,n,r);if(!s||!s.command)continue;s.command=="null"?o=!0:o=u.exec(s.command,this.$editor,s.args,r),o&&r&&e!=-1&&s.passEvent!=1&&s.command.passEvent!=1&&i.stopEvent(r);if(o)break}return!o&&e==-1&&(s={command:"insertstring"},o=u.exec("insertstring",this.$editor,t)),o&&this.$editor._signal("keyboardActivity",s),o},this.onCommandKey=function(e,t,n){var i=r.keyCodeToString(n);this.$callKeyboardHandlers(t,i,n,e)},this.onTextInput=function(e){this.$callKeyboardHandlers(-1,e)}}).call(s.prototype),t.KeyBinding=s}),define("ace/range",["require","exports","module"],function(e,t,n){"use strict";var r=function(e,t){return e.row-t.row||e.column-t.column},i=function(e,t,n,r){this.start={row:e,column:t},this.end={row:n,column:r}};(function(){this.isEqual=function(e){return this.start.row===e.start.row&&this.end.row===e.end.row&&this.start.column===e.start.column&&this.end.column===e.end.column},this.toString=function(){return"Range: ["+this.start.row+"/"+this.start.column+"] -> ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(e,t){return this.compare(e,t)==0},this.compareRange=function(e){var t,n=e.end,r=e.start;return t=this.compare(n.row,n.column),t==1?(t=this.compare(r.row,r.column),t==1?2:t==0?1:0):t==-1?-2:(t=this.compare(r.row,r.column),t==-1?-1:t==1?42:0)},this.comparePoint=function(e){return this.compare(e.row,e.column)},this.containsRange=function(e){return this.comparePoint(e.start)==0&&this.comparePoint(e.end)==0},this.intersects=function(e){var t=this.compareRange(e);return t==-1||t==0||t==1},this.isEnd=function(e,t){return this.end.row==e&&this.end.column==t},this.isStart=function(e,t){return this.start.row==e&&this.start.column==t},this.setStart=function(e,t){typeof e=="object"?(this.start.column=e.column,this.start.row=e.row):(this.start.row=e,this.start.column=t)},this.setEnd=function(e,t){typeof e=="object"?(this.end.column=e.column,this.end.row=e.row):(this.end.row=e,this.end.column=t)},this.inside=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)||this.isStart(e,t)?!1:!0:!1},this.insideStart=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)?!1:!0:!1},this.insideEnd=function(e,t){return this.compare(e,t)==0?this.isStart(e,t)?!1:!0:!1},this.compare=function(e,t){return!this.isMultiLine()&&e===this.start.row?t<this.start.column?-1:t>this.end.column?1:0:e<this.start.row?-1:e>this.end.row?1:this.start.row===e?t>=this.start.column?0:-1:this.end.row===e?t<=this.end.column?0:1:0},this.compareStart=function(e,t){return this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.compareEnd=function(e,t){return this.end.row==e&&this.end.column==t?1:this.compare(e,t)},this.compareInside=function(e,t){return this.end.row==e&&this.end.column==t?1:this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.clipRows=function(e,t){if(this.end.row>t)var n={row:t+1,column:0};else if(this.end.row<e)var n={row:e,column:0};if(this.start.row>t)var r={row:t+1,column:0};else if(this.start.row<e)var r={row:e,column:0};return i.fromPoints(r||this.start,n||this.end)},this.extend=function(e,t){var n=this.compare(e,t);if(n==0)return this;if(n==-1)var r={row:e,column:t};else var s={row:e,column:t};return i.fromPoints(r||this.start,s||this.end)},this.isEmpty=function(){return this.start.row===this.end.row&&this.start.column===this.end.column},this.isMultiLine=function(){return this.start.row!==this.end.row},this.clone=function(){return i.fromPoints(this.start,this.end)},this.collapseRows=function(){return this.end.column==0?new i(this.start.row,0,Math.max(this.start.row,this.end.row-1),0):new i(this.start.row,0,this.end.row,0)},this.toScreenRange=function(e){var t=e.documentToScreenPosition(this.start),n=e.documentToScreenPosition(this.end);return new i(t.row,t.column,n.row,n.column)},this.moveBy=function(e,t){this.start.row+=e,this.start.column+=t,this.end.row+=e,this.end.column+=t}}).call(i.prototype),i.fromPoints=function(e,t){return new i(e.row,e.column,t.row,t.column)},i.comparePoints=r,i.comparePoints=function(e,t){return e.row-t.row||e.column-t.column},t.Range=i}),define("ace/selection",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/lib/event_emitter","ace/range"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/lang"),s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=function(e){this.session=e,this.doc=e.getDocument(),this.clearSelection(),this.lead=this.selectionLead=this.doc.createAnchor(0,0),this.anchor=this.selectionAnchor=this.doc.createAnchor(0,0);var t=this;this.lead.on("change",function(e){t._emit("changeCursor"),t.$isEmpty||t._emit("changeSelection"),!t.$keepDesiredColumnOnChange&&e.old.column!=e.value.column&&(t.$desiredColumn=null)}),this.selectionAnchor.on("change",function(){t.$isEmpty||t._emit("changeSelection")})};(function(){r.implement(this,s),this.isEmpty=function(){return this.$isEmpty||this.anchor.row==this.lead.row&&this.anchor.column==this.lead.column},this.isMultiLine=function(){return this.isEmpty()?!1:this.getRange().isMultiLine()},this.getCursor=function(){return this.lead.getPosition()},this.setSelectionAnchor=function(e,t){this.anchor.setPosition(e,t),this.$isEmpty&&(this.$isEmpty=!1,this._emit("changeSelection"))},this.getSelectionAnchor=function(){return this.$isEmpty?this.getSelectionLead():this.anchor.getPosition()},this.getSelectionLead=function(){return this.lead.getPosition()},this.shiftSelection=function(e){if(this.$isEmpty){this.moveCursorTo(this.lead.row,this.lead.column+e);return}var t=this.getSelectionAnchor(),n=this.getSelectionLead(),r=this.isBackwards();(!r||t.column!==0)&&this.setSelectionAnchor(t.row,t.column+e),(r||n.column!==0)&&this.$moveSelection(function(){this.moveCursorTo(n.row,n.column+e)})},this.isBackwards=function(){var e=this.anchor,t=this.lead;return e.row>t.row||e.row==t.row&&e.column>t.column},this.getRange=function(){var e=this.anchor,t=this.lead;return this.isEmpty()?o.fromPoints(t,t):this.isBackwards()?o.fromPoints(t,e):o.fromPoints(e,t)},this.clearSelection=function(){this.$isEmpty||(this.$isEmpty=!0,this._emit("changeSelection"))},this.selectAll=function(){var e=this.doc.getLength()-1;this.setSelectionAnchor(0,0),this.moveCursorTo(e,this.doc.getLine(e).length)},this.setRange=this.setSelectionRange=function(e,t){t?(this.setSelectionAnchor(e.end.row,e.end.column),this.selectTo(e.start.row,e.start.column)):(this.setSelectionAnchor(e.start.row,e.start.column),this.selectTo(e.end.row,e.end.column)),this.getRange().isEmpty()&&(this.$isEmpty=!0),this.$desiredColumn=null},this.$moveSelection=function(e){var t=this.lead;this.$isEmpty&&this.setSelectionAnchor(t.row,t.column),e.call(this)},this.selectTo=function(e,t){this.$moveSelection(function(){this.moveCursorTo(e,t)})},this.selectToPosition=function(e){this.$moveSelection(function(){this.moveCursorToPosition(e)})},this.moveTo=function(e,t){this.clearSelection(),this.moveCursorTo(e,t)},this.moveToPosition=function(e){this.clearSelection(),this.moveCursorToPosition(e)},this.selectUp=function(){this.$moveSelection(this.moveCursorUp)},this.selectDown=function(){this.$moveSelection(this.moveCursorDown)},this.selectRight=function(){this.$moveSelection(this.moveCursorRight)},this.selectLeft=function(){this.$moveSelection(this.moveCursorLeft)},this.selectLineStart=function(){this.$moveSelection(this.moveCursorLineStart)},this.selectLineEnd=function(){this.$moveSelection(this.moveCursorLineEnd)},this.selectFileEnd=function(){this.$moveSelection(this.moveCursorFileEnd)},this.selectFileStart=function(){this.$moveSelection(this.moveCursorFileStart)},this.selectWordRight=function(){this.$moveSelection(this.moveCursorWordRight)},this.selectWordLeft=function(){this.$moveSelection(this.moveCursorWordLeft)},this.getWordRange=function(e,t){if(typeof t=="undefined"){var n=e||this.lead;e=n.row,t=n.column}return this.session.getWordRange(e,t)},this.selectWord=function(){this.setSelectionRange(this.getWordRange())},this.selectAWord=function(){var e=this.getCursor(),t=this.session.getAWordRange(e.row,e.column);this.setSelectionRange(t)},this.getLineRange=function(e,t){var n=typeof e=="number"?e:this.lead.row,r,i=this.session.getFoldLine(n);return i?(n=i.start.row,r=i.end.row):r=n,t===!0?new o(n,0,r,this.session.getLine(r).length):new o(n,0,r+1,0)},this.selectLine=function(){this.setSelectionRange(this.getLineRange())},this.moveCursorUp=function(){this.moveCursorBy(-1,0)},this.moveCursorDown=function(){this.moveCursorBy(1,0)},this.moveCursorLeft=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,-1))this.moveCursorTo(t.start.row,t.start.column);else if(e.column===0)e.row>0&&this.moveCursorTo(e.row-1,this.doc.getLine(e.row-1).length);else{var n=this.session.getTabSize();this.session.isTabStop(e)&&this.doc.getLine(e.row).slice(e.column-n,e.column).split(" ").length-1==n?this.moveCursorBy(0,-n):this.moveCursorBy(0,-1)}},this.moveCursorRight=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,1))this.moveCursorTo(t.end.row,t.end.column);else if(this.lead.column==this.doc.getLine(this.lead.row).length)this.lead.row<this.doc.getLength()-1&&this.moveCursorTo(this.lead.row+1,0);else{var n=this.session.getTabSize(),e=this.lead;this.session.isTabStop(e)&&this.doc.getLine(e.row).slice(e.column,e.column+n).split(" ").length-1==n?this.moveCursorBy(0,n):this.moveCursorBy(0,1)}},this.moveCursorLineStart=function(){var e=this.lead.row,t=this.lead.column,n=this.session.documentToScreenRow(e,t),r=this.session.screenToDocumentPosition(n,0),i=this.session.getDisplayLine(e,null,r.row,r.column),s=i.match(/^\s*/);s[0].length!=t&&!this.session.$useEmacsStyleLineStart&&(r.column+=s[0].length),this.moveCursorToPosition(r)},this.moveCursorLineEnd=function(){var e=this.lead,t=this.session.getDocumentLastRowColumnPosition(e.row,e.column);if(this.lead.column==t.column){var n=this.session.getLine(t.row);if(t.column==n.length){var r=n.search(/\s+$/);r>0&&(t.column=r)}}this.moveCursorTo(t.row,t.column)},this.moveCursorFileEnd=function(){var e=this.doc.getLength()-1,t=this.doc.getLine(e).length;this.moveCursorTo(e,t)},this.moveCursorFileStart=function(){this.moveCursorTo(0,0)},this.moveCursorLongWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t),i;this.session.nonTokenRe.lastIndex=0,this.session.tokenRe.lastIndex=0;var s=this.session.getFoldAt(e,t,1);if(s){this.moveCursorTo(s.end.row,s.end.column);return}if(i=this.session.nonTokenRe.exec(r))t+=this.session.nonTokenRe.lastIndex,this.session.nonTokenRe.lastIndex=0,r=n.substring(t);if(t>=n.length){this.moveCursorTo(e,n.length),this.moveCursorRight(),e<this.doc.getLength()-1&&this.moveCursorWordRight();return}if(i=this.session.tokenRe.exec(r))t+=this.session.tokenRe.lastIndex,this.session.tokenRe.lastIndex=0;this.moveCursorTo(e,t)},this.moveCursorLongWordLeft=function(){var e=this.lead.row,t=this.lead.column,n;if(n=this.session.getFoldAt(e,t,-1)){this.moveCursorTo(n.start.row,n.start.column);return}var r=this.session.getFoldStringAt(e,t,-1);r==null&&(r=this.doc.getLine(e).substring(0,t));var s=i.stringReverse(r),o;this.session.nonTokenRe.lastIndex=0,this.session.tokenRe.lastIndex=0;if(o=this.session.nonTokenRe.exec(s))t-=this.session.nonTokenRe.lastIndex,s=s.slice(this.session.nonTokenRe.lastIndex),this.session.nonTokenRe.lastIndex=0;if(t<=0){this.moveCursorTo(e,0),this.moveCursorLeft(),e>0&&this.moveCursorWordLeft();return}if(o=this.session.tokenRe.exec(s))t-=this.session.tokenRe.lastIndex,this.session.tokenRe.lastIndex=0;this.moveCursorTo(e,t)},this.$shortWordEndIndex=function(e){var t,n=0,r,i=/\s/,s=this.session.tokenRe;s.lastIndex=0;if(t=this.session.tokenRe.exec(e))n=this.session.tokenRe.lastIndex;else{while((r=e[n])&&i.test(r))n++;if(n<1){s.lastIndex=0;while((r=e[n])&&!s.test(r)){s.lastIndex=0,n++;if(i.test(r)){if(n>2){n--;break}while((r=e[n])&&i.test(r))n++;if(n>2)break}}}}return s.lastIndex=0,n},this.moveCursorShortWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t),i=this.session.getFoldAt(e,t,1);if(i)return this.moveCursorTo(i.end.row,i.end.column);if(t==n.length){var s=this.doc.getLength();do e++,r=this.doc.getLine(e);while(e<s&&/^\s*$/.test(r));/^\s+/.test(r)||(r=""),t=0}var o=this.$shortWordEndIndex(r);this.moveCursorTo(e,t+o)},this.moveCursorShortWordLeft=function(){var e=this.lead.row,t=this.lead.column,n;if(n=this.session.getFoldAt(e,t,-1))return this.moveCursorTo(n.start.row,n.start.column);var r=this.session.getLine(e).substring(0,t);if(t===0){do e--,r=this.doc.getLine(e);while(e>0&&/^\s*$/.test(r));t=r.length,/\s+$/.test(r)||(r="")}var s=i.stringReverse(r),o=this.$shortWordEndIndex(s);return this.moveCursorTo(e,t-o)},this.moveCursorWordRight=function(){this.session.$selectLongWords?this.moveCursorLongWordRight():this.moveCursorShortWordRight()},this.moveCursorWordLeft=function(){this.session.$selectLongWords?this.moveCursorLongWordLeft():this.moveCursorShortWordLeft()},this.moveCursorBy=function(e,t){var n=this.session.documentToScreenPosition(this.lead.row,this.lead.column);t===0&&(this.$desiredColumn?n.column=this.$desiredColumn:this.$desiredColumn=n.column);var r=this.session.screenToDocumentPosition(n.row+e,n.column);e!==0&&t===0&&r.row===this.lead.row&&r.column===this.lead.column&&this.session.lineWidgets&&this.session.lineWidgets[r.row]&&(r.row>0||e>0)&&r.row++,this.moveCursorTo(r.row,r.column+t,t===0)},this.moveCursorToPosition=function(e){this.moveCursorTo(e.row,e.column)},this.moveCursorTo=function(e,t,n){var r=this.session.getFoldAt(e,t,1);r&&(e=r.start.row,t=r.start.column),this.$keepDesiredColumnOnChange=!0,this.lead.setPosition(e,t),this.$keepDesiredColumnOnChange=!1,n||(this.$desiredColumn=null)},this.moveCursorToScreen=function(e,t,n){var r=this.session.screenToDocumentPosition(e,t);this.moveCursorTo(r.row,r.column,n)},this.detach=function(){this.lead.detach(),this.anchor.detach(),this.session=this.doc=null},this.fromOrientedRange=function(e){this.setSelectionRange(e,e.cursor==e.start),this.$desiredColumn=e.desiredColumn||this.$desiredColumn},this.toOrientedRange=function(e){var t=this.getRange();return e?(e.start.column=t.start.column,e.start.row=t.start.row,e.end.column=t.end.column,e.end.row=t.end.row):e=t,e.cursor=this.isBackwards()?e.start:e.end,e.desiredColumn=this.$desiredColumn,e},this.getRangeOfMovements=function(e){var t=this.getCursor();try{e(this);var n=this.getCursor();return o.fromPoints(t,n)}catch(r){return o.fromPoints(t,t)}finally{this.moveCursorToPosition(t)}},this.toJSON=function(){if(this.rangeCount)var e=this.ranges.map(function(e){var t=e.clone();return t.isBackwards=e.cursor==e.start,t});else{var e=this.getRange();e.isBackwards=this.isBackwards()}return e},this.fromJSON=function(e){if(e.start==undefined){if(this.rangeList){this.toSingleRange(e[0]);for(var t=e.length;t--;){var n=o.fromPoints(e[t].start,e[t].end);e[t].isBackwards&&(n.cursor=n.start),this.addRange(n,!0)}return}e=e[0]}this.rangeList&&this.toSingleRange(e),this.setSelectionRange(e,e.isBackwards)},this.isEqual=function(e){if((e.length||this.rangeCount)&&e.length!=this.rangeCount)return!1;if(!e.length||!this.ranges)return this.getRange().isEqual(e);for(var t=this.ranges.length;t--;)if(!this.ranges[t].isEqual(e[t]))return!1;return!0}}).call(u.prototype),t.Selection=u}),define("ace/tokenizer",["require","exports","module","ace/config"],function(e,t,n){"use strict";var r=e("./config"),i=2e3,s=function(e){this.states=e,this.regExps={},this.matchMappings={};for(var t in this.states){var n=this.states[t],r=[],i=0,s=this.matchMappings[t]={defaultToken:"text"},o="g",u=[];for(var a=0;a<n.length;a++){var f=n[a];f.defaultToken&&(s.defaultToken=f.defaultToken),f.caseInsensitive&&(o="gi");if(f.regex==null)continue;f.regex instanceof RegExp&&(f.regex=f.regex.toString().slice(1,-1));var l=f.regex,c=(new RegExp("(?:("+l+")|(.))")).exec("a").length-2;Array.isArray(f.token)?f.token.length==1||c==1?f.token=f.token[0]:c-1!=f.token.length?(this.reportError("number of classes and regexp groups doesn't match",{rule:f,groupCount:c-1}),f.token=f.token[0]):(f.tokenArray=f.token,f.token=null,f.onMatch=this.$arrayTokens):typeof f.token=="function"&&!f.onMatch&&(c>1?f.onMatch=this.$applyToken:f.onMatch=f.token),c>1&&(/\\\d/.test(f.regex)?l=f.regex.replace(/\\([0-9]+)/g,function(e,t){return"\\"+(parseInt(t,10)+i+1)}):(c=1,l=this.removeCapturingGroups(f.regex)),!f.splitRegex&&typeof f.token!="string"&&u.push(f)),s[i]=a,i+=c,r.push(l),f.onMatch||(f.onMatch=null)}r.length||(s[0]=0,r.push("$")),u.forEach(function(e){e.splitRegex=this.createSplitterRegexp(e.regex,o)},this),this.regExps[t]=new RegExp("("+r.join(")|(")+")|($)",o)}};(function(){this.$setMaxTokenCount=function(e){i=e|0},this.$applyToken=function(e){var t=this.splitRegex.exec(e).slice(1),n=this.token.apply(this,t);if(typeof n=="string")return[{type:n,value:e}];var r=[];for(var i=0,s=n.length;i<s;i++)t[i]&&(r[r.length]={type:n[i],value:t[i]});return r},this.$arrayTokens=function(e){if(!e)return[];var t=this.splitRegex.exec(e);if(!t)return"text";var n=[],r=this.tokenArray;for(var i=0,s=r.length;i<s;i++)t[i+1]&&(n[n.length]={type:r[i],value:t[i+1]});return n},this.removeCapturingGroups=function(e){var t=e.replace(/\[(?:\\.|[^\]])*?\]|\\.|\(\?[:=!]|(\()/g,function(e,t){return t?"(?:":e});return t},this.createSplitterRegexp=function(e,t){if(e.indexOf("(?=")!=-1){var n=0,r=!1,i={};e.replace(/(\\.)|(\((?:\?[=!])?)|(\))|([\[\]])/g,function(e,t,s,o,u,a){return r?r=u!="]":u?r=!0:o?(n==i.stack&&(i.end=a+1,i.stack=-1),n--):s&&(n++,s.length!=1&&(i.stack=n,i.start=a)),e}),i.end!=null&&/^\)*$/.test(e.substr(i.end))&&(e=e.substring(0,i.start)+e.substr(i.end))}return e.charAt(0)!="^"&&(e="^"+e),e.charAt(e.length-1)!="$"&&(e+="$"),new RegExp(e,(t||"").replace("g",""))},this.getLineTokens=function(e,t){if(t&&typeof t!="string"){var n=t.slice(0);t=n[0],t==="#tmp"&&(n.shift(),t=n.shift())}else var n=[];var r=t||"start",s=this.states[r];s||(r="start",s=this.states[r]);var o=this.matchMappings[r],u=this.regExps[r];u.lastIndex=0;var a,f=[],l=0,c=0,h={type:null,value:""};while(a=u.exec(e)){var p=o.defaultToken,d=null,v=a[0],m=u.lastIndex;if(m-v.length>l){var g=e.substring(l,m-v.length);h.type==p?h.value+=g:(h.type&&f.push(h),h={type:p,value:g})}for(var y=0;y<a.length-2;y++){if(a[y+1]===undefined)continue;d=s[o[y]],d.onMatch?p=d.onMatch(v,r,n):p=d.token,d.next&&(typeof d.next=="string"?r=d.next:r=d.next(r,n),s=this.states[r],s||(this.reportError("state doesn't exist",r),r="start",s=this.states[r]),o=this.matchMappings[r],l=m,u=this.regExps[r],u.lastIndex=m);break}if(v)if(typeof p=="string")!!d&&d.merge===!1||h.type!==p?(h.type&&f.push(h),h={type:p,value:v}):h.value+=v;else if(p){h.type&&f.push(h),h={type:null,value:""};for(var y=0;y<p.length;y++)f.push(p[y])}if(l==e.length)break;l=m;if(c++>i){c>2*e.length&&this.reportError("infinite loop with in ace tokenizer",{startState:t,line:e});while(l<e.length)h.type&&f.push(h),h={value:e.substring(l,l+=2e3),type:"overflow"};r="start",n=[];break}}return h.type&&f.push(h),n.length>1&&n[0]!==r&&n.unshift("#tmp",r),{tokens:f,state:n.length?n:r}},this.reportError=r.reportError}).call(s.prototype),t.Tokenizer=s}),define("ace/mode/text_highlight_rules",["require","exports","module","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../lib/lang"),i=function(){this.$rules={start:[{token:"empty_line",regex:"^$"},{defaultToken:"text"}]}};(function(){this.addRules=function(e,t){if(!t){for(var n in e)this.$rules[n]=e[n];return}for(var n in e){var r=e[n];for(var i=0;i<r.length;i++){var s=r[i];if(s.next||s.onMatch)typeof s.next=="string"&&s.next.indexOf(t)!==0&&(s.next=t+s.next),s.nextState&&s.nextState.indexOf(t)!==0&&(s.nextState=t+s.nextState)}this.$rules[t+n]=r}},this.getRules=function(){return this.$rules},this.embedRules=function(e,t,n,i,s){var o=typeof e=="function"?(new e).getRules():e;if(i)for(var u=0;u<i.length;u++)i[u]=t+i[u];else{i=[];for(var a in o)i.push(t+a)}this.addRules(o,t);if(n){var f=Array.prototype[s?"push":"unshift"];for(var u=0;u<i.length;u++)f.apply(this.$rules[i[u]],r.deepCopy(n))}this.$embeds||(this.$embeds=[]),this.$embeds.push(t)},this.getEmbeds=function(){return this.$embeds};var e=function(e,t){return(e!="start"||t.length)&&t.unshift(this.nextState,e),this.nextState},t=function(e,t){return t.shift(),t.shift()||"start"};this.normalizeRules=function(){function i(s){var o=r[s];o.processed=!0;for(var u=0;u<o.length;u++){var a=o[u],f=null;Array.isArray(a)&&(f=a,a={}),!a.regex&&a.start&&(a.regex=a.start,a.next||(a.next=[]),a.next.push({defaultToken:a.token},{token:a.token+".end",regex:a.end||a.start,next:"pop"}),a.token=a.token+".start",a.push=!0);var l=a.next||a.push;if(l&&Array.isArray(l)){var c=a.stateName;c||(c=a.token,typeof c!="string"&&(c=c[0]||""),r[c]&&(c+=n++)),r[c]=l,a.next=c,i(c)}else l=="pop"&&(a.next=t);a.push&&(a.nextState=a.next||a.push,a.next=e,delete a.push);if(a.rules)for(var h in a.rules)r[h]?r[h].push&&r[h].push.apply(r[h],a.rules[h]):r[h]=a.rules[h];var p=typeof a=="string"?a:typeof a.include=="string"?a.include:"";p&&(f=r[p]);if(f){var d=[u,1].concat(f);a.noEscape&&(d=d.filter(function(e){return!e.next})),o.splice.apply(o,d),u--}a.keywordMap&&(a.token=this.createKeywordMapper(a.keywordMap,a.defaultToken||"text",a.caseInsensitive),delete a.defaultToken)}}var n=0,r=this.$rules;Object.keys(r).forEach(i,this)},this.createKeywordMapper=function(e,t,n,r){var i=Object.create(null);return Object.keys(e).forEach(function(t){var s=e[t];n&&(s=s.toLowerCase());var o=s.split(r||"|");for(var u=o.length;u--;)i[o[u]]=t}),Object.getPrototypeOf(i)&&(i.__proto__=null),this.$keywordList=Object.keys(i),e=null,n?function(e){return i[e.toLowerCase()]||t}:function(e){return i[e]||t}},this.getKeywords=function(){return this.$keywords}}).call(i.prototype),t.TextHighlightRules=i}),define("ace/mode/behaviour",["require","exports","module"],function(e,t,n){"use strict";var r=function(){this.$behaviours={}};(function(){this.add=function(e,t,n){switch(undefined){case this.$behaviours:this.$behaviours={};case this.$behaviours[e]:this.$behaviours[e]={}}this.$behaviours[e][t]=n},this.addBehaviours=function(e){for(var t in e)for(var n in e[t])this.add(t,n,e[t][n])},this.remove=function(e){this.$behaviours&&this.$behaviours[e]&&delete this.$behaviours[e]},this.inherit=function(e,t){if(typeof e=="function")var n=(new e).getBehaviours(t);else var n=e.getBehaviours(t);this.addBehaviours(n)},this.getBehaviours=function(e){if(!e)return this.$behaviours;var t={};for(var n=0;n<e.length;n++)this.$behaviours[e[n]]&&(t[e[n]]=this.$behaviours[e[n]]);return t}}).call(r.prototype),t.Behaviour=r}),define("ace/unicode",["require","exports","module"],function(e,t,n){"use strict";function r(e){var n=/\w{4}/g;for(var r in e)t.packages[r]=e[r].replace(n,"\\u$&")}t.packages={},r({L:"0041-005A0061-007A00AA00B500BA00C0-00D600D8-00F600F8-02C102C6-02D102E0-02E402EC02EE0370-037403760377037A-037D03860388-038A038C038E-03A103A3-03F503F7-0481048A-05250531-055605590561-058705D0-05EA05F0-05F20621-064A066E066F0671-06D306D506E506E606EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA07F407F507FA0800-0815081A082408280904-0939093D09500958-0961097109720979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10D05-0D0C0D0E-0D100D12-0D280D2A-0D390D3D0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E460E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EC60EDC0EDD0F000F40-0F470F49-0F6C0F88-0F8B1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10A0-10C510D0-10FA10FC1100-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317D717DC1820-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541AA71B05-1B331B45-1B4B1B83-1BA01BAE1BAF1C00-1C231C4D-1C4F1C5A-1C7D1CE9-1CEC1CEE-1CF11D00-1DBF1E00-1F151F18-1F1D1F20-1F451F48-1F4D1F50-1F571F591F5B1F5D1F5F-1F7D1F80-1FB41FB6-1FBC1FBE1FC2-1FC41FC6-1FCC1FD0-1FD31FD6-1FDB1FE0-1FEC1FF2-1FF41FF6-1FFC2071207F2090-209421022107210A-211321152119-211D212421262128212A-212D212F-2139213C-213F2145-2149214E218321842C00-2C2E2C30-2C5E2C60-2CE42CEB-2CEE2D00-2D252D30-2D652D6F2D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE2E2F300530063031-3035303B303C3041-3096309D-309F30A1-30FA30FC-30FF3105-312D3131-318E31A0-31B731F0-31FF3400-4DB54E00-9FCBA000-A48CA4D0-A4FDA500-A60CA610-A61FA62AA62BA640-A65FA662-A66EA67F-A697A6A0-A6E5A717-A71FA722-A788A78BA78CA7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2A9CFAA00-AA28AA40-AA42AA44-AA4BAA60-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADB-AADDABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA2DFA30-FA6DFA70-FAD9FB00-FB06FB13-FB17FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF21-FF3AFF41-FF5AFF66-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC",Ll:"0061-007A00AA00B500BA00DF-00F600F8-00FF01010103010501070109010B010D010F01110113011501170119011B011D011F01210123012501270129012B012D012F01310133013501370138013A013C013E014001420144014601480149014B014D014F01510153015501570159015B015D015F01610163016501670169016B016D016F0171017301750177017A017C017E-0180018301850188018C018D019201950199-019B019E01A101A301A501A801AA01AB01AD01B001B401B601B901BA01BD-01BF01C601C901CC01CE01D001D201D401D601D801DA01DC01DD01DF01E101E301E501E701E901EB01ED01EF01F001F301F501F901FB01FD01FF02010203020502070209020B020D020F02110213021502170219021B021D021F02210223022502270229022B022D022F02310233-0239023C023F0240024202470249024B024D024F-02930295-02AF037103730377037B-037D039003AC-03CE03D003D103D5-03D703D903DB03DD03DF03E103E303E503E703E903EB03ED03EF-03F303F503F803FB03FC0430-045F04610463046504670469046B046D046F04710473047504770479047B047D047F0481048B048D048F04910493049504970499049B049D049F04A104A304A504A704A904AB04AD04AF04B104B304B504B704B904BB04BD04BF04C204C404C604C804CA04CC04CE04CF04D104D304D504D704D904DB04DD04DF04E104E304E504E704E904EB04ED04EF04F104F304F504F704F904FB04FD04FF05010503050505070509050B050D050F05110513051505170519051B051D051F0521052305250561-05871D00-1D2B1D62-1D771D79-1D9A1E011E031E051E071E091E0B1E0D1E0F1E111E131E151E171E191E1B1E1D1E1F1E211E231E251E271E291E2B1E2D1E2F1E311E331E351E371E391E3B1E3D1E3F1E411E431E451E471E491E4B1E4D1E4F1E511E531E551E571E591E5B1E5D1E5F1E611E631E651E671E691E6B1E6D1E6F1E711E731E751E771E791E7B1E7D1E7F1E811E831E851E871E891E8B1E8D1E8F1E911E931E95-1E9D1E9F1EA11EA31EA51EA71EA91EAB1EAD1EAF1EB11EB31EB51EB71EB91EBB1EBD1EBF1EC11EC31EC51EC71EC91ECB1ECD1ECF1ED11ED31ED51ED71ED91EDB1EDD1EDF1EE11EE31EE51EE71EE91EEB1EED1EEF1EF11EF31EF51EF71EF91EFB1EFD1EFF-1F071F10-1F151F20-1F271F30-1F371F40-1F451F50-1F571F60-1F671F70-1F7D1F80-1F871F90-1F971FA0-1FA71FB0-1FB41FB61FB71FBE1FC2-1FC41FC61FC71FD0-1FD31FD61FD71FE0-1FE71FF2-1FF41FF61FF7210A210E210F2113212F21342139213C213D2146-2149214E21842C30-2C5E2C612C652C662C682C6A2C6C2C712C732C742C76-2C7C2C812C832C852C872C892C8B2C8D2C8F2C912C932C952C972C992C9B2C9D2C9F2CA12CA32CA52CA72CA92CAB2CAD2CAF2CB12CB32CB52CB72CB92CBB2CBD2CBF2CC12CC32CC52CC72CC92CCB2CCD2CCF2CD12CD32CD52CD72CD92CDB2CDD2CDF2CE12CE32CE42CEC2CEE2D00-2D25A641A643A645A647A649A64BA64DA64FA651A653A655A657A659A65BA65DA65FA663A665A667A669A66BA66DA681A683A685A687A689A68BA68DA68FA691A693A695A697A723A725A727A729A72BA72DA72F-A731A733A735A737A739A73BA73DA73FA741A743A745A747A749A74BA74DA74FA751A753A755A757A759A75BA75DA75FA761A763A765A767A769A76BA76DA76FA771-A778A77AA77CA77FA781A783A785A787A78CFB00-FB06FB13-FB17FF41-FF5A",Lu:"0041-005A00C0-00D600D8-00DE01000102010401060108010A010C010E01100112011401160118011A011C011E01200122012401260128012A012C012E01300132013401360139013B013D013F0141014301450147014A014C014E01500152015401560158015A015C015E01600162016401660168016A016C016E017001720174017601780179017B017D018101820184018601870189-018B018E-0191019301940196-0198019C019D019F01A001A201A401A601A701A901AC01AE01AF01B1-01B301B501B701B801BC01C401C701CA01CD01CF01D101D301D501D701D901DB01DE01E001E201E401E601E801EA01EC01EE01F101F401F6-01F801FA01FC01FE02000202020402060208020A020C020E02100212021402160218021A021C021E02200222022402260228022A022C022E02300232023A023B023D023E02410243-02460248024A024C024E03700372037603860388-038A038C038E038F0391-03A103A3-03AB03CF03D2-03D403D803DA03DC03DE03E003E203E403E603E803EA03EC03EE03F403F703F903FA03FD-042F04600462046404660468046A046C046E04700472047404760478047A047C047E0480048A048C048E04900492049404960498049A049C049E04A004A204A404A604A804AA04AC04AE04B004B204B404B604B804BA04BC04BE04C004C104C304C504C704C904CB04CD04D004D204D404D604D804DA04DC04DE04E004E204E404E604E804EA04EC04EE04F004F204F404F604F804FA04FC04FE05000502050405060508050A050C050E05100512051405160518051A051C051E0520052205240531-055610A0-10C51E001E021E041E061E081E0A1E0C1E0E1E101E121E141E161E181E1A1E1C1E1E1E201E221E241E261E281E2A1E2C1E2E1E301E321E341E361E381E3A1E3C1E3E1E401E421E441E461E481E4A1E4C1E4E1E501E521E541E561E581E5A1E5C1E5E1E601E621E641E661E681E6A1E6C1E6E1E701E721E741E761E781E7A1E7C1E7E1E801E821E841E861E881E8A1E8C1E8E1E901E921E941E9E1EA01EA21EA41EA61EA81EAA1EAC1EAE1EB01EB21EB41EB61EB81EBA1EBC1EBE1EC01EC21EC41EC61EC81ECA1ECC1ECE1ED01ED21ED41ED61ED81EDA1EDC1EDE1EE01EE21EE41EE61EE81EEA1EEC1EEE1EF01EF21EF41EF61EF81EFA1EFC1EFE1F08-1F0F1F18-1F1D1F28-1F2F1F38-1F3F1F48-1F4D1F591F5B1F5D1F5F1F68-1F6F1FB8-1FBB1FC8-1FCB1FD8-1FDB1FE8-1FEC1FF8-1FFB21022107210B-210D2110-211221152119-211D212421262128212A-212D2130-2133213E213F214521832C00-2C2E2C602C62-2C642C672C692C6B2C6D-2C702C722C752C7E-2C802C822C842C862C882C8A2C8C2C8E2C902C922C942C962C982C9A2C9C2C9E2CA02CA22CA42CA62CA82CAA2CAC2CAE2CB02CB22CB42CB62CB82CBA2CBC2CBE2CC02CC22CC42CC62CC82CCA2CCC2CCE2CD02CD22CD42CD62CD82CDA2CDC2CDE2CE02CE22CEB2CEDA640A642A644A646A648A64AA64CA64EA650A652A654A656A658A65AA65CA65EA662A664A666A668A66AA66CA680A682A684A686A688A68AA68CA68EA690A692A694A696A722A724A726A728A72AA72CA72EA732A734A736A738A73AA73CA73EA740A742A744A746A748A74AA74CA74EA750A752A754A756A758A75AA75CA75EA760A762A764A766A768A76AA76CA76EA779A77BA77DA77EA780A782A784A786A78BFF21-FF3A",Lt:"01C501C801CB01F21F88-1F8F1F98-1F9F1FA8-1FAF1FBC1FCC1FFC",Lm:"02B0-02C102C6-02D102E0-02E402EC02EE0374037A0559064006E506E607F407F507FA081A0824082809710E460EC610FC17D718431AA71C78-1C7D1D2C-1D611D781D9B-1DBF2071207F2090-20942C7D2D6F2E2F30053031-3035303B309D309E30FC-30FEA015A4F8-A4FDA60CA67FA717-A71FA770A788A9CFAA70AADDFF70FF9EFF9F",Lo:"01BB01C0-01C3029405D0-05EA05F0-05F20621-063F0641-064A066E066F0671-06D306D506EE06EF06FA-06FC06FF07100712-072F074D-07A507B107CA-07EA0800-08150904-0939093D09500958-096109720979-097F0985-098C098F09900993-09A809AA-09B009B209B6-09B909BD09CE09DC09DD09DF-09E109F009F10A05-0A0A0A0F0A100A13-0A280A2A-0A300A320A330A350A360A380A390A59-0A5C0A5E0A72-0A740A85-0A8D0A8F-0A910A93-0AA80AAA-0AB00AB20AB30AB5-0AB90ABD0AD00AE00AE10B05-0B0C0B0F0B100B13-0B280B2A-0B300B320B330B35-0B390B3D0B5C0B5D0B5F-0B610B710B830B85-0B8A0B8E-0B900B92-0B950B990B9A0B9C0B9E0B9F0BA30BA40BA8-0BAA0BAE-0BB90BD00C05-0C0C0C0E-0C100C12-0C280C2A-0C330C35-0C390C3D0C580C590C600C610C85-0C8C0C8E-0C900C92-0CA80CAA-0CB30CB5-0CB90CBD0CDE0CE00CE10D05-0D0C0D0E-0D100D12-0D280D2A-0D390D3D0D600D610D7A-0D7F0D85-0D960D9A-0DB10DB3-0DBB0DBD0DC0-0DC60E01-0E300E320E330E40-0E450E810E820E840E870E880E8A0E8D0E94-0E970E99-0E9F0EA1-0EA30EA50EA70EAA0EAB0EAD-0EB00EB20EB30EBD0EC0-0EC40EDC0EDD0F000F40-0F470F49-0F6C0F88-0F8B1000-102A103F1050-1055105A-105D106110651066106E-10701075-1081108E10D0-10FA1100-1248124A-124D1250-12561258125A-125D1260-1288128A-128D1290-12B012B2-12B512B8-12BE12C012C2-12C512C8-12D612D8-13101312-13151318-135A1380-138F13A0-13F41401-166C166F-167F1681-169A16A0-16EA1700-170C170E-17111720-17311740-17511760-176C176E-17701780-17B317DC1820-18421844-18771880-18A818AA18B0-18F51900-191C1950-196D1970-19741980-19AB19C1-19C71A00-1A161A20-1A541B05-1B331B45-1B4B1B83-1BA01BAE1BAF1C00-1C231C4D-1C4F1C5A-1C771CE9-1CEC1CEE-1CF12135-21382D30-2D652D80-2D962DA0-2DA62DA8-2DAE2DB0-2DB62DB8-2DBE2DC0-2DC62DC8-2DCE2DD0-2DD62DD8-2DDE3006303C3041-3096309F30A1-30FA30FF3105-312D3131-318E31A0-31B731F0-31FF3400-4DB54E00-9FCBA000-A014A016-A48CA4D0-A4F7A500-A60BA610-A61FA62AA62BA66EA6A0-A6E5A7FB-A801A803-A805A807-A80AA80C-A822A840-A873A882-A8B3A8F2-A8F7A8FBA90A-A925A930-A946A960-A97CA984-A9B2AA00-AA28AA40-AA42AA44-AA4BAA60-AA6FAA71-AA76AA7AAA80-AAAFAAB1AAB5AAB6AAB9-AABDAAC0AAC2AADBAADCABC0-ABE2AC00-D7A3D7B0-D7C6D7CB-D7FBF900-FA2DFA30-FA6DFA70-FAD9FB1DFB1F-FB28FB2A-FB36FB38-FB3CFB3EFB40FB41FB43FB44FB46-FBB1FBD3-FD3DFD50-FD8FFD92-FDC7FDF0-FDFBFE70-FE74FE76-FEFCFF66-FF6FFF71-FF9DFFA0-FFBEFFC2-FFC7FFCA-FFCFFFD2-FFD7FFDA-FFDC",M:"0300-036F0483-04890591-05BD05BF05C105C205C405C505C70610-061A064B-065E067006D6-06DC06DE-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0900-0903093C093E-094E0951-0955096209630981-098309BC09BE-09C409C709C809CB-09CD09D709E209E30A01-0A030A3C0A3E-0A420A470A480A4B-0A4D0A510A700A710A750A81-0A830ABC0ABE-0AC50AC7-0AC90ACB-0ACD0AE20AE30B01-0B030B3C0B3E-0B440B470B480B4B-0B4D0B560B570B620B630B820BBE-0BC20BC6-0BC80BCA-0BCD0BD70C01-0C030C3E-0C440C46-0C480C4A-0C4D0C550C560C620C630C820C830CBC0CBE-0CC40CC6-0CC80CCA-0CCD0CD50CD60CE20CE30D020D030D3E-0D440D46-0D480D4A-0D4D0D570D620D630D820D830DCA0DCF-0DD40DD60DD8-0DDF0DF20DF30E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F3E0F3F0F71-0F840F860F870F90-0F970F99-0FBC0FC6102B-103E1056-1059105E-10601062-10641067-106D1071-10741082-108D108F109A-109D135F1712-17141732-1734175217531772177317B6-17D317DD180B-180D18A91920-192B1930-193B19B0-19C019C819C91A17-1A1B1A55-1A5E1A60-1A7C1A7F1B00-1B041B34-1B441B6B-1B731B80-1B821BA1-1BAA1C24-1C371CD0-1CD21CD4-1CE81CED1CF21DC0-1DE61DFD-1DFF20D0-20F02CEF-2CF12DE0-2DFF302A-302F3099309AA66F-A672A67CA67DA6F0A6F1A802A806A80BA823-A827A880A881A8B4-A8C4A8E0-A8F1A926-A92DA947-A953A980-A983A9B3-A9C0AA29-AA36AA43AA4CAA4DAA7BAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1ABE3-ABEAABECABEDFB1EFE00-FE0FFE20-FE26",Mn:"0300-036F0483-04870591-05BD05BF05C105C205C405C505C70610-061A064B-065E067006D6-06DC06DF-06E406E706E806EA-06ED07110730-074A07A6-07B007EB-07F30816-0819081B-08230825-08270829-082D0900-0902093C0941-0948094D0951-095509620963098109BC09C1-09C409CD09E209E30A010A020A3C0A410A420A470A480A4B-0A4D0A510A700A710A750A810A820ABC0AC1-0AC50AC70AC80ACD0AE20AE30B010B3C0B3F0B41-0B440B4D0B560B620B630B820BC00BCD0C3E-0C400C46-0C480C4A-0C4D0C550C560C620C630CBC0CBF0CC60CCC0CCD0CE20CE30D41-0D440D4D0D620D630DCA0DD2-0DD40DD60E310E34-0E3A0E47-0E4E0EB10EB4-0EB90EBB0EBC0EC8-0ECD0F180F190F350F370F390F71-0F7E0F80-0F840F860F870F90-0F970F99-0FBC0FC6102D-10301032-10371039103A103D103E10581059105E-10601071-1074108210851086108D109D135F1712-17141732-1734175217531772177317B7-17BD17C617C9-17D317DD180B-180D18A91920-19221927192819321939-193B1A171A181A561A58-1A5E1A601A621A65-1A6C1A73-1A7C1A7F1B00-1B031B341B36-1B3A1B3C1B421B6B-1B731B801B811BA2-1BA51BA81BA91C2C-1C331C361C371CD0-1CD21CD4-1CE01CE2-1CE81CED1DC0-1DE61DFD-1DFF20D0-20DC20E120E5-20F02CEF-2CF12DE0-2DFF302A-302F3099309AA66FA67CA67DA6F0A6F1A802A806A80BA825A826A8C4A8E0-A8F1A926-A92DA947-A951A980-A982A9B3A9B6-A9B9A9BCAA29-AA2EAA31AA32AA35AA36AA43AA4CAAB0AAB2-AAB4AAB7AAB8AABEAABFAAC1ABE5ABE8ABEDFB1EFE00-FE0FFE20-FE26",Mc:"0903093E-09400949-094C094E0982098309BE-09C009C709C809CB09CC09D70A030A3E-0A400A830ABE-0AC00AC90ACB0ACC0B020B030B3E0B400B470B480B4B0B4C0B570BBE0BBF0BC10BC20BC6-0BC80BCA-0BCC0BD70C01-0C030C41-0C440C820C830CBE0CC0-0CC40CC70CC80CCA0CCB0CD50CD60D020D030D3E-0D400D46-0D480D4A-0D4C0D570D820D830DCF-0DD10DD8-0DDF0DF20DF30F3E0F3F0F7F102B102C10311038103B103C105610571062-10641067-106D108310841087-108C108F109A-109C17B617BE-17C517C717C81923-19261929-192B193019311933-193819B0-19C019C819C91A19-1A1B1A551A571A611A631A641A6D-1A721B041B351B3B1B3D-1B411B431B441B821BA11BA61BA71BAA1C24-1C2B1C341C351CE11CF2A823A824A827A880A881A8B4-A8C3A952A953A983A9B4A9B5A9BAA9BBA9BD-A9C0AA2FAA30AA33AA34AA4DAA7BABE3ABE4ABE6ABE7ABE9ABEAABEC",Me:"0488048906DE20DD-20E020E2-20E4A670-A672",N:"0030-003900B200B300B900BC-00BE0660-066906F0-06F907C0-07C90966-096F09E6-09EF09F4-09F90A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BF20C66-0C6F0C78-0C7E0CE6-0CEF0D66-0D750E50-0E590ED0-0ED90F20-0F331040-10491090-10991369-137C16EE-16F017E0-17E917F0-17F91810-18191946-194F19D0-19DA1A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C5920702074-20792080-20892150-21822185-21892460-249B24EA-24FF2776-27932CFD30073021-30293038-303A3192-31953220-32293251-325F3280-328932B1-32BFA620-A629A6E6-A6EFA830-A835A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19",Nd:"0030-00390660-066906F0-06F907C0-07C90966-096F09E6-09EF0A66-0A6F0AE6-0AEF0B66-0B6F0BE6-0BEF0C66-0C6F0CE6-0CEF0D66-0D6F0E50-0E590ED0-0ED90F20-0F291040-10491090-109917E0-17E91810-18191946-194F19D0-19DA1A80-1A891A90-1A991B50-1B591BB0-1BB91C40-1C491C50-1C59A620-A629A8D0-A8D9A900-A909A9D0-A9D9AA50-AA59ABF0-ABF9FF10-FF19",Nl:"16EE-16F02160-21822185-218830073021-30293038-303AA6E6-A6EF",No:"00B200B300B900BC-00BE09F4-09F90BF0-0BF20C78-0C7E0D70-0D750F2A-0F331369-137C17F0-17F920702074-20792080-20892150-215F21892460-249B24EA-24FF2776-27932CFD3192-31953220-32293251-325F3280-328932B1-32BFA830-A835",P:"0021-00230025-002A002C-002F003A003B003F0040005B-005D005F007B007D00A100AB00B700BB00BF037E0387055A-055F0589058A05BE05C005C305C605F305F40609060A060C060D061B061E061F066A-066D06D40700-070D07F7-07F90830-083E0964096509700DF40E4F0E5A0E5B0F04-0F120F3A-0F3D0F850FD0-0FD4104A-104F10FB1361-13681400166D166E169B169C16EB-16ED1735173617D4-17D617D8-17DA1800-180A1944194519DE19DF1A1E1A1F1AA0-1AA61AA8-1AAD1B5A-1B601C3B-1C3F1C7E1C7F1CD32010-20272030-20432045-20512053-205E207D207E208D208E2329232A2768-277527C527C627E6-27EF2983-299829D8-29DB29FC29FD2CF9-2CFC2CFE2CFF2E00-2E2E2E302E313001-30033008-30113014-301F3030303D30A030FBA4FEA4FFA60D-A60FA673A67EA6F2-A6F7A874-A877A8CEA8CFA8F8-A8FAA92EA92FA95FA9C1-A9CDA9DEA9DFAA5C-AA5FAADEAADFABEBFD3EFD3FFE10-FE19FE30-FE52FE54-FE61FE63FE68FE6AFE6BFF01-FF03FF05-FF0AFF0C-FF0FFF1AFF1BFF1FFF20FF3B-FF3DFF3FFF5BFF5DFF5F-FF65",Pd:"002D058A05BE140018062010-20152E172E1A301C303030A0FE31FE32FE58FE63FF0D",Ps:"0028005B007B0F3A0F3C169B201A201E2045207D208D23292768276A276C276E27702772277427C527E627E827EA27EC27EE2983298529872989298B298D298F299129932995299729D829DA29FC2E222E242E262E283008300A300C300E3010301430163018301A301DFD3EFE17FE35FE37FE39FE3BFE3DFE3FFE41FE43FE47FE59FE5BFE5DFF08FF3BFF5BFF5FFF62",Pe:"0029005D007D0F3B0F3D169C2046207E208E232A2769276B276D276F27712773277527C627E727E927EB27ED27EF298429862988298A298C298E2990299229942996299829D929DB29FD2E232E252E272E293009300B300D300F3011301530173019301B301E301FFD3FFE18FE36FE38FE3AFE3CFE3EFE40FE42FE44FE48FE5AFE5CFE5EFF09FF3DFF5DFF60FF63",Pi:"00AB2018201B201C201F20392E022E042E092E0C2E1C2E20",Pf:"00BB2019201D203A2E032E052E0A2E0D2E1D2E21",Pc:"005F203F20402054FE33FE34FE4D-FE4FFF3F",Po:"0021-00230025-0027002A002C002E002F003A003B003F0040005C00A100B700BF037E0387055A-055F058905C005C305C605F305F40609060A060C060D061B061E061F066A-066D06D40700-070D07F7-07F90830-083E0964096509700DF40E4F0E5A0E5B0F04-0F120F850FD0-0FD4104A-104F10FB1361-1368166D166E16EB-16ED1735173617D4-17D617D8-17DA1800-18051807-180A1944194519DE19DF1A1E1A1F1AA0-1AA61AA8-1AAD1B5A-1B601C3B-1C3F1C7E1C7F1CD3201620172020-20272030-2038203B-203E2041-20432047-205120532055-205E2CF9-2CFC2CFE2CFF2E002E012E06-2E082E0B2E0E-2E162E182E192E1B2E1E2E1F2E2A-2E2E2E302E313001-3003303D30FBA4FEA4FFA60D-A60FA673A67EA6F2-A6F7A874-A877A8CEA8CFA8F8-A8FAA92EA92FA95FA9C1-A9CDA9DEA9DFAA5C-AA5FAADEAADFABEBFE10-FE16FE19FE30FE45FE46FE49-FE4CFE50-FE52FE54-FE57FE5F-FE61FE68FE6AFE6BFF01-FF03FF05-FF07FF0AFF0CFF0EFF0FFF1AFF1BFF1FFF20FF3CFF61FF64FF65",S:"0024002B003C-003E005E0060007C007E00A2-00A900AC00AE-00B100B400B600B800D700F702C2-02C502D2-02DF02E5-02EB02ED02EF-02FF03750384038503F604820606-0608060B060E060F06E906FD06FE07F609F209F309FA09FB0AF10B700BF3-0BFA0C7F0CF10CF20D790E3F0F01-0F030F13-0F170F1A-0F1F0F340F360F380FBE-0FC50FC7-0FCC0FCE0FCF0FD5-0FD8109E109F13601390-139917DB194019E0-19FF1B61-1B6A1B74-1B7C1FBD1FBF-1FC11FCD-1FCF1FDD-1FDF1FED-1FEF1FFD1FFE20442052207A-207C208A-208C20A0-20B8210021012103-21062108210921142116-2118211E-2123212521272129212E213A213B2140-2144214A-214D214F2190-2328232B-23E82400-24262440-244A249C-24E92500-26CD26CF-26E126E326E8-26FF2701-27042706-2709270C-27272729-274B274D274F-27522756-275E2761-276727942798-27AF27B1-27BE27C0-27C427C7-27CA27CC27D0-27E527F0-29822999-29D729DC-29FB29FE-2B4C2B50-2B592CE5-2CEA2E80-2E992E9B-2EF32F00-2FD52FF0-2FFB300430123013302030363037303E303F309B309C319031913196-319F31C0-31E33200-321E322A-32503260-327F328A-32B032C0-32FE3300-33FF4DC0-4DFFA490-A4C6A700-A716A720A721A789A78AA828-A82BA836-A839AA77-AA79FB29FDFCFDFDFE62FE64-FE66FE69FF04FF0BFF1C-FF1EFF3EFF40FF5CFF5EFFE0-FFE6FFE8-FFEEFFFCFFFD",Sm:"002B003C-003E007C007E00AC00B100D700F703F60606-060820442052207A-207C208A-208C2140-2144214B2190-2194219A219B21A021A321A621AE21CE21CF21D221D421F4-22FF2308-230B23202321237C239B-23B323DC-23E125B725C125F8-25FF266F27C0-27C427C7-27CA27CC27D0-27E527F0-27FF2900-29822999-29D729DC-29FB29FE-2AFF2B30-2B442B47-2B4CFB29FE62FE64-FE66FF0BFF1C-FF1EFF5CFF5EFFE2FFE9-FFEC",Sc:"002400A2-00A5060B09F209F309FB0AF10BF90E3F17DB20A0-20B8A838FDFCFE69FF04FFE0FFE1FFE5FFE6",Sk:"005E006000A800AF00B400B802C2-02C502D2-02DF02E5-02EB02ED02EF-02FF0375038403851FBD1FBF-1FC11FCD-1FCF1FDD-1FDF1FED-1FEF1FFD1FFE309B309CA700-A716A720A721A789A78AFF3EFF40FFE3",So:"00A600A700A900AE00B000B60482060E060F06E906FD06FE07F609FA0B700BF3-0BF80BFA0C7F0CF10CF20D790F01-0F030F13-0F170F1A-0F1F0F340F360F380FBE-0FC50FC7-0FCC0FCE0FCF0FD5-0FD8109E109F13601390-1399194019E0-19FF1B61-1B6A1B74-1B7C210021012103-21062108210921142116-2118211E-2123212521272129212E213A213B214A214C214D214F2195-2199219C-219F21A121A221A421A521A7-21AD21AF-21CD21D021D121D321D5-21F32300-2307230C-231F2322-2328232B-237B237D-239A23B4-23DB23E2-23E82400-24262440-244A249C-24E92500-25B625B8-25C025C2-25F72600-266E2670-26CD26CF-26E126E326E8-26FF2701-27042706-2709270C-27272729-274B274D274F-27522756-275E2761-276727942798-27AF27B1-27BE2800-28FF2B00-2B2F2B452B462B50-2B592CE5-2CEA2E80-2E992E9B-2EF32F00-2FD52FF0-2FFB300430123013302030363037303E303F319031913196-319F31C0-31E33200-321E322A-32503260-327F328A-32B032C0-32FE3300-33FF4DC0-4DFFA490-A4C6A828-A82BA836A837A839AA77-AA79FDFDFFE4FFE8FFEDFFEEFFFCFFFD",Z:"002000A01680180E2000-200A20282029202F205F3000",Zs:"002000A01680180E2000-200A202F205F3000",Zl:"2028",Zp:"2029",C:"0000-001F007F-009F00AD03780379037F-0383038B038D03A20526-05300557055805600588058B-059005C8-05CF05EB-05EF05F5-0605061C061D0620065F06DD070E070F074B074C07B2-07BF07FB-07FF082E082F083F-08FF093A093B094F095609570973-097809800984098D098E0991099209A909B109B3-09B509BA09BB09C509C609C909CA09CF-09D609D8-09DB09DE09E409E509FC-0A000A040A0B-0A0E0A110A120A290A310A340A370A3A0A3B0A3D0A43-0A460A490A4A0A4E-0A500A52-0A580A5D0A5F-0A650A76-0A800A840A8E0A920AA90AB10AB40ABA0ABB0AC60ACA0ACE0ACF0AD1-0ADF0AE40AE50AF00AF2-0B000B040B0D0B0E0B110B120B290B310B340B3A0B3B0B450B460B490B4A0B4E-0B550B58-0B5B0B5E0B640B650B72-0B810B840B8B-0B8D0B910B96-0B980B9B0B9D0BA0-0BA20BA5-0BA70BAB-0BAD0BBA-0BBD0BC3-0BC50BC90BCE0BCF0BD1-0BD60BD8-0BE50BFB-0C000C040C0D0C110C290C340C3A-0C3C0C450C490C4E-0C540C570C5A-0C5F0C640C650C70-0C770C800C810C840C8D0C910CA90CB40CBA0CBB0CC50CC90CCE-0CD40CD7-0CDD0CDF0CE40CE50CF00CF3-0D010D040D0D0D110D290D3A-0D3C0D450D490D4E-0D560D58-0D5F0D640D650D76-0D780D800D810D840D97-0D990DB20DBC0DBE0DBF0DC7-0DC90DCB-0DCE0DD50DD70DE0-0DF10DF5-0E000E3B-0E3E0E5C-0E800E830E850E860E890E8B0E8C0E8E-0E930E980EA00EA40EA60EA80EA90EAC0EBA0EBE0EBF0EC50EC70ECE0ECF0EDA0EDB0EDE-0EFF0F480F6D-0F700F8C-0F8F0F980FBD0FCD0FD9-0FFF10C6-10CF10FD-10FF1249124E124F12571259125E125F1289128E128F12B112B612B712BF12C112C612C712D7131113161317135B-135E137D-137F139A-139F13F5-13FF169D-169F16F1-16FF170D1715-171F1737-173F1754-175F176D17711774-177F17B417B517DE17DF17EA-17EF17FA-17FF180F181A-181F1878-187F18AB-18AF18F6-18FF191D-191F192C-192F193C-193F1941-1943196E196F1975-197F19AC-19AF19CA-19CF19DB-19DD1A1C1A1D1A5F1A7D1A7E1A8A-1A8F1A9A-1A9F1AAE-1AFF1B4C-1B4F1B7D-1B7F1BAB-1BAD1BBA-1BFF1C38-1C3A1C4A-1C4C1C80-1CCF1CF3-1CFF1DE7-1DFC1F161F171F1E1F1F1F461F471F4E1F4F1F581F5A1F5C1F5E1F7E1F7F1FB51FC51FD41FD51FDC1FF01FF11FF51FFF200B-200F202A-202E2060-206F20722073208F2095-209F20B9-20CF20F1-20FF218A-218F23E9-23FF2427-243F244B-245F26CE26E226E4-26E727002705270A270B2728274C274E2753-2755275F27602795-279727B027BF27CB27CD-27CF2B4D-2B4F2B5A-2BFF2C2F2C5F2CF2-2CF82D26-2D2F2D66-2D6E2D70-2D7F2D97-2D9F2DA72DAF2DB72DBF2DC72DCF2DD72DDF2E32-2E7F2E9A2EF4-2EFF2FD6-2FEF2FFC-2FFF3040309730983100-3104312E-3130318F31B8-31BF31E4-31EF321F32FF4DB6-4DBF9FCC-9FFFA48D-A48FA4C7-A4CFA62C-A63FA660A661A674-A67BA698-A69FA6F8-A6FFA78D-A7FAA82C-A82FA83A-A83FA878-A87FA8C5-A8CDA8DA-A8DFA8FC-A8FFA954-A95EA97D-A97FA9CEA9DA-A9DDA9E0-A9FFAA37-AA3FAA4EAA4FAA5AAA5BAA7C-AA7FAAC3-AADAAAE0-ABBFABEEABEFABFA-ABFFD7A4-D7AFD7C7-D7CAD7FC-F8FFFA2EFA2FFA6EFA6FFADA-FAFFFB07-FB12FB18-FB1CFB37FB3DFB3FFB42FB45FBB2-FBD2FD40-FD4FFD90FD91FDC8-FDEFFDFEFDFFFE1A-FE1FFE27-FE2FFE53FE67FE6C-FE6FFE75FEFD-FF00FFBF-FFC1FFC8FFC9FFD0FFD1FFD8FFD9FFDD-FFDFFFE7FFEF-FFFBFFFEFFFF",Cc:"0000-001F007F-009F",Cf:"00AD0600-060306DD070F17B417B5200B-200F202A-202E2060-2064206A-206FFEFFFFF9-FFFB",Co:"E000-F8FF",Cs:"D800-DFFF",Cn:"03780379037F-0383038B038D03A20526-05300557055805600588058B-059005C8-05CF05EB-05EF05F5-05FF06040605061C061D0620065F070E074B074C07B2-07BF07FB-07FF082E082F083F-08FF093A093B094F095609570973-097809800984098D098E0991099209A909B109B3-09B509BA09BB09C509C609C909CA09CF-09D609D8-09DB09DE09E409E509FC-0A000A040A0B-0A0E0A110A120A290A310A340A370A3A0A3B0A3D0A43-0A460A490A4A0A4E-0A500A52-0A580A5D0A5F-0A650A76-0A800A840A8E0A920AA90AB10AB40ABA0ABB0AC60ACA0ACE0ACF0AD1-0ADF0AE40AE50AF00AF2-0B000B040B0D0B0E0B110B120B290B310B340B3A0B3B0B450B460B490B4A0B4E-0B550B58-0B5B0B5E0B640B650B72-0B810B840B8B-0B8D0B910B96-0B980B9B0B9D0BA0-0BA20BA5-0BA70BAB-0BAD0BBA-0BBD0BC3-0BC50BC90BCE0BCF0BD1-0BD60BD8-0BE50BFB-0C000C040C0D0C110C290C340C3A-0C3C0C450C490C4E-0C540C570C5A-0C5F0C640C650C70-0C770C800C810C840C8D0C910CA90CB40CBA0CBB0CC50CC90CCE-0CD40CD7-0CDD0CDF0CE40CE50CF00CF3-0D010D040D0D0D110D290D3A-0D3C0D450D490D4E-0D560D58-0D5F0D640D650D76-0D780D800D810D840D97-0D990DB20DBC0DBE0DBF0DC7-0DC90DCB-0DCE0DD50DD70DE0-0DF10DF5-0E000E3B-0E3E0E5C-0E800E830E850E860E890E8B0E8C0E8E-0E930E980EA00EA40EA60EA80EA90EAC0EBA0EBE0EBF0EC50EC70ECE0ECF0EDA0EDB0EDE-0EFF0F480F6D-0F700F8C-0F8F0F980FBD0FCD0FD9-0FFF10C6-10CF10FD-10FF1249124E124F12571259125E125F1289128E128F12B112B612B712BF12C112C612C712D7131113161317135B-135E137D-137F139A-139F13F5-13FF169D-169F16F1-16FF170D1715-171F1737-173F1754-175F176D17711774-177F17DE17DF17EA-17EF17FA-17FF180F181A-181F1878-187F18AB-18AF18F6-18FF191D-191F192C-192F193C-193F1941-1943196E196F1975-197F19AC-19AF19CA-19CF19DB-19DD1A1C1A1D1A5F1A7D1A7E1A8A-1A8F1A9A-1A9F1AAE-1AFF1B4C-1B4F1B7D-1B7F1BAB-1BAD1BBA-1BFF1C38-1C3A1C4A-1C4C1C80-1CCF1CF3-1CFF1DE7-1DFC1F161F171F1E1F1F1F461F471F4E1F4F1F581F5A1F5C1F5E1F7E1F7F1FB51FC51FD41FD51FDC1FF01FF11FF51FFF2065-206920722073208F2095-209F20B9-20CF20F1-20FF218A-218F23E9-23FF2427-243F244B-245F26CE26E226E4-26E727002705270A270B2728274C274E2753-2755275F27602795-279727B027BF27CB27CD-27CF2B4D-2B4F2B5A-2BFF2C2F2C5F2CF2-2CF82D26-2D2F2D66-2D6E2D70-2D7F2D97-2D9F2DA72DAF2DB72DBF2DC72DCF2DD72DDF2E32-2E7F2E9A2EF4-2EFF2FD6-2FEF2FFC-2FFF3040309730983100-3104312E-3130318F31B8-31BF31E4-31EF321F32FF4DB6-4DBF9FCC-9FFFA48D-A48FA4C7-A4CFA62C-A63FA660A661A674-A67BA698-A69FA6F8-A6FFA78D-A7FAA82C-A82FA83A-A83FA878-A87FA8C5-A8CDA8DA-A8DFA8FC-A8FFA954-A95EA97D-A97FA9CEA9DA-A9DDA9E0-A9FFAA37-AA3FAA4EAA4FAA5AAA5BAA7C-AA7FAAC3-AADAAAE0-ABBFABEEABEFABFA-ABFFD7A4-D7AFD7C7-D7CAD7FC-D7FFFA2EFA2FFA6EFA6FFADA-FAFFFB07-FB12FB18-FB1CFB37FB3DFB3FFB42FB45FBB2-FBD2FD40-FD4FFD90FD91FDC8-FDEFFDFEFDFFFE1A-FE1FFE27-FE2FFE53FE67FE6C-FE6FFE75FEFDFEFEFF00FFBF-FFC1FFC8FFC9FFD0FFD1FFD8FFD9FFDD-FFDFFFE7FFEF-FFF8FFFEFFFF"})}),define("ace/token_iterator",["require","exports","module"],function(e,t,n){"use strict";var r=function(e,t,n){this.$session=e,this.$row=t,this.$rowTokens=e.getTokens(t);var r=e.getTokenAt(t,n);this.$tokenIndex=r?r.index:-1};(function(){this.stepBackward=function(){this.$tokenIndex-=1;while(this.$tokenIndex<0){this.$row-=1;if(this.$row<0)return this.$row=0,null;this.$rowTokens=this.$session.getTokens(this.$row),this.$tokenIndex=this.$rowTokens.length-1}return this.$rowTokens[this.$tokenIndex]},this.stepForward=function(){this.$tokenIndex+=1;var e;while(this.$tokenIndex>=this.$rowTokens.length){this.$row+=1,e||(e=this.$session.getLength());if(this.$row>=e)return this.$row=e-1,null;this.$rowTokens=this.$session.getTokens(this.$row),this.$tokenIndex=0}return this.$rowTokens[this.$tokenIndex]},this.getCurrentToken=function(){return this.$rowTokens[this.$tokenIndex]},this.getCurrentTokenRow=function(){return this.$row},this.getCurrentTokenColumn=function(){var e=this.$rowTokens,t=this.$tokenIndex,n=e[t].start;if(n!==undefined)return n;n=0;while(t>0)t-=1,n+=e[t].value.length;return n},this.getCurrentTokenPosition=function(){return{row:this.$row,column:this.getCurrentTokenColumn()}}}).call(r.prototype),t.TokenIterator=r}),define("ace/mode/text",["require","exports","module","ace/tokenizer","ace/mode/text_highlight_rules","ace/mode/behaviour","ace/unicode","ace/lib/lang","ace/token_iterator","ace/range"],function(e,t,n){"use strict";var r=e("../tokenizer").Tokenizer,i=e("./text_highlight_rules").TextHighlightRules,s=e("./behaviour").Behaviour,o=e("../unicode"),u=e("../lib/lang"),a=e("../token_iterator").TokenIterator,f=e("../range").Range,l=function(){this.HighlightRules=i,this.$behaviour=new s};(function(){this.tokenRe=new RegExp("^["+o.packages.L+o.packages.Mn+o.packages.Mc+o.packages.Nd+o.packages.Pc+"\\$_]+","g"),this.nonTokenRe=new RegExp("^(?:[^"+o.packages.L+o.packages.Mn+o.packages.Mc+o.packages.Nd+o.packages.Pc+"\\$_]|\\s])+","g"),this.getTokenizer=function(){return this.$tokenizer||(this.$highlightRules=this.$highlightRules||new this.HighlightRules,this.$tokenizer=new r(this.$highlightRules.getRules())),this.$tokenizer},this.lineCommentStart="",this.blockComment="",this.toggleCommentLines=function(e,t,n,r){function w(e){for(var t=n;t<=r;t++)e(i.getLine(t),t)}var i=t.doc,s=!0,o=!0,a=Infinity,f=t.getTabSize(),l=!1;if(!this.lineCommentStart){if(!this.blockComment)return!1;var c=this.blockComment.start,h=this.blockComment.end,p=new RegExp("^(\\s*)(?:"+u.escapeRegExp(c)+")"),d=new RegExp("(?:"+u.escapeRegExp(h)+")\\s*$"),v=function(e,t){if(g(e,t))return;if(!s||/\S/.test(e))i.insertInLine({row:t,column:e.length},h),i.insertInLine({row:t,column:a},c)},m=function(e,t){var n;(n=e.match(d))&&i.removeInLine(t,e.length-n[0].length,e.length),(n=e.match(p))&&i.removeInLine(t,n[1].length,n[0].length)},g=function(e,n){if(p.test(e))return!0;var r=t.getTokens(n);for(var i=0;i<r.length;i++)if(r[i].type==="comment")return!0}}else{if(Array.isArray(this.lineCommentStart))var p=this.lineCommentStart.map(u.escapeRegExp).join("|"),c=this.lineCommentStart[0];else var p=u.escapeRegExp(this.lineCommentStart),c=this.lineCommentStart;p=new RegExp("^(\\s*)(?:"+p+") ?"),l=t.getUseSoftTabs();var m=function(e,t){var n=e.match(p);if(!n)return;var r=n[1].length,s=n[0].length;!b(e,r,s)&&n[0][s-1]==" "&&s--,i.removeInLine(t,r,s)},y=c+" ",v=function(e,t){if(!s||/\S/.test(e))b(e,a,a)?i.insertInLine({row:t,column:a},y):i.insertInLine({row:t,column:a},c)},g=function(e,t){return p.test(e)},b=function(e,t,n){var r=0;while(t--&&e.charAt(t)==" ")r++;if(r%f!=0)return!1;var r=0;while(e.charAt(n++)==" ")r++;return f>2?r%f!=f-1:r%f==0}}var E=Infinity;w(function(e,t){var n=e.search(/\S/);n!==-1?(n<a&&(a=n),o&&!g(e,t)&&(o=!1)):E>e.length&&(E=e.length)}),a==Infinity&&(a=E,s=!1,o=!1),l&&a%f!=0&&(a=Math.floor(a/f)*f),w(o?m:v)},this.toggleBlockComment=function(e,t,n,r){var i=this.blockComment;if(!i)return;!i.start&&i[0]&&(i=i[0]);var s=new a(t,r.row,r.column),o=s.getCurrentToken(),u=t.selection,l=t.selection.toOrientedRange(),c,h;if(o&&/comment/.test(o.type)){var p,d;while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.start);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;p=new f(m,g,m,g+i.start.length);break}o=s.stepBackward()}var s=new a(t,r.row,r.column),o=s.getCurrentToken();while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.end);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;d=new f(m,g,m,g+i.end.length);break}o=s.stepForward()}d&&t.remove(d),p&&(t.remove(p),c=p.start.row,h=-i.start.length)}else h=i.start.length,c=n.start.row,t.insert(n.end,i.end),t.insert(n.start,i.start);l.start.row==c&&(l.start.column+=h),l.end.row==c&&(l.end.column+=h),t.selection.fromOrientedRange(l)},this.getNextLineIndent=function(e,t,n){return this.$getIndent(t)},this.checkOutdent=function(e,t,n){return!1},this.autoOutdent=function(e,t,n){},this.$getIndent=function(e){return e.match(/^\s*/)[0]},this.createWorker=function(e){return null},this.createModeDelegates=function(e){this.$embeds=[],this.$modes={};for(var t in e)e[t]&&(this.$embeds.push(t),this.$modes[t]=new e[t]);var n=["toggleBlockComment","toggleCommentLines","getNextLineIndent","checkOutdent","autoOutdent","transformAction","getCompletions"];for(var t=0;t<n.length;t++)(function(e){var r=n[t],i=e[r];e[n[t]]=function(){return this.$delegator(r,arguments,i)}})(this)},this.$delegator=function(e,t,n){var r=t[0];typeof r!="string"&&(r=r[0]);for(var i=0;i<this.$embeds.length;i++){if(!this.$modes[this.$embeds[i]])continue;var s=r.split(this.$embeds[i]);if(!s[0]&&s[1]){t[0]=s[1];var o=this.$modes[this.$embeds[i]];return o[e].apply(o,t)}}var u=n.apply(this,t);return n?u:undefined},this.transformAction=function(e,t,n,r,i){if(this.$behaviour){var s=this.$behaviour.getBehaviours();for(var o in s)if(s[o][t]){var u=s[o][t].apply(this,arguments);if(u)return u}}},this.getKeywords=function(e){if(!this.completionKeywords){var t=this.$tokenizer.rules,n=[];for(var r in t){var i=t[r];for(var s=0,o=i.length;s<o;s++)if(typeof i[s].token=="string")/keyword|support|storage/.test(i[s].token)&&n.push(i[s].regex);else if(typeof i[s].token=="object")for(var u=0,a=i[s].token.length;u<a;u++)if(/keyword|support|storage/.test(i[s].token[u])){var r=i[s].regex.match(/\(.+?\)/g)[u];n.push(r.substr(1,r.length-2))}}this.completionKeywords=n}return e?n.concat(this.$keywordList||[]):this.$keywordList},this.$createKeywordList=function(){return this.$highlightRules||this.getTokenizer(),this.$keywordList=this.$highlightRules.$keywordList||[]},this.getCompletions=function(e,t,n,r){var i=this.$keywordList||this.$createKeywordList();return i.map(function(e){return{name:e,value:e,score:0,meta:"keyword"}})},this.$id="ace/mode/text"}).call(l.prototype),t.Mode=l}),define("ace/apply_delta",["require","exports","module"],function(e,t,n){"use strict";function r(e,t){throw console.log("Invalid Delta:",e),"Invalid Delta: "+t}function i(e,t){return t.row>=0&&t.row<e.length&&t.column>=0&&t.column<=e[t.row].length}function s(e,t){t.action!="insert"&&t.action!="remove"&&r(t,"delta.action must be 'insert' or 'remove'"),t.lines instanceof Array||r(t,"delta.lines must be an Array"),(!t.start||!t.end)&&r(t,"delta.start/end must be an present");var n=t.start;i(e,t.start)||r(t,"delta.start must be contained in document");var s=t.end;t.action=="remove"&&!i(e,s)&&r(t,"delta.end must contained in document for 'remove' actions");var o=s.row-n.row,u=s.column-(o==0?n.column:0);(o!=t.lines.length-1||t.lines[o].length!=u)&&r(t,"delta.range must match delta lines")}t.applyDelta=function(e,t,n){var r=t.start.row,i=t.start.column,s=e[r]||"";switch(t.action){case"insert":var o=t.lines;if(o.length===1)e[r]=s.substring(0,i)+t.lines[0]+s.substring(i);else{var u=[r,1].concat(t.lines);e.splice.apply(e,u),e[r]=s.substring(0,i)+e[r],e[r+t.lines.length-1]+=s.substring(i)}break;case"remove":var a=t.end.column,f=t.end.row;r===f?e[r]=s.substring(0,i)+s.substring(a):e.splice(r,f-r+1,s.substring(0,i)+e[f].substring(a))}}}),define("ace/anchor",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/event_emitter").EventEmitter,s=t.Anchor=function(e,t,n){this.$onChange=this.onChange.bind(this),this.attach(e),typeof n=="undefined"?this.setPosition(t.row,t.column):this.setPosition(t,n)};(function(){function e(e,t,n){var r=n?e.column<=t.column:e.column<t.column;return e.row<t.row||e.row==t.row&&r}function t(t,n,r){var i=t.action=="insert",s=(i?1:-1)*(t.end.row-t.start.row),o=(i?1:-1)*(t.end.column-t.start.column),u=t.start,a=i?u:t.end;return e(n,u,r)?{row:n.row,column:n.column}:e(a,n,!r)?{row:n.row+s,column:n.column+(n.row==a.row?o:0)}:{row:u.row,column:u.column}}r.implement(this,i),this.getPosition=function(){return this.$clipPositionToDocument(this.row,this.column)},this.getDocument=function(){return this.document},this.$insertRight=!1,this.onChange=function(e){if(e.start.row==e.end.row&&e.start.row!=this.row)return;if(e.start.row>this.row)return;var n=t(e,{row:this.row,column:this.column},this.$insertRight);this.setPosition(n.row,n.column,!0)},this.setPosition=function(e,t,n){var r;n?r={row:e,column:t}:r=this.$clipPositionToDocument(e,t);if(this.row==r.row&&this.column==r.column)return;var i={row:this.row,column:this.column};this.row=r.row,this.column=r.column,this._signal("change",{old:i,value:r})},this.detach=function(){this.document.removeEventListener("change",this.$onChange)},this.attach=function(e){this.document=e||this.document,this.document.on("change",this.$onChange)},this.$clipPositionToDocument=function(e,t){var n={};return e>=this.document.getLength()?(n.row=Math.max(0,this.document.getLength()-1),n.column=this.document.getLine(n.row).length):e<0?(n.row=0,n.column=0):(n.row=e,n.column=Math.min(this.document.getLine(n.row).length,Math.max(0,t))),t<0&&(n.column=0),n}}).call(s.prototype)}),define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./apply_delta").applyDelta,s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=e("./anchor").Anchor,a=function(e){this.$lines=[""],e.length===0?this.$lines=[""]:Array.isArray(e)?this.insertMergedLines({row:0,column:0},e):this.insert({row:0,column:0},e)};(function(){r.implement(this,s),this.setValue=function(e){var t=this.getLength()-1;this.remove(new o(0,0,t,this.getLine(t).length)),this.insert({row:0,column:0},e)},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(e,t){return new u(this,e,t)},"aaa".split(/a/).length===0?this.$split=function(e){return e.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(e){return e.split(/\r\n|\r|\n/)},this.$detectNewLine=function(e){var t=e.match(/^.*?(\r\n|\r|\n)/m);this.$autoNewLine=t?t[1]:"\n",this._signal("changeNewLineMode")},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";default:return this.$autoNewLine||"\n"}},this.$autoNewLine="",this.$newLineMode="auto",this.setNewLineMode=function(e){if(this.$newLineMode===e)return;this.$newLineMode=e,this._signal("changeNewLineMode")},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(e){return e=="\r\n"||e=="\r"||e=="\n"},this.getLine=function(e){return this.$lines[e]||""},this.getLines=function(e,t){return this.$lines.slice(e,t+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(e){return this.getLinesForRange(e).join(this.getNewLineCharacter())},this.getLinesForRange=function(e){var t;if(e.start.row===e.end.row)t=[this.getLine(e.start.row).substring(e.start.column,e.end.column)];else{t=this.getLines(e.start.row,e.end.row),t[0]=(t[0]||"").substring(e.start.column);var n=t.length-1;e.end.row-e.start.row==n&&(t[n]=t[n].substring(0,e.end.column))}return t},this.insertLines=function(e,t){return console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."),this.insertFullLines(e,t)},this.removeLines=function(e,t){return console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."),this.removeFullLines(e,t)},this.insertNewLine=function(e){return console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."),this.insertMergedLines(e,["",""])},this.insert=function(e,t){return this.getLength()<=1&&this.$detectNewLine(t),this.insertMergedLines(e,this.$split(t))},this.insertInLine=function(e,t){var n=this.clippedPos(e.row,e.column),r=this.pos(e.row,e.column+t.length);return this.applyDelta({start:n,end:r,action:"insert",lines:[t]},!0),this.clonePos(r)},this.clippedPos=function(e,t){var n=this.getLength();e===undefined?e=n:e<0?e=0:e>=n&&(e=n-1,t=undefined);var r=this.getLine(e);return t==undefined&&(t=r.length),t=Math.min(Math.max(t,0),r.length),{row:e,column:t}},this.clonePos=function(e){return{row:e.row,column:e.column}},this.pos=function(e,t){return{row:e,column:t}},this.$clipPosition=function(e){var t=this.getLength();return e.row>=t?(e.row=Math.max(0,t-1),e.column=this.getLine(t-1).length):(e.row=Math.max(0,e.row),e.column=Math.min(Math.max(e.column,0),this.getLine(e.row).length)),e},this.insertFullLines=function(e,t){e=Math.min(Math.max(e,0),this.getLength());var n=0;e<this.getLength()?(t=t.concat([""]),n=0):(t=[""].concat(t),e--,n=this.$lines[e].length),this.insertMergedLines({row:e,column:n},t)},this.insertMergedLines=function(e,t){var n=this.clippedPos(e.row,e.column),r={row:n.row+t.length-1,column:(t.length==1?n.column:0)+t[t.length-1].length};return this.applyDelta({start:n,end:r,action:"insert",lines:t}),this.clonePos(r)},this.remove=function(e){var t=this.clippedPos(e.start.row,e.start.column),n=this.clippedPos(e.end.row,e.end.column);return this.applyDelta({start:t,end:n,action:"remove",lines:this.getLinesForRange({start:t,end:n})}),this.clonePos(t)},this.removeInLine=function(e,t,n){var r=this.clippedPos(e,t),i=this.clippedPos(e,n);return this.applyDelta({start:r,end:i,action:"remove",lines:this.getLinesForRange({start:r,end:i})},!0),this.clonePos(r)},this.removeFullLines=function(e,t){e=Math.min(Math.max(0,e),this.getLength()-1),t=Math.min(Math.max(0,t),this.getLength()-1);var n=t==this.getLength()-1&&e>0,r=t<this.getLength()-1,i=n?e-1:e,s=n?this.getLine(i).length:0,u=r?t+1:t,a=r?0:this.getLine(u).length,f=new o(i,s,u,a),l=this.$lines.slice(e,t+1);return this.applyDelta({start:f.start,end:f.end,action:"remove",lines:this.getLinesForRange(f)}),l},this.removeNewLine=function(e){e<this.getLength()-1&&e>=0&&this.applyDelta({start:this.pos(e,this.getLine(e).length),end:this.pos(e+1,0),action:"remove",lines:["",""]})},this.replace=function(e,t){e instanceof o||(e=o.fromPoints(e.start,e.end));if(t.length===0&&e.isEmpty())return e.start;if(t==this.getTextRange(e))return e.end;this.remove(e);var n;return t?n=this.insert(e.start,t):n=e.start,n},this.applyDeltas=function(e){for(var t=0;t<e.length;t++)this.applyDelta(e[t])},this.revertDeltas=function(e){for(var t=e.length-1;t>=0;t--)this.revertDelta(e[t])},this.applyDelta=function(e,t){var n=e.action=="insert";if(n?e.lines.length<=1&&!e.lines[0]:!o.comparePoints(e.start,e.end))return;n&&e.lines.length>2e4&&this.$splitAndapplyLargeDelta(e,2e4),i(this.$lines,e,t),this._signal("change",e)},this.$splitAndapplyLargeDelta=function(e,t){var n=e.lines,r=n.length,i=e.start.row,s=e.start.column,o=0,u=0;do{o=u,u+=t-1;var a=n.slice(o,u);if(u>r){e.lines=a,e.start.row=i+o,e.start.column=s;break}a.push(""),this.applyDelta({start:this.pos(i+o,s),end:this.pos(i+u,s=0),action:e.action,lines:a},!0)}while(!0)},this.revertDelta=function(e){this.applyDelta({start:this.clonePos(e.start),end:this.clonePos(e.end),action:e.action=="insert"?"remove":"insert",lines:e.lines.slice()})},this.indexToPosition=function(e,t){var n=this.$lines||this.getAllLines(),r=this.getNewLineCharacter().length;for(var i=t||0,s=n.length;i<s;i++){e-=n[i].length+r;if(e<0)return{row:i,column:e+n[i].length+r}}return{row:s-1,column:n[s-1].length}},this.positionToIndex=function(e,t){var n=this.$lines||this.getAllLines(),r=this.getNewLineCharacter().length,i=0,s=Math.min(e.row,n.length);for(var o=t||0;o<s;++o)i+=n[o].length+r;return i+e.column}}).call(a.prototype),t.Document=a}),define("ace/background_tokenizer",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/event_emitter").EventEmitter,s=function(e,t){this.running=!1,this.lines=[],this.states=[],this.currentLine=0,this.tokenizer=e;var n=this;this.$worker=function(){if(!n.running)return;var e=new Date,t=n.currentLine,r=-1,i=n.doc,s=t;while(n.lines[t])t++;var o=i.getLength(),u=0;n.running=!1;while(t<o){n.$tokenizeRow(t),r=t;do t++;while(n.lines[t]);u++;if(u%5===0&&new Date-e>20){n.running=setTimeout(n.$worker,20);break}}n.currentLine=t,s<=r&&n.fireUpdateEvent(s,r)}};(function(){r.implement(this,i),this.setTokenizer=function(e){this.tokenizer=e,this.lines=[],this.states=[],this.start(0)},this.setDocument=function(e){this.doc=e,this.lines=[],this.states=[],this.stop()},this.fireUpdateEvent=function(e,t){var n={first:e,last:t};this._signal("update",{data:n})},this.start=function(e){this.currentLine=Math.min(e||0,this.currentLine,this.doc.getLength()),this.lines.splice(this.currentLine,this.lines.length),this.states.splice(this.currentLine,this.states.length),this.stop(),this.running=setTimeout(this.$worker,700)},this.scheduleStart=function(){this.running||(this.running=setTimeout(this.$worker,700))},this.$updateOnChange=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.lines[t]=null;else if(e.action=="remove")this.lines.splice(t,n+1,null),this.states.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.lines.splice.apply(this.lines,r),this.states.splice.apply(this.states,r)}this.currentLine=Math.min(t,this.currentLine,this.doc.getLength()),this.stop()},this.stop=function(){this.running&&clearTimeout(this.running),this.running=!1},this.getTokens=function(e){return this.lines[e]||this.$tokenizeRow(e)},this.getState=function(e){return this.currentLine==e&&this.$tokenizeRow(e),this.states[e]||"start"},this.$tokenizeRow=function(e){var t=this.doc.getLine(e),n=this.states[e-1],r=this.tokenizer.getLineTokens(t,n,e);return this.states[e]+""!=r.state+""?(this.states[e]=r.state,this.lines[e+1]=null,this.currentLine>e+1&&(this.currentLine=e+1)):this.currentLine==e&&(this.currentLine=e+1),this.lines[e]=r.tokens}}).call(s.prototype),t.BackgroundTokenizer=s}),define("ace/search_highlight",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(e,t,n){this.setRegexp(e),this.clazz=t,this.type=n||"text"};(function(){this.MAX_RANGES=500,this.setRegexp=function(e){if(this.regExp+""==e+"")return;this.regExp=e,this.cache=[]},this.update=function(e,t,n,i){if(!this.regExp)return;var o=i.firstRow,u=i.lastRow;for(var a=o;a<=u;a++){var f=this.cache[a];f==null&&(f=r.getMatchOffsets(n.getLine(a),this.regExp),f.length>this.MAX_RANGES&&(f=f.slice(0,this.MAX_RANGES)),f=f.map(function(e){return new s(a,e.offset,a,e.offset+e.length)}),this.cache[a]=f.length?f:"");for(var l=f.length;l--;)t.drawSingleLineMarker(e,f[l].toScreenRange(n),this.clazz,i)}}}).call(o.prototype),t.SearchHighlight=o}),define("ace/edit_session/fold_line",["require","exports","module","ace/range"],function(e,t,n){"use strict";function i(e,t){this.foldData=e,Array.isArray(t)?this.folds=t:t=this.folds=[t];var n=t[t.length-1];this.range=new r(t[0].start.row,t[0].start.column,n.end.row,n.end.column),this.start=this.range.start,this.end=this.range.end,this.folds.forEach(function(e){e.setFoldLine(this)},this)}var r=e("../range").Range;(function(){this.shiftRow=function(e){this.start.row+=e,this.end.row+=e,this.folds.forEach(function(t){t.start.row+=e,t.end.row+=e})},this.addFold=function(e){if(e.sameRow){if(e.start.row<this.startRow||e.endRow>this.endRow)throw new Error("Can't add a fold to this FoldLine as it has no connection");this.folds.push(e),this.folds.sort(function(e,t){return-e.range.compareEnd(t.start.row,t.start.column)}),this.range.compareEnd(e.start.row,e.start.column)>0?(this.end.row=e.end.row,this.end.column=e.end.column):this.range.compareStart(e.end.row,e.end.column)<0&&(this.start.row=e.start.row,this.start.column=e.start.column)}else if(e.start.row==this.end.row)this.folds.push(e),this.end.row=e.end.row,this.end.column=e.end.column;else{if(e.end.row!=this.start.row)throw new Error("Trying to add fold to FoldRow that doesn't have a matching row");this.folds.unshift(e),this.start.row=e.start.row,this.start.column=e.start.column}e.foldLine=this},this.containsRow=function(e){return e>=this.start.row&&e<=this.end.row},this.walk=function(e,t,n){var r=0,i=this.folds,s,o,u,a=!0;t==null&&(t=this.end.row,n=this.end.column);for(var f=0;f<i.length;f++){s=i[f],o=s.range.compareStart(t,n);if(o==-1){e(null,t,n,r,a);return}u=e(null,s.start.row,s.start.column,r,a),u=!u&&e(s.placeholder,s.start.row,s.start.column,r);if(u||o===0)return;a=!s.sameRow,r=s.end.column}e(null,t,n,r,a)},this.getNextFoldTo=function(e,t){var n,r;for(var i=0;i<this.folds.length;i++){n=this.folds[i],r=n.range.compareEnd(e,t);if(r==-1)return{fold:n,kind:"after"};if(r===0)return{fold:n,kind:"inside"}}return null},this.addRemoveChars=function(e,t,n){var r=this.getNextFoldTo(e,t),i,s;if(r){i=r.fold;if(r.kind=="inside"&&i.start.column!=t&&i.start.row!=e)window.console&&window.console.log(e,t,i);else if(i.start.row==e){s=this.folds;var o=s.indexOf(i);o===0&&(this.start.column+=n);for(o;o<s.length;o++){i=s[o],i.start.column+=n;if(!i.sameRow)return;i.end.column+=n}this.end.column+=n}}},this.split=function(e,t){var n=this.getNextFoldTo(e,t);if(!n||n.kind=="inside")return null;var r=n.fold,s=this.folds,o=this.foldData,u=s.indexOf(r),a=s[u-1];this.end.row=a.end.row,this.end.column=a.end.column,s=s.splice(u,s.length-u);var f=new i(o,s);return o.splice(o.indexOf(this)+1,0,f),f},this.merge=function(e){var t=e.folds;for(var n=0;n<t.length;n++)this.addFold(t[n]);var r=this.foldData;r.splice(r.indexOf(e),1)},this.toString=function(){var e=[this.range.toString()+": ["];return this.folds.forEach(function(t){e.push(" "+t.toString())}),e.push("]"),e.join("\n")},this.idxToPosition=function(e){var t=0;for(var n=0;n<this.folds.length;n++){var r=this.folds[n];e-=r.start.column-t;if(e<0)return{row:r.start.row,column:r.start.column+e};e-=r.placeholder.length;if(e<0)return r.start;t=r.end.column}return{row:this.end.row,column:this.end.column+e}}}).call(i.prototype),t.FoldLine=i}),define("ace/range_list",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("./range").Range,i=r.comparePoints,s=function(){this.ranges=[]};(function(){this.comparePoints=i,this.pointIndex=function(e,t,n){var r=this.ranges;for(var s=n||0;s<r.length;s++){var o=r[s],u=i(e,o.end);if(u>0)continue;var a=i(e,o.start);return u===0?t&&a!==0?-s-2:s:a>0||a===0&&!t?s:-s-1}return-s-1},this.add=function(e){var t=!e.isEmpty(),n=this.pointIndex(e.start,t);n<0&&(n=-n-1);var r=this.pointIndex(e.end,t,n);return r<0?r=-r-1:r++,this.ranges.splice(n,r-n,e)},this.addList=function(e){var t=[];for(var n=e.length;n--;)t.push.apply(t,this.add(e[n]));return t},this.substractPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges.splice(t,1)},this.merge=function(){var e=[],t=this.ranges;t=t.sort(function(e,t){return i(e.start,t.start)});var n=t[0],r;for(var s=1;s<t.length;s++){r=n,n=t[s];var o=i(r.end,n.start);if(o<0)continue;if(o==0&&!r.isEmpty()&&!n.isEmpty())continue;i(r.end,n.end)<0&&(r.end.row=n.end.row,r.end.column=n.end.column),t.splice(s,1),e.push(n),n=r,s--}return this.ranges=t,e},this.contains=function(e,t){return this.pointIndex({row:e,column:t})>=0},this.containsPoint=function(e){return this.pointIndex(e)>=0},this.rangeAtPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges[t]},this.clipRows=function(e,t){var n=this.ranges;if(n[0].start.row>t||n[n.length-1].start.row<e)return[];var r=this.pointIndex({row:e,column:0});r<0&&(r=-r-1);var i=this.pointIndex({row:t,column:0},r);i<0&&(i=-i-1);var s=[];for(var o=r;o<i;o++)s.push(n[o]);return s},this.removeAll=function(){return this.ranges.splice(0,this.ranges.length)},this.attach=function(e){this.session&&this.detach(),this.session=e,this.onChange=this.$onChange.bind(this),this.session.on("change",this.onChange)},this.detach=function(){if(!this.session)return;this.session.removeListener("change",this.onChange),this.session=null},this.$onChange=function(e){if(e.action=="insert")var t=e.start,n=e.end;else var n=e.start,t=e.end;var r=t.row,i=n.row,s=i-r,o=-t.column+n.column,u=this.ranges;for(var a=0,f=u.length;a<f;a++){var l=u[a];if(l.end.row<r)continue;if(l.start.row>r)break;l.start.row==r&&l.start.column>=t.column&&(l.start.column!=t.column||!this.$insertRight)&&(l.start.column+=o,l.start.row+=s);if(l.end.row==r&&l.end.column>=t.column){if(l.end.column==t.column&&this.$insertRight)continue;l.end.column==t.column&&o>0&&a<f-1&&l.end.column>l.start.column&&l.end.column==u[a+1].start.column&&(l.end.column-=o),l.end.column+=o,l.end.row+=s}}if(s!=0&&a<f)for(;a<f;a++){var l=u[a];l.start.row+=s,l.end.row+=s}}}).call(s.prototype),t.RangeList=s}),define("ace/edit_session/fold",["require","exports","module","ace/range","ace/range_list","ace/lib/oop"],function(e,t,n){"use strict";function u(e,t){e.row-=t.row,e.row==0&&(e.column-=t.column)}function a(e,t){u(e.start,t),u(e.end,t)}function f(e,t){e.row==0&&(e.column+=t.column),e.row+=t.row}function l(e,t){f(e.start,t),f(e.end,t)}var r=e("../range").Range,i=e("../range_list").RangeList,s=e("../lib/oop"),o=t.Fold=function(e,t){this.foldLine=null,this.placeholder=t,this.range=e,this.start=e.start,this.end=e.end,this.sameRow=e.start.row==e.end.row,this.subFolds=this.ranges=[]};s.inherits(o,i),function(){this.toString=function(){return'"'+this.placeholder+'" '+this.range.toString()},this.setFoldLine=function(e){this.foldLine=e,this.subFolds.forEach(function(t){t.setFoldLine(e)})},this.clone=function(){var e=this.range.clone(),t=new o(e,this.placeholder);return this.subFolds.forEach(function(e){t.subFolds.push(e.clone())}),t.collapseChildren=this.collapseChildren,t},this.addSubFold=function(e){if(this.range.isEqual(e))return;if(!this.range.containsRange(e))throw new Error("A fold can't intersect already existing fold"+e.range+this.range);a(e,this.start);var t=e.start.row,n=e.start.column;for(var r=0,i=-1;r<this.subFolds.length;r++){i=this.subFolds[r].range.compare(t,n);if(i!=1)break}var s=this.subFolds[r];if(i==0)return s.addSubFold(e);var t=e.range.end.row,n=e.range.end.column;for(var o=r,i=-1;o<this.subFolds.length;o++){i=this.subFolds[o].range.compare(t,n);if(i!=1)break}var u=this.subFolds[o];if(i==0)throw new Error("A fold can't intersect already existing fold"+e.range+this.range);var f=this.subFolds.splice(r,o-r,e);return e.setFoldLine(this.foldLine),e},this.restoreRange=function(e){return l(e,this.start)}}.call(o.prototype)}),define("ace/edit_session/folding",["require","exports","module","ace/range","ace/edit_session/fold_line","ace/edit_session/fold","ace/token_iterator"],function(e,t,n){"use strict";function u(){this.getFoldAt=function(e,t,n){var r=this.getFoldLine(e);if(!r)return null;var i=r.folds;for(var s=0;s<i.length;s++){var o=i[s];if(o.range.contains(e,t)){if(n==1&&o.range.isEnd(e,t))continue;if(n==-1&&o.range.isStart(e,t))continue;return o}}},this.getFoldsInRange=function(e){var t=e.start,n=e.end,r=this.$foldData,i=[];t.column+=1,n.column-=1;for(var s=0;s<r.length;s++){var o=r[s].range.compareRange(e);if(o==2)continue;if(o==-2)break;var u=r[s].folds;for(var a=0;a<u.length;a++){var f=u[a];o=f.range.compareRange(e);if(o==-2)break;if(o==2)continue;if(o==42)break;i.push(f)}}return t.column-=1,n.column+=1,i},this.getFoldsInRangeList=function(e){if(Array.isArray(e)){var t=[];e.forEach(function(e){t=t.concat(this.getFoldsInRange(e))},this)}else var t=this.getFoldsInRange(e);return t},this.getAllFolds=function(){var e=[],t=this.$foldData;for(var n=0;n<t.length;n++)for(var r=0;r<t[n].folds.length;r++)e.push(t[n].folds[r]);return e},this.getFoldStringAt=function(e,t,n,r){r=r||this.getFoldLine(e);if(!r)return null;var i={end:{column:0}},s,o;for(var u=0;u<r.folds.length;u++){o=r.folds[u];var a=o.range.compareEnd(e,t);if(a==-1){s=this.getLine(o.start.row).substring(i.end.column,o.start.column);break}if(a===0)return null;i=o}return s||(s=this.getLine(o.start.row).substring(i.end.column)),n==-1?s.substring(0,t-i.end.column):n==1?s.substring(t-i.end.column):s},this.getFoldLine=function(e,t){var n=this.$foldData,r=0;t&&(r=n.indexOf(t)),r==-1&&(r=0);for(r;r<n.length;r++){var i=n[r];if(i.start.row<=e&&i.end.row>=e)return i;if(i.end.row>e)return null}return null},this.getNextFoldLine=function(e,t){var n=this.$foldData,r=0;t&&(r=n.indexOf(t)),r==-1&&(r=0);for(r;r<n.length;r++){var i=n[r];if(i.end.row>=e)return i}return null},this.getFoldedRowCount=function(e,t){var n=this.$foldData,r=t-e+1;for(var i=0;i<n.length;i++){var s=n[i],o=s.end.row,u=s.start.row;if(o>=t){u<t&&(u>=e?r-=t-u:r=0);break}o>=e&&(u>=e?r-=o-u:r-=o-e+1)}return r},this.$addFoldLine=function(e){return this.$foldData.push(e),this.$foldData.sort(function(e,t){return e.start.row-t.start.row}),e},this.addFold=function(e,t){var n=this.$foldData,r=!1,o;e instanceof s?o=e:(o=new s(t,e),o.collapseChildren=t.collapseChildren),this.$clipRangeToDocument(o.range);var u=o.start.row,a=o.start.column,f=o.end.row,l=o.end.column;if(u<f||u==f&&a<=l-2){var c=this.getFoldAt(u,a,1),h=this.getFoldAt(f,l,-1);if(c&&h==c)return c.addSubFold(o);c&&!c.range.isStart(u,a)&&this.removeFold(c),h&&!h.range.isEnd(f,l)&&this.removeFold(h);var p=this.getFoldsInRange(o.range);p.length>0&&(this.removeFolds(p),p.forEach(function(e){o.addSubFold(e)}));for(var d=0;d<n.length;d++){var v=n[d];if(f==v.start.row){v.addFold(o),r=!0;break}if(u==v.end.row){v.addFold(o),r=!0;if(!o.sameRow){var m=n[d+1];if(m&&m.start.row==f){v.merge(m);break}}break}if(f<=v.start.row)break}return r||(v=this.$addFoldLine(new i(this.$foldData,o))),this.$useWrapMode?this.$updateWrapData(v.start.row,v.start.row):this.$updateRowLengthCache(v.start.row,v.start.row),this.$modified=!0,this._signal("changeFold",{data:o,action:"add"}),o}throw new Error("The range has to be at least 2 characters width")},this.addFolds=function(e){e.forEach(function(e){this.addFold(e)},this)},this.removeFold=function(e){var t=e.foldLine,n=t.start.row,r=t.end.row,i=this.$foldData,s=t.folds;if(s.length==1)i.splice(i.indexOf(t),1);else if(t.range.isEnd(e.end.row,e.end.column))s.pop(),t.end.row=s[s.length-1].end.row,t.end.column=s[s.length-1].end.column;else if(t.range.isStart(e.start.row,e.start.column))s.shift(),t.start.row=s[0].start.row,t.start.column=s[0].start.column;else if(e.sameRow)s.splice(s.indexOf(e),1);else{var o=t.split(e.start.row,e.start.column);s=o.folds,s.shift(),o.start.row=s[0].start.row,o.start.column=s[0].start.column}this.$updating||(this.$useWrapMode?this.$updateWrapData(n,r):this.$updateRowLengthCache(n,r)),this.$modified=!0,this._signal("changeFold",{data:e,action:"remove"})},this.removeFolds=function(e){var t=[];for(var n=0;n<e.length;n++)t.push(e[n]);t.forEach(function(e){this.removeFold(e)},this),this.$modified=!0},this.expandFold=function(e){this.removeFold(e),e.subFolds.forEach(function(t){e.restoreRange(t),this.addFold(t)},this),e.collapseChildren>0&&this.foldAll(e.start.row+1,e.end.row,e.collapseChildren-1),e.subFolds=[]},this.expandFolds=function(e){e.forEach(function(e){this.expandFold(e)},this)},this.unfold=function(e,t){var n,i;e==null?(n=new r(0,0,this.getLength(),0),t=!0):typeof e=="number"?n=new r(e,0,e,this.getLine(e).length):"row"in e?n=r.fromPoints(e,e):n=e,i=this.getFoldsInRangeList(n);if(t)this.removeFolds(i);else{var s=i;while(s.length)this.expandFolds(s),s=this.getFoldsInRangeList(n)}if(i.length)return i},this.isRowFolded=function(e,t){return!!this.getFoldLine(e,t)},this.getRowFoldEnd=function(e,t){var n=this.getFoldLine(e,t);return n?n.end.row:e},this.getRowFoldStart=function(e,t){var n=this.getFoldLine(e,t);return n?n.start.row:e},this.getFoldDisplayLine=function(e,t,n,r,i){r==null&&(r=e.start.row),i==null&&(i=0),t==null&&(t=e.end.row),n==null&&(n=this.getLine(t).length);var s=this.doc,o="";return e.walk(function(e,t,n,u){if(t<r)return;if(t==r){if(n<i)return;u=Math.max(i,u)}e!=null?o+=e:o+=s.getLine(t).substring(u,n)},t,n),o},this.getDisplayLine=function(e,t,n,r){var i=this.getFoldLine(e);if(!i){var s;return s=this.doc.getLine(e),s.substring(r||0,t||s.length)}return this.getFoldDisplayLine(i,e,t,n,r)},this.$cloneFoldData=function(){var e=[];return e=this.$foldData.map(function(t){var n=t.folds.map(function(e){return e.clone()});return new i(e,n)}),e},this.toggleFold=function(e){var t=this.selection,n=t.getRange(),r,i;if(n.isEmpty()){var s=n.start;r=this.getFoldAt(s.row,s.column);if(r){this.expandFold(r);return}(i=this.findMatchingBracket(s))?n.comparePoint(i)==1?n.end=i:(n.start=i,n.start.column++,n.end.column--):(i=this.findMatchingBracket({row:s.row,column:s.column+1}))?(n.comparePoint(i)==1?n.end=i:n.start=i,n.start.column++):n=this.getCommentFoldRange(s.row,s.column)||n}else{var o=this.getFoldsInRange(n);if(e&&o.length){this.expandFolds(o);return}o.length==1&&(r=o[0])}r||(r=this.getFoldAt(n.start.row,n.start.column));if(r&&r.range.toString()==n.toString()){this.expandFold(r);return}var u="...";if(!n.isMultiLine()){u=this.getTextRange(n);if(u.length<4)return;u=u.trim().substring(0,2)+".."}this.addFold(u,n)},this.getCommentFoldRange=function(e,t,n){var i=new o(this,e,t),s=i.getCurrentToken();if(s&&/^comment|string/.test(s.type)){var u=new r,a=new RegExp(s.type.replace(/\..*/,"\\."));if(n!=1){do s=i.stepBackward();while(s&&a.test(s.type));i.stepForward()}u.start.row=i.getCurrentTokenRow(),u.start.column=i.getCurrentTokenColumn()+2,i=new o(this,e,t);if(n!=-1){do s=i.stepForward();while(s&&a.test(s.type));s=i.stepBackward()}else s=i.getCurrentToken();return u.end.row=i.getCurrentTokenRow(),u.end.column=i.getCurrentTokenColumn()+s.value.length-2,u}},this.foldAll=function(e,t,n){n==undefined&&(n=1e5);var r=this.foldWidgets;if(!r)return;t=t||this.getLength(),e=e||0;for(var i=e;i<t;i++){r[i]==null&&(r[i]=this.getFoldWidget(i));if(r[i]!="start")continue;var s=this.getFoldWidgetRange(i);if(s&&s.isMultiLine()&&s.end.row<=t&&s.start.row>=e){i=s.end.row;try{var o=this.addFold("...",s);o&&(o.collapseChildren=n)}catch(u){}}}},this.$foldStyles={manual:1,markbegin:1,markbeginend:1},this.$foldStyle="markbegin",this.setFoldStyle=function(e){if(!this.$foldStyles[e])throw new Error("invalid fold style: "+e+"["+Object.keys(this.$foldStyles).join(", ")+"]");if(this.$foldStyle==e)return;this.$foldStyle=e,e=="manual"&&this.unfold();var t=this.$foldMode;this.$setFolding(null),this.$setFolding(t)},this.$setFolding=function(e){if(this.$foldMode==e)return;this.$foldMode=e,this.off("change",this.$updateFoldWidgets),this.off("tokenizerUpdate",this.$tokenizerUpdateFoldWidgets),this._signal("changeAnnotation");if(!e||this.$foldStyle=="manual"){this.foldWidgets=null;return}this.foldWidgets=[],this.getFoldWidget=e.getFoldWidget.bind(e,this,this.$foldStyle),this.getFoldWidgetRange=e.getFoldWidgetRange.bind(e,this,this.$foldStyle),this.$updateFoldWidgets=this.updateFoldWidgets.bind(this),this.$tokenizerUpdateFoldWidgets=this.tokenizerUpdateFoldWidgets.bind(this),this.on("change",this.$updateFoldWidgets),this.on("tokenizerUpdate",this.$tokenizerUpdateFoldWidgets)},this.getParentFoldRangeData=function(e,t){var n=this.foldWidgets;if(!n||t&&n[e])return{};var r=e-1,i;while(r>=0){var s=n[r];s==null&&(s=n[r]=this.getFoldWidget(r));if(s=="start"){var o=this.getFoldWidgetRange(r);i||(i=o);if(o&&o.end.row>=e)break}r--}return{range:r!==-1&&o,firstRange:i}},this.onFoldWidgetClick=function(e,t){t=t.domEvent;var n={children:t.shiftKey,all:t.ctrlKey||t.metaKey,siblings:t.altKey},r=this.$toggleFoldWidget(e,n);if(!r){var i=t.target||t.srcElement;i&&/ace_fold-widget/.test(i.className)&&(i.className+=" ace_invalid")}},this.$toggleFoldWidget=function(e,t){if(!this.getFoldWidget)return;var n=this.getFoldWidget(e),r=this.getLine(e),i=n==="end"?-1:1,s=this.getFoldAt(e,i===-1?0:r.length,i);if(s){t.children||t.all?this.removeFold(s):this.expandFold(s);return}var o=this.getFoldWidgetRange(e,!0);if(o&&!o.isMultiLine()){s=this.getFoldAt(o.start.row,o.start.column,1);if(s&&o.isEqual(s.range)){this.removeFold(s);return}}if(t.siblings){var u=this.getParentFoldRangeData(e);if(u.range)var a=u.range.start.row+1,f=u.range.end.row;this.foldAll(a,f,t.all?1e4:0)}else t.children?(f=o?o.end.row:this.getLength(),this.foldAll(e+1,f,t.all?1e4:0)):o&&(t.all&&(o.collapseChildren=1e4),this.addFold("...",o));return o},this.toggleFoldWidget=function(e){var t=this.selection.getCursor().row;t=this.getRowFoldStart(t);var n=this.$toggleFoldWidget(t,{});if(n)return;var r=this.getParentFoldRangeData(t,!0);n=r.range||r.firstRange;if(n){t=n.start.row;var i=this.getFoldAt(t,this.getLine(t).length,1);i?this.removeFold(i):this.addFold("...",n)}},this.updateFoldWidgets=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.foldWidgets[t]=null;else if(e.action=="remove")this.foldWidgets.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.foldWidgets.splice.apply(this.foldWidgets,r)}},this.tokenizerUpdateFoldWidgets=function(e){var t=e.data;t.first!=t.last&&this.foldWidgets.length>t.first&&this.foldWidgets.splice(t.first,this.foldWidgets.length)}}var r=e("../range").Range,i=e("./fold_line").FoldLine,s=e("./fold").Fold,o=e("../token_iterator").TokenIterator;t.Folding=u}),define("ace/edit_session/bracket_match",["require","exports","module","ace/token_iterator","ace/range"],function(e,t,n){"use strict";function s(){this.findMatchingBracket=function(e,t){if(e.column==0)return null;var n=t||this.getLine(e.row).charAt(e.column-1);if(n=="")return null;var r=n.match(/([\(\[\{])|([\)\]\}])/);return r?r[1]?this.$findClosingBracket(r[1],e):this.$findOpeningBracket(r[2],e):null},this.getBracketRange=function(e){var t=this.getLine(e.row),n=!0,r,s=t.charAt(e.column-1),o=s&&s.match(/([\(\[\{])|([\)\]\}])/);o||(s=t.charAt(e.column),e={row:e.row,column:e.column+1},o=s&&s.match(/([\(\[\{])|([\)\]\}])/),n=!1);if(!o)return null;if(o[1]){var u=this.$findClosingBracket(o[1],e);if(!u)return null;r=i.fromPoints(e,u),n||(r.end.column++,r.start.column--),r.cursor=r.end}else{var u=this.$findOpeningBracket(o[2],e);if(!u)return null;r=i.fromPoints(u,e),n||(r.start.column++,r.end.column--),r.cursor=r.start}return r},this.$brackets={")":"(","(":")","]":"[","[":"]","{":"}","}":"{"},this.$findOpeningBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("rparen",".paren").replace(/\b(?:end)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn()-2,f=u.value;for(;;){while(a>=0){var l=f.charAt(a);if(l==i){s-=1;if(s==0)return{row:o.getCurrentTokenRow(),column:a+o.getCurrentTokenColumn()}}else l==e&&(s+=1);a-=1}do u=o.stepBackward();while(u&&!n.test(u.type));if(u==null)break;f=u.value,a=f.length-1}return null},this.$findClosingBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("lparen",".paren").replace(/\b(?:start|begin)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn();for(;;){var f=u.value,l=f.length;while(a<l){var c=f.charAt(a);if(c==i){s-=1;if(s==0)return{row:o.getCurrentTokenRow(),column:a+o.getCurrentTokenColumn()}}else c==e&&(s+=1);a+=1}do u=o.stepForward();while(u&&!n.test(u.type));if(u==null)break;a=0}return null}}var r=e("../token_iterator").TokenIterator,i=e("../range").Range;t.BracketMatch=s}),define("ace/edit_session",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/config","ace/lib/event_emitter","ace/selection","ace/mode/text","ace/range","ace/document","ace/background_tokenizer","ace/search_highlight","ace/edit_session/folding","ace/edit_session/bracket_match"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/lang"),s=e("./config"),o=e("./lib/event_emitter").EventEmitter,u=e("./selection").Selection,a=e("./mode/text").Mode,f=e("./range").Range,l=e("./document").Document,c=e("./background_tokenizer").BackgroundTokenizer,h=e("./search_highlight").SearchHighlight,p=function(e,t){this.$breakpoints=[],this.$decorations=[],this.$frontMarkers={},this.$backMarkers={},this.$markerId=1,this.$undoSelect=!0,this.$foldData=[],this.$foldData.toString=function(){return this.join("\n")},this.on("changeFold",this.onChangeFold.bind(this)),this.$onChange=this.onChange.bind(this);if(typeof e!="object"||!e.getLine)e=new l(e);this.setDocument(e),this.selection=new u(this),s.resetOptions(this),this.setMode(t),s._signal("session",this)};(function(){function m(e){return e<4352?!1:e>=4352&&e<=4447||e>=4515&&e<=4519||e>=4602&&e<=4607||e>=9001&&e<=9002||e>=11904&&e<=11929||e>=11931&&e<=12019||e>=12032&&e<=12245||e>=12272&&e<=12283||e>=12288&&e<=12350||e>=12353&&e<=12438||e>=12441&&e<=12543||e>=12549&&e<=12589||e>=12593&&e<=12686||e>=12688&&e<=12730||e>=12736&&e<=12771||e>=12784&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=13054||e>=13056&&e<=19903||e>=19968&&e<=42124||e>=42128&&e<=42182||e>=43360&&e<=43388||e>=44032&&e<=55203||e>=55216&&e<=55238||e>=55243&&e<=55291||e>=63744&&e<=64255||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=65281&&e<=65376||e>=65504&&e<=65510}r.implement(this,o),this.setDocument=function(e){this.doc&&this.doc.removeListener("change",this.$onChange),this.doc=e,e.on("change",this.$onChange),this.bgTokenizer&&this.bgTokenizer.setDocument(this.getDocument()),this.resetCaches()},this.getDocument=function(){return this.doc},this.$resetRowCache=function(e){if(!e){this.$docRowCache=[],this.$screenRowCache=[];return}var t=this.$docRowCache.length,n=this.$getRowCacheIndex(this.$docRowCache,e)+1;t>n&&(this.$docRowCache.splice(n,t),this.$screenRowCache.splice(n,t))},this.$getRowCacheIndex=function(e,t){var n=0,r=e.length-1;while(n<=r){var i=n+r>>1,s=e[i];if(t>s)n=i+1;else{if(!(t<s))return i;r=i-1}}return n-1},this.resetCaches=function(){this.$modified=!0,this.$wrapData=[],this.$rowLengthCache=[],this.$resetRowCache(0),this.bgTokenizer&&this.bgTokenizer.start(0)},this.onChangeFold=function(e){var t=e.data;this.$resetRowCache(t.start.row)},this.onChange=function(e){this.$modified=!0,this.$resetRowCache(e.start.row);var t=this.$updateInternalDataOnChange(e);!this.$fromUndo&&this.$undoManager&&!e.ignore&&(this.$deltasDoc.push(e),t&&t.length!=0&&this.$deltasFold.push({action:"removeFolds",folds:t}),this.$informUndoManager.schedule()),this.bgTokenizer&&this.bgTokenizer.$updateOnChange(e),this._signal("change",e)},this.setValue=function(e){this.doc.setValue(e),this.selection.moveTo(0,0),this.$resetRowCache(0),this.$deltas=[],this.$deltasDoc=[],this.$deltasFold=[],this.setUndoManager(this.$undoManager),this.getUndoManager().reset()},this.getValue=this.toString=function(){return this.doc.getValue()},this.getSelection=function(){return this.selection},this.getState=function(e){return this.bgTokenizer.getState(e)},this.getTokens=function(e){return this.bgTokenizer.getTokens(e)},this.getTokenAt=function(e,t){var n=this.bgTokenizer.getTokens(e),r,i=0;if(t==null)s=n.length-1,i=this.getLine(e).length;else for(var s=0;s<n.length;s++){i+=n[s].value.length;if(i>=t)break}return r=n[s],r?(r.index=s,r.start=i-r.value.length,r):null},this.setUndoManager=function(e){this.$undoManager=e,this.$deltas=[],this.$deltasDoc=[],this.$deltasFold=[],this.$informUndoManager&&this.$informUndoManager.cancel();if(e){var t=this;this.$syncInformUndoManager=function(){t.$informUndoManager.cancel(),t.$deltasFold.length&&(t.$deltas.push({group:"fold",deltas:t.$deltasFold}),t.$deltasFold=[]),t.$deltasDoc.length&&(t.$deltas.push({group:"doc",deltas:t.$deltasDoc}),t.$deltasDoc=[]),t.$deltas.length>0&&e.execute({action:"aceupdate",args:[t.$deltas,t],merge:t.mergeUndoDeltas}),t.mergeUndoDeltas=!1,t.$deltas=[]},this.$informUndoManager=i.delayedCall(this.$syncInformUndoManager)}},this.markUndoGroup=function(){this.$syncInformUndoManager&&this.$syncInformUndoManager()},this.$defaultUndoManager={undo:function(){},redo:function(){},reset:function(){}},this.getUndoManager=function(){return this.$undoManager||this.$defaultUndoManager},this.getTabString=function(){return this.getUseSoftTabs()?i.stringRepeat(" ",this.getTabSize()):" "},this.setUseSoftTabs=function(e){this.setOption("useSoftTabs",e)},this.getUseSoftTabs=function(){return this.$useSoftTabs&&!this.$mode.$indentWithTabs},this.setTabSize=function(e){this.setOption("tabSize",e)},this.getTabSize=function(){return this.$tabSize},this.isTabStop=function(e){return this.$useSoftTabs&&e.column%this.$tabSize===0},this.$overwrite=!1,this.setOverwrite=function(e){this.setOption("overwrite",e)},this.getOverwrite=function(){return this.$overwrite},this.toggleOverwrite=function(){this.setOverwrite(!this.$overwrite)},this.addGutterDecoration=function(e,t){this.$decorations[e]||(this.$decorations[e]=""),this.$decorations[e]+=" "+t,this._signal("changeBreakpoint",{})},this.removeGutterDecoration=function(e,t){this.$decorations[e]=(this.$decorations[e]||"").replace(" "+t,""),this._signal("changeBreakpoint",{})},this.getBreakpoints=function(){return this.$breakpoints},this.setBreakpoints=function(e){this.$breakpoints=[];for(var t=0;t<e.length;t++)this.$breakpoints[e[t]]="ace_breakpoint";this._signal("changeBreakpoint",{})},this.clearBreakpoints=function(){this.$breakpoints=[],this._signal("changeBreakpoint",{})},this.setBreakpoint=function(e,t){t===undefined&&(t="ace_breakpoint"),t?this.$breakpoints[e]=t:delete this.$breakpoints[e],this._signal("changeBreakpoint",{})},this.clearBreakpoint=function(e){delete this.$breakpoints[e],this._signal("changeBreakpoint",{})},this.addMarker=function(e,t,n,r){var i=this.$markerId++,s={range:e,type:n||"line",renderer:typeof n=="function"?n:null,clazz:t,inFront:!!r,id:i};return r?(this.$frontMarkers[i]=s,this._signal("changeFrontMarker")):(this.$backMarkers[i]=s,this._signal("changeBackMarker")),i},this.addDynamicMarker=function(e,t){if(!e.update)return;var n=this.$markerId++;return e.id=n,e.inFront=!!t,t?(this.$frontMarkers[n]=e,this._signal("changeFrontMarker")):(this.$backMarkers[n]=e,this._signal("changeBackMarker")),e},this.removeMarker=function(e){var t=this.$frontMarkers[e]||this.$backMarkers[e];if(!t)return;var n=t.inFront?this.$frontMarkers:this.$backMarkers;t&&(delete n[e],this._signal(t.inFront?"changeFrontMarker":"changeBackMarker"))},this.getMarkers=function(e){return e?this.$frontMarkers:this.$backMarkers},this.highlight=function(e){if(!this.$searchHighlight){var t=new h(null,"ace_selected-word","text");this.$searchHighlight=this.addDynamicMarker(t)}this.$searchHighlight.setRegexp(e)},this.highlightLines=function(e,t,n,r){typeof t!="number"&&(n=t,t=e),n||(n="ace_step");var i=new f(e,0,t,Infinity);return i.id=this.addMarker(i,n,"fullLine",r),i},this.setAnnotations=function(e){this.$annotations=e,this._signal("changeAnnotation",{})},this.getAnnotations=function(){return this.$annotations||[]},this.clearAnnotations=function(){this.setAnnotations([])},this.$detectNewLine=function(e){var t=e.match(/^.*?(\r?\n)/m);t?this.$autoNewLine=t[1]:this.$autoNewLine="\n"},this.getWordRange=function(e,t){var n=this.getLine(e),r=!1;t>0&&(r=!!n.charAt(t-1).match(this.tokenRe)),r||(r=!!n.charAt(t).match(this.tokenRe));if(r)var i=this.tokenRe;else if(/^\s+$/.test(n.slice(t-1,t+1)))var i=/\s/;else var i=this.nonTokenRe;var s=t;if(s>0){do s--;while(s>=0&&n.charAt(s).match(i));s++}var o=t;while(o<n.length&&n.charAt(o).match(i))o++;return new f(e,s,e,o)},this.getAWordRange=function(e,t){var n=this.getWordRange(e,t),r=this.getLine(n.end.row);while(r.charAt(n.end.column).match(/[ \t]/))n.end.column+=1;return n},this.setNewLineMode=function(e){this.doc.setNewLineMode(e)},this.getNewLineMode=function(){return this.doc.getNewLineMode()},this.setUseWorker=function(e){this.setOption("useWorker",e)},this.getUseWorker=function(){return this.$useWorker},this.onReloadTokenizer=function(e){var t=e.data;this.bgTokenizer.start(t.first),this._signal("tokenizerUpdate",e)},this.$modes={},this.$mode=null,this.$modeId=null,this.setMode=function(e,t){if(e&&typeof e=="object"){if(e.getTokenizer)return this.$onChangeMode(e);var n=e,r=n.path}else r=e||"ace/mode/text";this.$modes["ace/mode/text"]||(this.$modes["ace/mode/text"]=new a);if(this.$modes[r]&&!n){this.$onChangeMode(this.$modes[r]),t&&t();return}this.$modeId=r,s.loadModule(["mode",r],function(e){if(this.$modeId!==r)return t&&t();this.$modes[r]&&!n?this.$onChangeMode(this.$modes[r]):e&&e.Mode&&(e=new e.Mode(n),n||(this.$modes[r]=e,e.$id=r),this.$onChangeMode(e)),t&&t()}.bind(this)),this.$mode||this.$onChangeMode(this.$modes["ace/mode/text"],!0)},this.$onChangeMode=function(e,t){t||(this.$modeId=e.$id);if(this.$mode===e)return;this.$mode=e,this.$stopWorker(),this.$useWorker&&this.$startWorker();var n=e.getTokenizer();if(n.addEventListener!==undefined){var r=this.onReloadTokenizer.bind(this);n.addEventListener("update",r)}if(!this.bgTokenizer){this.bgTokenizer=new c(n);var i=this;this.bgTokenizer.addEventListener("update",function(e){i._signal("tokenizerUpdate",e)})}else this.bgTokenizer.setTokenizer(n);this.bgTokenizer.setDocument(this.getDocument()),this.tokenRe=e.tokenRe,this.nonTokenRe=e.nonTokenRe,t||(e.attachToSession&&e.attachToSession(this),this.$options.wrapMethod.set.call(this,this.$wrapMethod),this.$setFolding(e.foldingRules),this.bgTokenizer.start(0),this._emit("changeMode"))},this.$stopWorker=function(){this.$worker&&(this.$worker.terminate(),this.$worker=null)},this.$startWorker=function(){try{this.$worker=this.$mode.createWorker(this)}catch(e){s.warn("Could not load worker",e),this.$worker=null}},this.getMode=function(){return this.$mode},this.$scrollTop=0,this.setScrollTop=function(e){if(this.$scrollTop===e||isNaN(e))return;this.$scrollTop=e,this._signal("changeScrollTop",e)},this.getScrollTop=function(){return this.$scrollTop},this.$scrollLeft=0,this.setScrollLeft=function(e){if(this.$scrollLeft===e||isNaN(e))return;this.$scrollLeft=e,this._signal("changeScrollLeft",e)},this.getScrollLeft=function(){return this.$scrollLeft},this.getScreenWidth=function(){return this.$computeWidth(),this.lineWidgets?Math.max(this.getLineWidgetMaxWidth(),this.screenWidth):this.screenWidth},this.getLineWidgetMaxWidth=function(){if(this.lineWidgetsWidth!=null)return this.lineWidgetsWidth;var e=0;return this.lineWidgets.forEach(function(t){t&&t.screenWidth>e&&(e=t.screenWidth)}),this.lineWidgetWidth=e},this.$computeWidth=function(e){if(this.$modified||e){this.$modified=!1;if(this.$useWrapMode)return this.screenWidth=this.$wrapLimit;var t=this.doc.getAllLines(),n=this.$rowLengthCache,r=0,i=0,s=this.$foldData[i],o=s?s.start.row:Infinity,u=t.length;for(var a=0;a<u;a++){if(a>o){a=s.end.row+1;if(a>=u)break;s=this.$foldData[i++],o=s?s.start.row:Infinity}n[a]==null&&(n[a]=this.$getStringScreenWidth(t[a])[0]),n[a]>r&&(r=n[a])}this.screenWidth=r}},this.getLine=function(e){return this.doc.getLine(e)},this.getLines=function(e,t){return this.doc.getLines(e,t)},this.getLength=function(){return this.doc.getLength()},this.getTextRange=function(e){return this.doc.getTextRange(e||this.selection.getRange())},this.insert=function(e,t){return this.doc.insert(e,t)},this.remove=function(e){return this.doc.remove(e)},this.removeFullLines=function(e,t){return this.doc.removeFullLines(e,t)},this.undoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;var n=null;for(var r=e.length-1;r!=-1;r--){var i=e[r];i.group=="doc"?(this.doc.revertDeltas(i.deltas),n=this.$getUndoSelection(i.deltas,!0,n)):i.deltas.forEach(function(e){this.addFolds(e.folds)},this)}return this.$fromUndo=!1,n&&this.$undoSelect&&!t&&this.selection.setSelectionRange(n),n},this.redoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;var n=null;for(var r=0;r<e.length;r++){var i=e[r];i.group=="doc"&&(this.doc.applyDeltas(i.deltas),n=this.$getUndoSelection(i.deltas,!1,n))}return this.$fromUndo=!1,n&&this.$undoSelect&&!t&&this.selection.setSelectionRange(n),n},this.setUndoSelect=function(e){this.$undoSelect=e},this.$getUndoSelection=function(e,t,n){function r(e){return t?e.action!=="insert":e.action==="insert"}var i=e[0],s,o,u=!1;r(i)?(s=f.fromPoints(i.start,i.end),u=!0):(s=f.fromPoints(i.start,i.start),u=!1);for(var a=1;a<e.length;a++)i=e[a],r(i)?(o=i.start,s.compare(o.row,o.column)==-1&&s.setStart(o),o=i.end,s.compare(o.row,o.column)==1&&s.setEnd(o),u=!0):(o=i.start,s.compare(o.row,o.column)==-1&&(s=f.fromPoints(i.start,i.start)),u=!1);if(n!=null){f.comparePoints(n.start,s.start)===0&&(n.start.column+=s.end.column-s.start.column,n.end.column+=s.end.column-s.start.column);var l=n.compareRange(s);l==1?s.setStart(n.start):l==-1&&s.setEnd(n.end)}return s},this.replace=function(e,t){return this.doc.replace(e,t)},this.moveText=function(e,t,n){var r=this.getTextRange(e),i=this.getFoldsInRange(e),s=f.fromPoints(t,t);if(!n){this.remove(e);var o=e.start.row-e.end.row,u=o?-e.end.column:e.start.column-e.end.column;u&&(s.start.row==e.end.row&&s.start.column>e.end.column&&(s.start.column+=u),s.end.row==e.end.row&&s.end.column>e.end.column&&(s.end.column+=u)),o&&s.start.row>=e.end.row&&(s.start.row+=o,s.end.row+=o)}s.end=this.insert(s.start,r);if(i.length){var a=e.start,l=s.start,o=l.row-a.row,u=l.column-a.column;this.addFolds(i.map(function(e){return e=e.clone(),e.start.row==a.row&&(e.start.column+=u),e.end.row==a.row&&(e.end.column+=u),e.start.row+=o,e.end.row+=o,e}))}return s},this.indentRows=function(e,t,n){n=n.replace(/\t/g,this.getTabString());for(var r=e;r<=t;r++)this.doc.insertInLine({row:r,column:0},n)},this.outdentRows=function(e){var t=e.collapseRows(),n=new f(0,0,0,0),r=this.getTabSize();for(var i=t.start.row;i<=t.end.row;++i){var s=this.getLine(i);n.start.row=i,n.end.row=i;for(var o=0;o<r;++o)if(s.charAt(o)!=" ")break;o<r&&s.charAt(o)==" "?(n.start.column=o,n.end.column=o+1):(n.start.column=0,n.end.column=o),this.remove(n)}},this.$moveLines=function(e,t,n){e=this.getRowFoldStart(e),t=this.getRowFoldEnd(t);if(n<0){var r=this.getRowFoldStart(e+n);if(r<0)return 0;var i=r-e}else if(n>0){var r=this.getRowFoldEnd(t+n);if(r>this.doc.getLength()-1)return 0;var i=r-t}else{e=this.$clipRowToDocument(e),t=this.$clipRowToDocument(t);var i=t-e+1}var s=new f(e,0,t,Number.MAX_VALUE),o=this.getFoldsInRange(s).map(function(e){return e=e.clone(),e.start.row+=i,e.end.row+=i,e}),u=n==0?this.doc.getLines(e,t):this.doc.removeFullLines(e,t);return this.doc.insertFullLines(e+i,u),o.length&&this.addFolds(o),i},this.moveLinesUp=function(e,t){return this.$moveLines(e,t,-1)},this.moveLinesDown=function(e,t){return this.$moveLines(e,t,1)},this.duplicateLines=function(e,t){return this.$moveLines(e,t,0)},this.$clipRowToDocument=function(e){return Math.max(0,Math.min(e,this.doc.getLength()-1))},this.$clipColumnToRow=function(e,t){return t<0?0:Math.min(this.doc.getLine(e).length,t)},this.$clipPositionToDocument=function(e,t){t=Math.max(0,t);if(e<0)e=0,t=0;else{var n=this.doc.getLength();e>=n?(e=n-1,t=this.doc.getLine(n-1).length):t=Math.min(this.doc.getLine(e).length,t)}return{row:e,column:t}},this.$clipRangeToDocument=function(e){e.start.row<0?(e.start.row=0,e.start.column=0):e.start.column=this.$clipColumnToRow(e.start.row,e.start.column);var t=this.doc.getLength()-1;return e.end.row>t?(e.end.row=t,e.end.column=this.doc.getLine(t).length):e.end.column=this.$clipColumnToRow(e.end.row,e.end.column),e},this.$wrapLimit=80,this.$useWrapMode=!1,this.$wrapLimitRange={min:null,max:null},this.setUseWrapMode=function(e){if(e!=this.$useWrapMode){this.$useWrapMode=e,this.$modified=!0,this.$resetRowCache(0);if(e){var t=this.getLength();this.$wrapData=Array(t),this.$updateWrapData(0,t-1)}this._signal("changeWrapMode")}},this.getUseWrapMode=function(){return this.$useWrapMode},this.setWrapLimitRange=function(e,t){if(this.$wrapLimitRange.min!==e||this.$wrapLimitRange.max!==t)this.$wrapLimitRange={min:e,max:t},this.$modified=!0,this.$useWrapMode&&this._signal("changeWrapMode")},this.adjustWrapLimit=function(e,t){var n=this.$wrapLimitRange;n.max<0&&(n={min:t,max:t});var r=this.$constrainWrapLimit(e,n.min,n.max);return r!=this.$wrapLimit&&r>1?(this.$wrapLimit=r,this.$modified=!0,this.$useWrapMode&&(this.$updateWrapData(0,this.getLength()-1),this.$resetRowCache(0),this._signal("changeWrapLimit")),!0):!1},this.$constrainWrapLimit=function(e,t,n){return t&&(e=Math.max(t,e)),n&&(e=Math.min(n,e)),e},this.getWrapLimit=function(){return this.$wrapLimit},this.setWrapLimit=function(e){this.setWrapLimitRange(e,e)},this.getWrapLimitRange=function(){return{min:this.$wrapLimitRange.min,max:this.$wrapLimitRange.max}},this.$updateInternalDataOnChange=function(e){var t=this.$useWrapMode,n=e.action,r=e.start,i=e.end,s=r.row,o=i.row,u=o-s,a=null;this.$updating=!0;if(u!=0)if(n==="remove"){this[t?"$wrapData":"$rowLengthCache"].splice(s,u);var f=this.$foldData;a=this.getFoldsInRange(e),this.removeFolds(a);var l=this.getFoldLine(i.row),c=0;if(l){l.addRemoveChars(i.row,i.column,r.column-i.column),l.shiftRow(-u);var h=this.getFoldLine(s);h&&h!==l&&(h.merge(l),l=h),c=f.indexOf(l)+1}for(c;c<f.length;c++){var l=f[c];l.start.row>=i.row&&l.shiftRow(-u)}o=s}else{var p=Array(u);p.unshift(s,0);var d=t?this.$wrapData:this.$rowLengthCache;d.splice.apply(d,p);var f=this.$foldData,l=this.getFoldLine(s),c=0;if(l){var v=l.range.compareInside(r.row,r.column);v==0?(l=l.split(r.row,r.column),l&&(l.shiftRow(u),l.addRemoveChars(o,0,i.column-r.column))):v==-1&&(l.addRemoveChars(s,0,i.column-r.column),l.shiftRow(u)),c=f.indexOf(l)+1}for(c;c<f.length;c++){var l=f[c];l.start.row>=s&&l.shiftRow(u)}}else{u=Math.abs(e.start.column-e.end.column),n==="remove"&&(a=this.getFoldsInRange(e),this.removeFolds(a),u=-u);var l=this.getFoldLine(s);l&&l.addRemoveChars(s,r.column,u)}return t&&this.$wrapData.length!=this.doc.getLength()&&console.error("doc.getLength() and $wrapData.length have to be the same!"),this.$updating=!1,t?this.$updateWrapData(s,o):this.$updateRowLengthCache(s,o),a},this.$updateRowLengthCache=function(e,t,n){this.$rowLengthCache[e]=null,this.$rowLengthCache[t]=null},this.$updateWrapData=function(e,t){var r=this.doc.getAllLines(),i=this.getTabSize(),s=this.$wrapData,o=this.$wrapLimit,a,f,l=e;t=Math.min(t,r.length-1);while(l<=t)f=this.getFoldLine(l,f),f?(a=[],f.walk(function(e,t,i,s){var o;if(e!=null){o=this.$getDisplayTokens(e,a.length),o[0]=n;for(var f=1;f<o.length;f++)o[f]=u}else o=this.$getDisplayTokens(r[t].substring(s,i),a.length);a=a.concat(o)}.bind(this),f.end.row,r[f.end.row].length+1),s[f.start.row]=this.$computeWrapSplits(a,o,i),l=f.end.row+1):(a=this.$getDisplayTokens(r[l]),s[l]=this.$computeWrapSplits(a,o,i),l++)};var e=1,t=2,n=3,u=4,l=9,p=10,d=11,v=12;this.$computeWrapSplits=function(e,r,i){function g(){var t=0;if(m===0)return t;if(h)for(var n=0;n<e.length;n++){var r=e[n];if(r==p)t+=1;else{if(r!=d){if(r==v)continue;break}t+=i}}return c&&h!==!1&&(t+=i),Math.min(t,m)}function y(t){var n=e.slice(a,t),r=n.length;n.join("").replace(/12/g,function(){r-=1}).replace(/2/g,function(){r-=1}),s.length||(b=g(),s.indent=b),f+=r,s.push(f),a=t}if(e.length==0)return[];var s=[],o=e.length,a=0,f=0,c=this.$wrapAsCode,h=this.$indentedSoftWrap,m=r<=Math.max(2*i,8)||h===!1?0:Math.floor(r/2),b=0;while(o-a>r-b){var w=a+r-b;if(e[w-1]>=p&&e[w]>=p){y(w);continue}if(e[w]==n||e[w]==u){for(w;w!=a-1;w--)if(e[w]==n)break;if(w>a){y(w);continue}w=a+r;for(w;w<e.length;w++)if(e[w]!=u)break;if(w==e.length)break;y(w);continue}var E=Math.max(w-(r-(r>>2)),a-1);while(w>E&&e[w]<n)w--;if(c){while(w>E&&e[w]<n)w--;while(w>E&&e[w]==l)w--}else while(w>E&&e[w]<p)w--;if(w>E){y(++w);continue}w=a+r,e[w]==t&&w--,y(w-b)}return s},this.$getDisplayTokens=function(n,r){var i=[],s;r=r||0;for(var o=0;o<n.length;o++){var u=n.charCodeAt(o);if(u==9){s=this.getScreenTabSize(i.length+r),i.push(d);for(var a=1;a<s;a++)i.push(v)}else u==32?i.push(p):u>39&&u<48||u>57&&u<64?i.push(l):u>=4352&&m(u)?i.push(e,t):i.push(e)}return i},this.$getStringScreenWidth=function(e,t,n){if(t==0)return[0,0];t==null&&(t=Infinity),n=n||0;var r,i;for(i=0;i<e.length;i++){r=e.charCodeAt(i),r==9?n+=this.getScreenTabSize(n):r>=4352&&m(r)?n+=2:n+=1;if(n>t)break}return[n,i]},this.lineWidgets=null,this.getRowLength=function(e){if(this.lineWidgets)var t=this.lineWidgets[e]&&this.lineWidgets[e].rowCount||0;else t=0;return!this.$useWrapMode||!this.$wrapData[e]?1+t:this.$wrapData[e].length+1+t},this.getRowLineCount=function(e){return!this.$useWrapMode||!this.$wrapData[e]?1:this.$wrapData[e].length+1},this.getRowWrapIndent=function(e){if(this.$useWrapMode){var t=this.screenToDocumentPosition(e,Number.MAX_VALUE),n=this.$wrapData[t.row];return n.length&&n[0]<t.column?n.indent:0}return 0},this.getScreenLastRowColumn=function(e){var t=this.screenToDocumentPosition(e,Number.MAX_VALUE);return this.documentToScreenColumn(t.row,t.column)},this.getDocumentLastRowColumn=function(e,t){var n=this.documentToScreenRow(e,t);return this.getScreenLastRowColumn(n)},this.getDocumentLastRowColumnPosition=function(e,t){var n=this.documentToScreenRow(e,t);return this.screenToDocumentPosition(n,Number.MAX_VALUE/10)},this.getRowSplitData=function(e){return this.$useWrapMode?this.$wrapData[e]:undefined},this.getScreenTabSize=function(e){return this.$tabSize-e%this.$tabSize},this.screenToDocumentRow=function(e,t){return this.screenToDocumentPosition(e,t).row},this.screenToDocumentColumn=function(e,t){return this.screenToDocumentPosition(e,t).column},this.screenToDocumentPosition=function(e,t){if(e<0)return{row:0,column:0};var n,r=0,i=0,s,o=0,u=0,a=this.$screenRowCache,f=this.$getRowCacheIndex(a,e),l=a.length;if(l&&f>=0)var o=a[f],r=this.$docRowCache[f],c=e>a[l-1];else var c=!l;var h=this.getLength()-1,p=this.getNextFoldLine(r),d=p?p.start.row:Infinity;while(o<=e){u=this.getRowLength(r);if(o+u>e||r>=h)break;o+=u,r++,r>d&&(r=p.end.row+1,p=this.getNextFoldLine(r,p),d=p?p.start.row:Infinity),c&&(this.$docRowCache.push(r),this.$screenRowCache.push(o))}if(p&&p.start.row<=r)n=this.getFoldDisplayLine(p),r=p.start.row;else{if(o+u<=e||r>h)return{row:h,column:this.getLine(h).length};n=this.getLine(r),p=null}var v=0;if(this.$useWrapMode){var m=this.$wrapData[r];if(m){var g=Math.floor(e-o);s=m[g],g>0&&m.length&&(v=m.indent,i=m[g-1]||m[m.length-1],n=n.substring(i))}}return i+=this.$getStringScreenWidth(n,t-v)[1],this.$useWrapMode&&i>=s&&(i=s-1),p?p.idxToPosition(i):{row:r,column:i}},this.documentToScreenPosition=function(e,t){if(typeof t=="undefined")var n=this.$clipPositionToDocument(e.row,e.column);else n=this.$clipPositionToDocument(e,t);e=n.row,t=n.column;var r=0,i=null,s=null;s=this.getFoldAt(e,t,1),s&&(e=s.start.row,t=s.start.column);var o,u=0,a=this.$docRowCache,f=this.$getRowCacheIndex(a,e),l=a.length;if(l&&f>=0)var u=a[f],r=this.$screenRowCache[f],c=e>a[l-1];else var c=!l;var h=this.getNextFoldLine(u),p=h?h.start.row:Infinity;while(u<e){if(u>=p){o=h.end.row+1;if(o>e)break;h=this.getNextFoldLine(o,h),p=h?h.start.row:Infinity}else o=u+1;r+=this.getRowLength(u),u=o,c&&(this.$docRowCache.push(u),this.$screenRowCache.push(r))}var d="";h&&u>=p?(d=this.getFoldDisplayLine(h,e,t),i=h.start.row):(d=this.getLine(e).substring(0,t),i=e);var v=0;if(this.$useWrapMode){var m=this.$wrapData[i];if(m){var g=0;while(d.length>=m[g])r++,g++;d=d.substring(m[g-1]||0,d.length),v=g>0?m.indent:0}}return{row:r,column:v+this.$getStringScreenWidth(d)[0]}},this.documentToScreenColumn=function(e,t){return this.documentToScreenPosition(e,t).column},this.documentToScreenRow=function(e,t){return this.documentToScreenPosition(e,t).row},this.getScreenLength=function(){var e=0,t=null;if(!this.$useWrapMode){e=this.getLength();var n=this.$foldData;for(var r=0;r<n.length;r++)t=n[r],e-=t.end.row-t.start.row}else{var i=this.$wrapData.length,s=0,r=0,t=this.$foldData[r++],o=t?t.start.row:Infinity;while(s<i){var u=this.$wrapData[s];e+=u?u.length+1:1,s++,s>o&&(s=t.end.row+1,t=this.$foldData[r++],o=t?t.start.row:Infinity)}}return this.lineWidgets&&(e+=this.$getWidgetScreenLength()),e},this.$setFontMetrics=function(e){if(!this.$enableVarChar)return;this.$getStringScreenWidth=function(t,n,r){if(n===0)return[0,0];n||(n=Infinity),r=r||0;var i,s;for(s=0;s<t.length;s++){i=t.charAt(s),i===" "?r+=this.getScreenTabSize(r):r+=e.getCharacterWidth(i);if(r>n)break}return[r,s]}},this.destroy=function(){this.bgTokenizer&&(this.bgTokenizer.setDocument(null),this.bgTokenizer=null),this.$stopWorker()}}).call(p.prototype),e("./edit_session/folding").Folding.call(p.prototype),e("./edit_session/bracket_match").BracketMatch.call(p.prototype),s.defineOptions(p.prototype,"session",{wrap:{set:function(e){!e||e=="off"?e=!1:e=="free"?e=!0:e=="printMargin"?e=-1:typeof e=="string"&&(e=parseInt(e,10)||!1);if(this.$wrap==e)return;this.$wrap=e;if(!e)this.setUseWrapMode(!1);else{var t=typeof e=="number"?e:null;this.setWrapLimitRange(t,t),this.setUseWrapMode(!0)}},get:function(){return this.getUseWrapMode()?this.$wrap==-1?"printMargin":this.getWrapLimitRange().min?this.$wrap:"free":"off"},handlesSet:!0},wrapMethod:{set:function(e){e=e=="auto"?this.$mode.type!="text":e!="text",e!=this.$wrapAsCode&&(this.$wrapAsCode=e,this.$useWrapMode&&(this.$modified=!0,this.$resetRowCache(0),this.$updateWrapData(0,this.getLength()-1)))},initialValue:"auto"},indentedSoftWrap:{initialValue:!0},firstLineNumber:{set:function(){this._signal("changeBreakpoint")},initialValue:1},useWorker:{set:function(e){this.$useWorker=e,this.$stopWorker(),e&&this.$startWorker()},initialValue:!0},useSoftTabs:{initialValue:!0},tabSize:{set:function(e){if(isNaN(e)||this.$tabSize===e)return;this.$modified=!0,this.$rowLengthCache=[],this.$tabSize=e,this._signal("changeTabSize")},initialValue:4,handlesSet:!0},overwrite:{set:function(e){this._signal("changeOverwrite")},initialValue:!1},newLineMode:{set:function(e){this.doc.setNewLineMode(e)},get:function(){return this.doc.getNewLineMode()},handlesSet:!0},mode:{set:function(e){this.setMode(e)},get:function(){return this.$modeId}}}),t.EditSession=p}),define("ace/search",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(){this.$options={}};(function(){this.set=function(e){return i.mixin(this.$options,e),this},this.getOptions=function(){return r.copyObject(this.$options)},this.setOptions=function(e){this.$options=e},this.find=function(e){var t=this.$options,n=this.$matchIterator(e,t);if(!n)return!1;var r=null;return n.forEach(function(e,n,i){if(!e.start){var o=e.offset+(i||0);r=new s(n,o,n,o+e.length);if(!e.length&&t.start&&t.start.start&&t.skipCurrent!=0&&r.isEqual(t.start))return r=null,!1}else r=e;return!0}),r},this.findAll=function(e){var t=this.$options;if(!t.needle)return[];this.$assembleRegExp(t);var n=t.range,i=n?e.getLines(n.start.row,n.end.row):e.doc.getAllLines(),o=[],u=t.re;if(t.$isMultiLine){var a=u.length,f=i.length-a,l;e:for(var c=u.offset||0;c<=f;c++){for(var h=0;h<a;h++)if(i[c+h].search(u[h])==-1)continue e;var p=i[c],d=i[c+a-1],v=p.length-p.match(u[0])[0].length,m=d.match(u[a-1])[0].length;if(l&&l.end.row===c&&l.end.column>v)continue;o.push(l=new s(c,v,c+a-1,m)),a>2&&(c=c+a-2)}}else for(var g=0;g<i.length;g++){var y=r.getMatchOffsets(i[g],u);for(var h=0;h<y.length;h++){var b=y[h];o.push(new s(g,b.offset,g,b.offset+b.length))}}if(n){var w=n.start.column,E=n.start.column,g=0,h=o.length-1;while(g<h&&o[g].start.column<w&&o[g].start.row==n.start.row)g++;while(g<h&&o[h].end.column>E&&o[h].end.row==n.end.row)h--;o=o.slice(g,h+1);for(g=0,h=o.length;g<h;g++)o[g].start.row+=n.start.row,o[g].end.row+=n.start.row}return o},this.replace=function(e,t){var n=this.$options,r=this.$assembleRegExp(n);if(n.$isMultiLine)return t;if(!r)return;var i=r.exec(e);if(!i||i[0].length!=e.length)return null;t=e.replace(r,t);if(n.preserveCase){t=t.split("");for(var s=Math.min(e.length,e.length);s--;){var o=e[s];o&&o.toLowerCase()!=o?t[s]=t[s].toUpperCase():t[s]=t[s].toLowerCase()}t=t.join("")}return t},this.$matchIterator=function(e,t){var n=this.$assembleRegExp(t);if(!n)return!1;var i;if(t.$isMultiLine)var o=n.length,u=function(t,r,u){var a=t.search(n[0]);if(a==-1)return;for(var f=1;f<o;f++){t=e.getLine(r+f);if(t.search(n[f])==-1)return}var l=t.match(n[o-1])[0].length,c=new s(r,a,r+o-1,l);n.offset==1?(c.start.row--,c.start.column=Number.MAX_VALUE):u&&(c.start.column+=u);if(i(c))return!0};else if(t.backwards)var u=function(e,t,s){var o=r.getMatchOffsets(e,n);for(var u=o.length-1;u>=0;u--)if(i(o[u],t,s))return!0};else var u=function(e,t,s){var o=r.getMatchOffsets(e,n);for(var u=0;u<o.length;u++)if(i(o[u],t,s))return!0};var a=this.$lineIterator(e,t);return{forEach:function(e){i=e,a.forEach(u)}}},this.$assembleRegExp=function(e,t){if(e.needle instanceof RegExp)return e.re=e.needle;var n=e.needle;if(!e.needle)return e.re=!1;e.regExp||(n=r.escapeRegExp(n)),e.wholeWord&&(n="\\b"+n+"\\b");var i=e.caseSensitive?"gm":"gmi";e.$isMultiLine=!t&&/[\n\r]/.test(n);if(e.$isMultiLine)return e.re=this.$assembleMultilineRegExp(n,i);try{var s=new RegExp(n,i)}catch(o){s=!1}return e.re=s},this.$assembleMultilineRegExp=function(e,t){var n=e.replace(/\r\n|\r|\n/g,"$\n^").split("\n"),r=[];for(var i=0;i<n.length;i++)try{r.push(new RegExp(n[i],t))}catch(s){return!1}return n[0]==""?(r.shift(),r.offset=1):r.offset=0,r},this.$lineIterator=function(e,t){var n=t.backwards==1,r=t.skipCurrent!=0,i=t.range,s=t.start;s||(s=i?i[n?"end":"start"]:e.selection.getRange()),s.start&&(s=s[r!=n?"end":"start"]);var o=i?i.start.row:0,u=i?i.end.row:e.getLength()-1,a=n?function(n){var r=s.row,i=e.getLine(r).substring(0,s.column);if(n(i,r))return;for(r--;r>=o;r--)if(n(e.getLine(r),r))return;if(t.wrap==0)return;for(r=u,o=s.row;r>=o;r--)if(n(e.getLine(r),r))return}:function(n){var r=s.row,i=e.getLine(r).substr(s.column);if(n(i,r,s.column))return;for(r+=1;r<=u;r++)if(n(e.getLine(r),r))return;if(t.wrap==0)return;for(r=o,u=s.row;r<=u;r++)if(n(e.getLine(r),r))return};return{forEach:a}}}).call(o.prototype),t.Search=o}),define("ace/keyboard/hash_handler",["require","exports","module","ace/lib/keys","ace/lib/useragent"],function(e,t,n){"use strict";function o(e,t){this.platform=t||(i.isMac?"mac":"win"),this.commands={},this.commandKeyBinding={},this.addCommands(e),this.$singleCommand=!0}function u(e,t){o.call(this,e,t),this.$singleCommand=!1}var r=e("../lib/keys"),i=e("../lib/useragent"),s=r.KEY_MODS;u.prototype=o.prototype,function(){function e(e){return typeof e=="object"&&e.bindKey&&e.bindKey.position||0}this.addCommand=function(e){this.commands[e.name]&&this.removeCommand(e),this.commands[e.name]=e,e.bindKey&&this._buildKeyHash(e)},this.removeCommand=function(e,t){var n=e&&(typeof e=="string"?e:e.name);e=this.commands[n],t||delete this.commands[n];var r=this.commandKeyBinding;for(var i in r){var s=r[i];if(s==e)delete r[i];else if(Array.isArray(s)){var o=s.indexOf(e);o!=-1&&(s.splice(o,1),s.length==1&&(r[i]=s[0]))}}},this.bindKey=function(e,t,n){typeof e=="object"&&e&&(n==undefined&&(n=e.position),e=e[this.platform]);if(!e)return;if(typeof t=="function")return this.addCommand({exec:t,bindKey:e,name:t.name||e});e.split("|").forEach(function(e){var r="";if(e.indexOf(" ")!=-1){var i=e.split(/\s+/);e=i.pop(),i.forEach(function(e){var t=this.parseKeys(e),n=s[t.hashId]+t.key;r+=(r?" ":"")+n,this._addCommandToBinding(r,"chainKeys")},this),r+=" "}var o=this.parseKeys(e),u=s[o.hashId]+o.key;this._addCommandToBinding(r+u,t,n)},this)},this._addCommandToBinding=function(t,n,r){var i=this.commandKeyBinding,s;if(!n)delete i[t];else if(!i[t]||this.$singleCommand)i[t]=n;else{Array.isArray(i[t])?(s=i[t].indexOf(n))!=-1&&i[t].splice(s,1):i[t]=[i[t]],typeof r!="number"&&(r||n.isDefault?r=-100:r=e(n));var o=i[t];for(s=0;s<o.length;s++){var u=o[s],a=e(u);if(a>r)break}o.splice(s,0,n)}},this.addCommands=function(e){e&&Object.keys(e).forEach(function(t){var n=e[t];if(!n)return;if(typeof n=="string")return this.bindKey(n,t);typeof n=="function"&&(n={exec:n});if(typeof n!="object")return;n.name||(n.name=t),this.addCommand(n)},this)},this.removeCommands=function(e){Object.keys(e).forEach(function(t){this.removeCommand(e[t])},this)},this.bindKeys=function(e){Object.keys(e).forEach(function(t){this.bindKey(t,e[t])},this)},this._buildKeyHash=function(e){this.bindKey(e.bindKey,e)},this.parseKeys=function(e){var t=e.toLowerCase().split(/[\-\+]([\-\+])?/).filter(function(e){return e}),n=t.pop(),i=r[n];if(r.FUNCTION_KEYS[i])n=r.FUNCTION_KEYS[i].toLowerCase();else{if(!t.length)return{key:n,hashId:-1};if(t.length==1&&t[0]=="shift")return{key:n.toUpperCase(),hashId:-1}}var s=0;for(var o=t.length;o--;){var u=r.KEY_MODS[t[o]];if(u==null)return typeof console!="undefined"&&console.error("invalid modifier "+t[o]+" in "+e),!1;s|=u}return{key:n,hashId:s}},this.findKeyCommand=function(t,n){var r=s[t]+n;return this.commandKeyBinding[r]},this.handleKeyboard=function(e,t,n,r){if(r<0)return;var i=s[t]+n,o=this.commandKeyBinding[i];e.$keyChain&&(e.$keyChain+=" "+i,o=this.commandKeyBinding[e.$keyChain]||o);if(o)if(o=="chainKeys"||o[o.length-1]=="chainKeys")return e.$keyChain=e.$keyChain||i,{command:"null"};if(e.$keyChain)if(!!t&&t!=4||n.length!=1){if(t==-1||r>0)e.$keyChain=""}else e.$keyChain=e.$keyChain.slice(0,-i.length-1);return{command:o}},this.getStatusText=function(e,t){return t.$keyChain||""}}.call(o.prototype),t.HashHandler=o,t.MultiHashHandler=u}),define("ace/commands/command_manager",["require","exports","module","ace/lib/oop","ace/keyboard/hash_handler","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../keyboard/hash_handler").MultiHashHandler,s=e("../lib/event_emitter").EventEmitter,o=function(e,t){i.call(this,t,e),this.byName=this.commands,this.setDefaultHandler("exec",function(e){return e.command.exec(e.editor,e.args||{})})};r.inherits(o,i),function(){r.implement(this,s),this.exec=function(e,t,n){if(Array.isArray(e)){for(var r=e.length;r--;)if(this.exec(e[r],t,n))return!0;return!1}typeof e=="string"&&(e=this.commands[e]);if(!e)return!1;if(t&&t.$readOnly&&!e.readOnly)return!1;var i={editor:t,command:e,args:n};return i.returnValue=this._emit("exec",i),this._signal("afterExec",i),i.returnValue===!1?!1:!0},this.toggleRecording=function(e){if(this.$inReplay)return;return e&&e._emit("changeStatus"),this.recording?(this.macro.pop(),this.removeEventListener("exec",this.$addCommandToMacro),this.macro.length||(this.macro=this.oldMacro),this.recording=!1):(this.$addCommandToMacro||(this.$addCommandToMacro=function(e){this.macro.push([e.command,e.args])}.bind(this)),this.oldMacro=this.macro,this.macro=[],this.on("exec",this.$addCommandToMacro),this.recording=!0)},this.replay=function(e){if(this.$inReplay||!this.macro)return;if(this.recording)return this.toggleRecording(e);try{this.$inReplay=!0,this.macro.forEach(function(t){typeof t=="string"?this.exec(t,e):this.exec(t[0],e,t[1])},this)}finally{this.$inReplay=!1}},this.trimMacro=function(e){return e.map(function(e){return typeof e[0]!="string"&&(e[0]=e[0].name),e[1]||(e=e[0]),e})}}.call(o.prototype),t.CommandManager=o}),define("ace/commands/default_commands",["require","exports","module","ace/lib/lang","ace/config","ace/range"],function(e,t,n){"use strict";function o(e,t){return{win:e,mac:t}}var r=e("../lib/lang"),i=e("../config"),s=e("../range").Range;t.commands=[{name:"showSettingsMenu",bindKey:o("Ctrl-,","Command-,"),exec:function(e){i.loadModule("ace/ext/settings_menu",function(t){t.init(e),e.showSettingsMenu()})},readOnly:!0},{name:"goToNextError",bindKey:o("Alt-E","Ctrl-E"),exec:function(e){i.loadModule("ace/ext/error_marker",function(t){t.showErrorMarker(e,1)})},scrollIntoView:"animate",readOnly:!0},{name:"goToPreviousError",bindKey:o("Alt-Shift-E","Ctrl-Shift-E"),exec:function(e){i.loadModule("ace/ext/error_marker",function(t){t.showErrorMarker(e,-1)})},scrollIntoView:"animate",readOnly:!0},{name:"selectall",bindKey:o("Ctrl-A","Command-A"),exec:function(e){e.selectAll()},readOnly:!0},{name:"centerselection",bindKey:o(null,"Ctrl-L"),exec:function(e){e.centerSelection()},readOnly:!0},{name:"gotoline",bindKey:o("Ctrl-L","Command-L"),exec:function(e){var t=parseInt(prompt("Enter line number:"),10);isNaN(t)||e.gotoLine(t)},readOnly:!0},{name:"fold",bindKey:o("Alt-L|Ctrl-F1","Command-Alt-L|Command-F1"),exec:function(e){e.session.toggleFold(!1)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"unfold",bindKey:o("Alt-Shift-L|Ctrl-Shift-F1","Command-Alt-Shift-L|Command-Shift-F1"),exec:function(e){e.session.toggleFold(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleFoldWidget",bindKey:o("F2","F2"),exec:function(e){e.session.toggleFoldWidget()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleParentFoldWidget",bindKey:o("Alt-F2","Alt-F2"),exec:function(e){e.session.toggleFoldWidget(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"foldall",bindKey:o(null,"Ctrl-Command-Option-0"),exec:function(e){e.session.foldAll()},scrollIntoView:"center",readOnly:!0},{name:"foldOther",bindKey:o("Alt-0","Command-Option-0"),exec:function(e){e.session.foldAll(),e.session.unfold(e.selection.getAllRanges())},scrollIntoView:"center",readOnly:!0},{name:"unfoldall",bindKey:o("Alt-Shift-0","Command-Option-Shift-0"),exec:function(e){e.session.unfold()},scrollIntoView:"center",readOnly:!0},{name:"findnext",bindKey:o("Ctrl-K","Command-G"),exec:function(e){e.findNext()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"findprevious",bindKey:o("Ctrl-Shift-K","Command-Shift-G"),exec:function(e){e.findPrevious()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"selectOrFindNext",bindKey:o("Alt-K","Ctrl-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findNext()},readOnly:!0},{name:"selectOrFindPrevious",bindKey:o("Alt-Shift-K","Ctrl-Shift-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findPrevious()},readOnly:!0},{name:"find",bindKey:o("Ctrl-F","Command-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e)})},readOnly:!0},{name:"overwrite",bindKey:"Insert",exec:function(e){e.toggleOverwrite()},readOnly:!0},{name:"selecttostart",bindKey:o("Ctrl-Shift-Home","Command-Shift-Up"),exec:function(e){e.getSelection().selectFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotostart",bindKey:o("Ctrl-Home","Command-Home|Command-Up"),exec:function(e){e.navigateFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectup",bindKey:o("Shift-Up","Shift-Up"),exec:function(e){e.getSelection().selectUp()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golineup",bindKey:o("Up","Up|Ctrl-P"),exec:function(e,t){e.navigateUp(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttoend",bindKey:o("Ctrl-Shift-End","Command-Shift-Down"),exec:function(e){e.getSelection().selectFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotoend",bindKey:o("Ctrl-End","Command-End|Command-Down"),exec:function(e){e.navigateFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectdown",bindKey:o("Shift-Down","Shift-Down"),exec:function(e){e.getSelection().selectDown()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golinedown",bindKey:o("Down","Down|Ctrl-N"),exec:function(e,t){e.navigateDown(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordleft",bindKey:o("Ctrl-Shift-Left","Option-Shift-Left"),exec:function(e){e.getSelection().selectWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordleft",bindKey:o("Ctrl-Left","Option-Left"),exec:function(e){e.navigateWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolinestart",bindKey:o("Alt-Shift-Left","Command-Shift-Left"),exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolinestart",bindKey:o("Alt-Left|Home","Command-Left|Home|Ctrl-A"),exec:function(e){e.navigateLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectleft",bindKey:o("Shift-Left","Shift-Left"),exec:function(e){e.getSelection().selectLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoleft",bindKey:o("Left","Left|Ctrl-B"),exec:function(e,t){e.navigateLeft(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordright",bindKey:o("Ctrl-Shift-Right","Option-Shift-Right"),exec:function(e){e.getSelection().selectWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordright",bindKey:o("Ctrl-Right","Option-Right"),exec:function(e){e.navigateWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolineend",bindKey:o("Alt-Shift-Right","Command-Shift-Right"),exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolineend",bindKey:o("Alt-Right|End","Command-Right|End|Ctrl-E"),exec:function(e){e.navigateLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectright",bindKey:o("Shift-Right","Shift-Right"),exec:function(e){e.getSelection().selectRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoright",bindKey:o("Right","Right|Ctrl-F"),exec:function(e,t){e.navigateRight(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectpagedown",bindKey:"Shift-PageDown",exec:function(e){e.selectPageDown()},readOnly:!0},{name:"pagedown",bindKey:o(null,"Option-PageDown"),exec:function(e){e.scrollPageDown()},readOnly:!0},{name:"gotopagedown",bindKey:o("PageDown","PageDown|Ctrl-V"),exec:function(e){e.gotoPageDown()},readOnly:!0},{name:"selectpageup",bindKey:"Shift-PageUp",exec:function(e){e.selectPageUp()},readOnly:!0},{name:"pageup",bindKey:o(null,"Option-PageUp"),exec:function(e){e.scrollPageUp()},readOnly:!0},{name:"gotopageup",bindKey:"PageUp",exec:function(e){e.gotoPageUp()},readOnly:!0},{name:"scrollup",bindKey:o("Ctrl-Up",null),exec:function(e){e.renderer.scrollBy(0,-2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"scrolldown",bindKey:o("Ctrl-Down",null),exec:function(e){e.renderer.scrollBy(0,2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"selectlinestart",bindKey:"Shift-Home",exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectlineend",bindKey:"Shift-End",exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"togglerecording",bindKey:o("Ctrl-Alt-E","Command-Option-E"),exec:function(e){e.commands.toggleRecording(e)},readOnly:!0},{name:"replaymacro",bindKey:o("Ctrl-Shift-E","Command-Shift-E"),exec:function(e){e.commands.replay(e)},readOnly:!0},{name:"jumptomatching",bindKey:o("Ctrl-P","Ctrl-P"),exec:function(e){e.jumpToMatching()},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"selecttomatching",bindKey:o("Ctrl-Shift-P","Ctrl-Shift-P"),exec:function(e){e.jumpToMatching(!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"expandToMatching",bindKey:o("Ctrl-Shift-M","Ctrl-Shift-M"),exec:function(e){e.jumpToMatching(!0,!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"passKeysToBrowser",bindKey:o(null,null),exec:function(){},passEvent:!0,readOnly:!0},{name:"copy",exec:function(e){},readOnly:!0},{name:"cut",exec:function(e){var t=e.getSelectionRange();e._emit("cut",t),e.selection.isEmpty()||(e.session.remove(t),e.clearSelection())},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"paste",exec:function(e,t){e.$handlePaste(t)},scrollIntoView:"cursor"},{name:"removeline",bindKey:o("Ctrl-D","Command-D"),exec:function(e){e.removeLines()},scrollIntoView:"cursor",multiSelectAction:"forEachLine"},{name:"duplicateSelection",bindKey:o("Ctrl-Shift-D","Command-Shift-D"),exec:function(e){e.duplicateSelection()},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"sortlines",bindKey:o("Ctrl-Alt-S","Command-Alt-S"),exec:function(e){e.sortLines()},scrollIntoView:"selection",multiSelectAction:"forEachLine"},{name:"togglecomment",bindKey:o("Ctrl-/","Command-/"),exec:function(e){e.toggleCommentLines()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"toggleBlockComment",bindKey:o("Ctrl-Shift-/","Command-Shift-/"),exec:function(e){e.toggleBlockComment()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"modifyNumberUp",bindKey:o("Ctrl-Shift-Up","Alt-Shift-Up"),exec:function(e){e.modifyNumber(1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"modifyNumberDown",bindKey:o("Ctrl-Shift-Down","Alt-Shift-Down"),exec:function(e){e.modifyNumber(-1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"replace",bindKey:o("Ctrl-H","Command-Option-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e,!0)})}},{name:"undo",bindKey:o("Ctrl-Z","Command-Z"),exec:function(e){e.undo()}},{name:"redo",bindKey:o("Ctrl-Shift-Z|Ctrl-Y","Command-Shift-Z|Command-Y"),exec:function(e){e.redo()}},{name:"copylinesup",bindKey:o("Alt-Shift-Up","Command-Option-Up"),exec:function(e){e.copyLinesUp()},scrollIntoView:"cursor"},{name:"movelinesup",bindKey:o("Alt-Up","Option-Up"),exec:function(e){e.moveLinesUp()},scrollIntoView:"cursor"},{name:"copylinesdown",bindKey:o("Alt-Shift-Down","Command-Option-Down"),exec:function(e){e.copyLinesDown()},scrollIntoView:"cursor"},{name:"movelinesdown",bindKey:o("Alt-Down","Option-Down"),exec:function(e){e.moveLinesDown()},scrollIntoView:"cursor"},{name:"del",bindKey:o("Delete","Delete|Ctrl-D|Shift-Delete"),exec:function(e){e.remove("right")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"backspace",bindKey:o("Shift-Backspace|Backspace","Ctrl-Backspace|Shift-Backspace|Backspace|Ctrl-H"),exec:function(e){e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"cut_or_delete",bindKey:o("Shift-Delete",null),exec:function(e){if(!e.selection.isEmpty())return!1;e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolinestart",bindKey:o("Alt-Backspace","Command-Backspace"),exec:function(e){e.removeToLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolineend",bindKey:o("Alt-Delete","Ctrl-K"),exec:function(e){e.removeToLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordleft",bindKey:o("Ctrl-Backspace","Alt-Backspace|Ctrl-Alt-Backspace"),exec:function(e){e.removeWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordright",bindKey:o("Ctrl-Delete","Alt-Delete"),exec:function(e){e.removeWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"outdent",bindKey:o("Shift-Tab","Shift-Tab"),exec:function(e){e.blockOutdent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"indent",bindKey:o("Tab","Tab"),exec:function(e){e.indent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"blockoutdent",bindKey:o("Ctrl-[","Ctrl-["),exec:function(e){e.blockOutdent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"blockindent",bindKey:o("Ctrl-]","Ctrl-]"),exec:function(e){e.blockIndent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"insertstring",exec:function(e,t){e.insert(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"inserttext",exec:function(e,t){e.insert(r.stringRepeat(t.text||"",t.times||1))},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"splitline",bindKey:o(null,"Ctrl-O"),exec:function(e){e.splitLine()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"transposeletters",bindKey:o("Ctrl-T","Ctrl-T"),exec:function(e){e.transposeLetters()},multiSelectAction:function(e){e.transposeSelections(1)},scrollIntoView:"cursor"},{name:"touppercase",bindKey:o("Ctrl-U","Ctrl-U"),exec:function(e){e.toUpperCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"tolowercase",bindKey:o("Ctrl-Shift-U","Ctrl-Shift-U"),exec:function(e){e.toLowerCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"expandtoline",bindKey:o("Ctrl-Shift-L","Command-Shift-L"),exec:function(e){var t=e.selection.getRange();t.start.column=t.end.column=0,t.end.row++,e.selection.setRange(t,!1)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"joinlines",bindKey:o(null,null),exec:function(e){var t=e.selection.isBackwards(),n=t?e.selection.getSelectionLead():e.selection.getSelectionAnchor(),i=t?e.selection.getSelectionAnchor():e.selection.getSelectionLead(),o=e.session.doc.getLine(n.row).length,u=e.session.doc.getTextRange(e.selection.getRange()),a=u.replace(/\n\s*/," ").length,f=e.session.doc.getLine(n.row);for(var l=n.row+1;l<=i.row+1;l++){var c=r.stringTrimLeft(r.stringTrimRight(e.session.doc.getLine(l)));c.length!==0&&(c=" "+c),f+=c}i.row+1<e.session.doc.getLength()-1&&(f+=e.session.doc.getNewLineCharacter()),e.clearSelection(),e.session.doc.replace(new s(n.row,0,i.row+2,0),f),a>0?(e.selection.moveCursorTo(n.row,n.column),e.selection.selectTo(n.row,n.column+a)):(o=e.session.doc.getLine(n.row).length>o?o+1:o,e.selection.moveCursorTo(n.row,o))},multiSelectAction:"forEach",readOnly:!0},{name:"invertSelection",bindKey:o(null,null),exec:function(e){var t=e.session.doc.getLength()-1,n=e.session.doc.getLine(t).length,r=e.selection.rangeList.ranges,i=[];r.length<1&&(r=[e.selection.getRange()]);for(var o=0;o<r.length;o++)o==r.length-1&&(r[o].end.row!==t||r[o].end.column!==n)&&i.push(new s(r[o].end.row,r[o].end.column,t,n)),o===0?(r[o].start.row!==0||r[o].start.column!==0)&&i.push(new s(0,0,r[o].start.row,r[o].start.column)):i.push(new s(r[o-1].end.row,r[o-1].end.column,r[o].start.row,r[o].start.column));e.exitMultiSelectMode(),e.clearSelection();for(var o=0;o<i.length;o++)e.selection.addRange(i[o],!1)},readOnly:!0,scrollIntoView:"none"}]}),define("ace/editor",["require","exports","module","ace/lib/fixoldbrowsers","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/useragent","ace/keyboard/textinput","ace/mouse/mouse_handler","ace/mouse/fold_handler","ace/keyboard/keybinding","ace/edit_session","ace/search","ace/range","ace/lib/event_emitter","ace/commands/command_manager","ace/commands/default_commands","ace/config","ace/token_iterator"],function(e,t,n){"use strict";e("./lib/fixoldbrowsers");var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./lib/lang"),o=e("./lib/useragent"),u=e("./keyboard/textinput").TextInput,a=e("./mouse/mouse_handler").MouseHandler,f=e("./mouse/fold_handler").FoldHandler,l=e("./keyboard/keybinding").KeyBinding,c=e("./edit_session").EditSession,h=e("./search").Search,p=e("./range").Range,d=e("./lib/event_emitter").EventEmitter,v=e("./commands/command_manager").CommandManager,m=e("./commands/default_commands").commands,g=e("./config"),y=e("./token_iterator").TokenIterator,b=function(e,t){var n=e.getContainerElement();this.container=n,this.renderer=e,this.commands=new v(o.isMac?"mac":"win",m),this.textInput=new u(e.getTextAreaContainer(),this),this.renderer.textarea=this.textInput.getElement(),this.keyBinding=new l(this),this.$mouseHandler=new a(this),new f(this),this.$blockScrolling=0,this.$search=(new h).set({wrap:!0}),this.$historyTracker=this.$historyTracker.bind(this),this.commands.on("exec",this.$historyTracker),this.$initOperationListeners(),this._$emitInputEvent=s.delayedCall(function(){this._signal("input",{}),this.session&&this.session.bgTokenizer&&this.session.bgTokenizer.scheduleStart()}.bind(this)),this.on("change",function(e,t){t._$emitInputEvent.schedule(31)}),this.setSession(t||new c("")),g.resetOptions(this),g._signal("editor",this)};(function(){r.implement(this,d),this.$initOperationListeners=function(){function e(e){return e[e.length-1]}this.selections=[],this.commands.on("exec",this.startOperation.bind(this),!0),this.commands.on("afterExec",this.endOperation.bind(this),!0),this.$opResetTimer=s.delayedCall(this.endOperation.bind(this)),this.on("change",function(){this.curOp||this.startOperation(),this.curOp.docChanged=!0}.bind(this),!0),this.on("changeSelection",function(){this.curOp||this.startOperation(),this.curOp.selectionChanged=!0}.bind(this),!0)},this.curOp=null,this.prevOp={},this.startOperation=function(e){if(this.curOp){if(!e||this.curOp.command)return;this.prevOp=this.curOp}e||(this.previousCommand=null,e={}),this.$opResetTimer.schedule(),this.curOp={command:e.command||{},args:e.args,scrollTop:this.renderer.scrollTop},this.curOp.command.name&&this.curOp.command.scrollIntoView!==undefined&&this.$blockScrolling++},this.endOperation=function(e){if(this.curOp){if(e&&e.returnValue===!1)return this.curOp=null;this._signal("beforeEndOperation");var t=this.curOp.command;t.name&&this.$blockScrolling>0&&this.$blockScrolling--;var n=t&&t.scrollIntoView;if(n){switch(n){case"center-animate":n="animate";case"center":this.renderer.scrollCursorIntoView(null,.5);break;case"animate":case"cursor":this.renderer.scrollCursorIntoView();break;case"selectionPart":var r=this.selection.getRange(),i=this.renderer.layerConfig;(r.start.row>=i.lastRow||r.end.row<=i.firstRow)&&this.renderer.scrollSelectionIntoView(this.selection.anchor,this.selection.lead);break;default:}n=="animate"&&this.renderer.animateScrolling(this.curOp.scrollTop)}this.prevOp=this.curOp,this.curOp=null}},this.$mergeableCommands=["backspace","del","insertstring"],this.$historyTracker=function(e){if(!this.$mergeUndoDeltas)return;var t=this.prevOp,n=this.$mergeableCommands,r=t.command&&e.command.name==t.command.name;if(e.command.name=="insertstring"){var i=e.args;this.mergeNextCommand===undefined&&(this.mergeNextCommand=!0),r=r&&this.mergeNextCommand&&(!/\s/.test(i)||/\s/.test(t.args)),this.mergeNextCommand=!0}else r=r&&n.indexOf(e.command.name)!==-1;this.$mergeUndoDeltas!="always"&&Date.now()-this.sequenceStartTime>2e3&&(r=!1),r?this.session.mergeUndoDeltas=!0:n.indexOf(e.command.name)!==-1&&(this.sequenceStartTime=Date.now())},this.setKeyboardHandler=function(e,t){if(e&&typeof e=="string"){this.$keybindingId=e;var n=this;g.loadModule(["keybinding",e],function(r){n.$keybindingId==e&&n.keyBinding.setKeyboardHandler(r&&r.handler),t&&t()})}else this.$keybindingId=null,this.keyBinding.setKeyboardHandler(e),t&&t()},this.getKeyboardHandler=function(){return this.keyBinding.getKeyboardHandler()},this.setSession=function(e){if(this.session==e)return;this.curOp&&this.endOperation(),this.curOp={};var t=this.session;if(t){this.session.off("change",this.$onDocumentChange),this.session.off("changeMode",this.$onChangeMode),this.session.off("tokenizerUpdate",this.$onTokenizerUpdate),this.session.off("changeTabSize",this.$onChangeTabSize),this.session.off("changeWrapLimit",this.$onChangeWrapLimit),this.session.off("changeWrapMode",this.$onChangeWrapMode),this.session.off("changeFold",this.$onChangeFold),this.session.off("changeFrontMarker",this.$onChangeFrontMarker),this.session.off("changeBackMarker",this.$onChangeBackMarker),this.session.off("changeBreakpoint",this.$onChangeBreakpoint),this.session.off("changeAnnotation",this.$onChangeAnnotation),this.session.off("changeOverwrite",this.$onCursorChange),this.session.off("changeScrollTop",this.$onScrollTopChange),this.session.off("changeScrollLeft",this.$onScrollLeftChange);var n=this.session.getSelection();n.off("changeCursor",this.$onCursorChange),n.off("changeSelection",this.$onSelectionChange)}this.session=e,e?(this.$onDocumentChange=this.onDocumentChange.bind(this),e.on("change",this.$onDocumentChange),this.renderer.setSession(e),this.$onChangeMode=this.onChangeMode.bind(this),e.on("changeMode",this.$onChangeMode),this.$onTokenizerUpdate=this.onTokenizerUpdate.bind(this),e.on("tokenizerUpdate",this.$onTokenizerUpdate),this.$onChangeTabSize=this.renderer.onChangeTabSize.bind(this.renderer),e.on("changeTabSize",this.$onChangeTabSize),this.$onChangeWrapLimit=this.onChangeWrapLimit.bind(this),e.on("changeWrapLimit",this.$onChangeWrapLimit),this.$onChangeWrapMode=this.onChangeWrapMode.bind(this),e.on("changeWrapMode",this.$onChangeWrapMode),this.$onChangeFold=this.onChangeFold.bind(this),e.on("changeFold",this.$onChangeFold),this.$onChangeFrontMarker=this.onChangeFrontMarker.bind(this),this.session.on("changeFrontMarker",this.$onChangeFrontMarker),this.$onChangeBackMarker=this.onChangeBackMarker.bind(this),this.session.on("changeBackMarker",this.$onChangeBackMarker),this.$onChangeBreakpoint=this.onChangeBreakpoint.bind(this),this.session.on("changeBreakpoint",this.$onChangeBreakpoint),this.$onChangeAnnotation=this.onChangeAnnotation.bind(this),this.session.on("changeAnnotation",this.$onChangeAnnotation),this.$onCursorChange=this.onCursorChange.bind(this),this.session.on("changeOverwrite",this.$onCursorChange),this.$onScrollTopChange=this.onScrollTopChange.bind(this),this.session.on("changeScrollTop",this.$onScrollTopChange),this.$onScrollLeftChange=this.onScrollLeftChange.bind(this),this.session.on("changeScrollLeft",this.$onScrollLeftChange),this.selection=e.getSelection(),this.selection.on("changeCursor",this.$onCursorChange),this.$onSelectionChange=this.onSelectionChange.bind(this),this.selection.on("changeSelection",this.$onSelectionChange),this.onChangeMode(),this.$blockScrolling+=1,this.onCursorChange(),this.$blockScrolling-=1,this.onScrollTopChange(),this.onScrollLeftChange(),this.onSelectionChange(),this.onChangeFrontMarker(),this.onChangeBackMarker(),this.onChangeBreakpoint(),this.onChangeAnnotation(),this.session.getUseWrapMode()&&this.renderer.adjustWrapLimit(),this.renderer.updateFull()):(this.selection=null,this.renderer.setSession(e)),this._signal("changeSession",{session:e,oldSession:t}),this.curOp=null,t&&t._signal("changeEditor",{oldEditor:this}),e&&e._signal("changeEditor",{editor:this})},this.getSession=function(){return this.session},this.setValue=function(e,t){return this.session.doc.setValue(e),t?t==1?this.navigateFileEnd():t==-1&&this.navigateFileStart():this.selectAll(),e},this.getValue=function(){return this.session.getValue()},this.getSelection=function(){return this.selection},this.resize=function(e){this.renderer.onResize(e)},this.setTheme=function(e,t){this.renderer.setTheme(e,t)},this.getTheme=function(){return this.renderer.getTheme()},this.setStyle=function(e){this.renderer.setStyle(e)},this.unsetStyle=function(e){this.renderer.unsetStyle(e)},this.getFontSize=function(){return this.getOption("fontSize")||i.computedStyle(this.container,"fontSize")},this.setFontSize=function(e){this.setOption("fontSize",e)},this.$highlightBrackets=function(){this.session.$bracketHighlight&&(this.session.removeMarker(this.session.$bracketHighlight),this.session.$bracketHighlight=null);if(this.$highlightPending)return;var e=this;this.$highlightPending=!0,setTimeout(function(){e.$highlightPending=!1;var t=e.session;if(!t||!t.bgTokenizer)return;var n=t.findMatchingBracket(e.getCursorPosition());if(n)var r=new p(n.row,n.column,n.row,n.column+1);else if(t.$mode.getMatching)var r=t.$mode.getMatching(e.session);r&&(t.$bracketHighlight=t.addMarker(r,"ace_bracket","text"))},50)},this.$highlightTags=function(){if(this.$highlightTagPending)return;var e=this;this.$highlightTagPending=!0,setTimeout(function(){e.$highlightTagPending=!1;var t=e.session;if(!t||!t.bgTokenizer)return;var n=e.getCursorPosition(),r=new y(e.session,n.row,n.column),i=r.getCurrentToken();if(!i||!/\b(?:tag-open|tag-name)/.test(i.type)){t.removeMarker(t.$tagHighlight),t.$tagHighlight=null;return}if(i.type.indexOf("tag-open")!=-1){i=r.stepForward();if(!i)return}var s=i.value,o=0,u=r.stepBackward();if(u.value=="<"){do u=i,i=r.stepForward(),i&&i.value===s&&i.type.indexOf("tag-name")!==-1&&(u.value==="<"?o++:u.value==="</"&&o--);while(i&&o>=0)}else{do i=u,u=r.stepBackward(),i&&i.value===s&&i.type.indexOf("tag-name")!==-1&&(u.value==="<"?o++:u.value==="</"&&o--);while(u&&o<=0);r.stepForward()}if(!i){t.removeMarker(t.$tagHighlight),t.$tagHighlight=null;return}var a=r.getCurrentTokenRow(),f=r.getCurrentTokenColumn(),l=new p(a,f,a,f+i.value.length);t.$tagHighlight&&l.compareRange(t.$backMarkers[t.$tagHighlight].range)!==0&&(t.removeMarker(t.$tagHighlight),t.$tagHighlight=null),l&&!t.$tagHighlight&&(t.$tagHighlight=t.addMarker(l,"ace_bracket","text"))},50)},this.focus=function(){var e=this;setTimeout(function(){e.textInput.focus()}),this.textInput.focus()},this.isFocused=function(){return this.textInput.isFocused()},this.blur=function(){this.textInput.blur()},this.onFocus=function(e){if(this.$isFocused)return;this.$isFocused=!0,this.renderer.showCursor(),this.renderer.visualizeFocus(),this._emit("focus",e)},this.onBlur=function(e){if(!this.$isFocused)return;this.$isFocused=!1,this.renderer.hideCursor(),this.renderer.visualizeBlur(),this._emit("blur",e)},this.$cursorChange=function(){this.renderer.updateCursor()},this.onDocumentChange=function(e){var t=this.session.$useWrapMode,n=e.start.row==e.end.row?e.end.row:Infinity;this.renderer.updateLines(e.start.row,n,t),this._signal("change",e),this.$cursorChange(),this.$updateHighlightActiveLine()},this.onTokenizerUpdate=function(e){var t=e.data;this.renderer.updateLines(t.first,t.last)},this.onScrollTopChange=function(){this.renderer.scrollToY(this.session.getScrollTop())},this.onScrollLeftChange=function(){this.renderer.scrollToX(this.session.getScrollLeft())},this.onCursorChange=function(){this.$cursorChange(),this.$blockScrolling||(g.warn("Automatically scrolling cursor into view after selection change","this will be disabled in the next version","set editor.$blockScrolling = Infinity to disable this message"),this.renderer.scrollCursorIntoView()),this.$highlightBrackets(),this.$highlightTags(),this.$updateHighlightActiveLine(),this._signal("changeSelection")},this.$updateHighlightActiveLine=function(){var e=this.getSession(),t;if(this.$highlightActiveLine){if(this.$selectionStyle!="line"||!this.selection.isMultiLine())t=this.getCursorPosition();this.renderer.$maxLines&&this.session.getLength()===1&&!(this.renderer.$minLines>1)&&(t=!1)}if(e.$highlightLineMarker&&!t)e.removeMarker(e.$highlightLineMarker.id),e.$highlightLineMarker=null;else if(!e.$highlightLineMarker&&t){var n=new p(t.row,t.column,t.row,Infinity);n.id=e.addMarker(n,"ace_active-line","screenLine"),e.$highlightLineMarker=n}else t&&(e.$highlightLineMarker.start.row=t.row,e.$highlightLineMarker.end.row=t.row,e.$highlightLineMarker.start.column=t.column,e._signal("changeBackMarker"))},this.onSelectionChange=function(e){var t=this.session;t.$selectionMarker&&t.removeMarker(t.$selectionMarker),t.$selectionMarker=null;if(!this.selection.isEmpty()){var n=this.selection.getRange(),r=this.getSelectionStyle();t.$selectionMarker=t.addMarker(n,"ace_selection",r)}else this.$updateHighlightActiveLine();var i=this.$highlightSelectedWord&&this.$getSelectionHighLightRegexp();this.session.highlight(i),this._signal("changeSelection")},this.$getSelectionHighLightRegexp=function(){var e=this.session,t=this.getSelectionRange();if(t.isEmpty()||t.isMultiLine())return;var n=t.start.column-1,r=t.end.column+1,i=e.getLine(t.start.row),s=i.length,o=i.substring(Math.max(n,0),Math.min(r,s));if(n>=0&&/^[\w\d]/.test(o)||r<=s&&/[\w\d]$/.test(o))return;o=i.substring(t.start.column,t.end.column);if(!/^[\w\d]+$/.test(o))return;var u=this.$search.$assembleRegExp({wholeWord:!0,caseSensitive:!0,needle:o});return u},this.onChangeFrontMarker=function(){this.renderer.updateFrontMarkers()},this.onChangeBackMarker=function(){this.renderer.updateBackMarkers()},this.onChangeBreakpoint=function(){this.renderer.updateBreakpoints()},this.onChangeAnnotation=function(){this.renderer.setAnnotations(this.session.getAnnotations())},this.onChangeMode=function(e){this.renderer.updateText(),this._emit("changeMode",e)},this.onChangeWrapLimit=function(){this.renderer.updateFull()},this.onChangeWrapMode=function(){this.renderer.onResize(!0)},this.onChangeFold=function(){this.$updateHighlightActiveLine(),this.renderer.updateFull()},this.getSelectedText=function(){return this.session.getTextRange(this.getSelectionRange())},this.getCopyText=function(){var e=this.getSelectedText();return this._signal("copy",e),e},this.onCopy=function(){this.commands.exec("copy",this)},this.onCut=function(){this.commands.exec("cut",this)},this.onPaste=function(e,t){var n={text:e,event:t};this.commands.exec("paste",this,n)},this.$handlePaste=function(e){typeof e=="string"&&(e={text:e}),this._signal("paste",e);var t=e.text;if(!this.inMultiSelectMode||this.inVirtualSelectionMode)this.insert(t);else{var n=t.split(/\r\n|\r|\n/),r=this.selection.rangeList.ranges;if(n.length>r.length||n.length<2||!n[1])return this.commands.exec("insertstring",this,t);for(var i=r.length;i--;){var s=r[i];s.isEmpty()||this.session.remove(s),this.session.insert(s.start,n[i])}}},this.execCommand=function(e,t){return this.commands.exec(e,this,t)},this.insert=function(e,t){var n=this.session,r=n.getMode(),i=this.getCursorPosition();if(this.getBehavioursEnabled()&&!t){var s=r.transformAction(n.getState(i.row),"insertion",this,n,e);s&&(e!==s.text&&(this.session.mergeUndoDeltas=!1,this.$mergeNextCommand=!1),e=s.text)}e==" "&&(e=this.session.getTabString());if(!this.selection.isEmpty()){var o=this.getSelectionRange();i=this.session.remove(o),this.clearSelection()}else if(this.session.getOverwrite()){var o=new p.fromPoints(i,i);o.end.column+=e.length,this.session.remove(o)}if(e=="\n"||e=="\r\n"){var u=n.getLine(i.row);if(i.column>u.search(/\S|$/)){var a=u.substr(i.column).search(/\S|$/);n.doc.removeInLine(i.row,i.column,i.column+a)}}this.clearSelection();var f=i.column,l=n.getState(i.row),u=n.getLine(i.row),c=r.checkOutdent(l,u,e),h=n.insert(i,e);s&&s.selection&&(s.selection.length==2?this.selection.setSelectionRange(new p(i.row,f+s.selection[0],i.row,f+s.selection[1])):this.selection.setSelectionRange(new p(i.row+s.selection[0],s.selection[1],i.row+s.selection[2],s.selection[3])));if(n.getDocument().isNewLine(e)){var d=r.getNextLineIndent(l,u.slice(0,i.column),n.getTabString());n.insert({row:i.row+1,column:0},d)}c&&r.autoOutdent(l,n,i.row)},this.onTextInput=function(e){this.keyBinding.onTextInput(e)},this.onCommandKey=function(e,t,n){this.keyBinding.onCommandKey(e,t,n)},this.setOverwrite=function(e){this.session.setOverwrite(e)},this.getOverwrite=function(){return this.session.getOverwrite()},this.toggleOverwrite=function(){this.session.toggleOverwrite()},this.setScrollSpeed=function(e){this.setOption("scrollSpeed",e)},this.getScrollSpeed=function(){return this.getOption("scrollSpeed")},this.setDragDelay=function(e){this.setOption("dragDelay",e)},this.getDragDelay=function(){return this.getOption("dragDelay")},this.setSelectionStyle=function(e){this.setOption("selectionStyle",e)},this.getSelectionStyle=function(){return this.getOption("selectionStyle")},this.setHighlightActiveLine=function(e){this.setOption("highlightActiveLine",e)},this.getHighlightActiveLine=function(){return this.getOption("highlightActiveLine")},this.setHighlightGutterLine=function(e){this.setOption("highlightGutterLine",e)},this.getHighlightGutterLine=function(){return this.getOption("highlightGutterLine")},this.setHighlightSelectedWord=function(e){this.setOption("highlightSelectedWord",e)},this.getHighlightSelectedWord=function(){return this.$highlightSelectedWord},this.setAnimatedScroll=function(e){this.renderer.setAnimatedScroll(e)},this.getAnimatedScroll=function(){return this.renderer.getAnimatedScroll()},this.setShowInvisibles=function(e){this.renderer.setShowInvisibles(e)},this.getShowInvisibles=function(){return this.renderer.getShowInvisibles()},this.setDisplayIndentGuides=function(e){this.renderer.setDisplayIndentGuides(e)},this.getDisplayIndentGuides=function(){return this.renderer.getDisplayIndentGuides()},this.setShowPrintMargin=function(e){this.renderer.setShowPrintMargin(e)},this.getShowPrintMargin=function(){return this.renderer.getShowPrintMargin()},this.setPrintMarginColumn=function(e){this.renderer.setPrintMarginColumn(e)},this.getPrintMarginColumn=function(){return this.renderer.getPrintMarginColumn()},this.setReadOnly=function(e){this.setOption("readOnly",e)},this.getReadOnly=function(){return this.getOption("readOnly")},this.setBehavioursEnabled=function(e){this.setOption("behavioursEnabled",e)},this.getBehavioursEnabled=function(){return this.getOption("behavioursEnabled")},this.setWrapBehavioursEnabled=function(e){this.setOption("wrapBehavioursEnabled",e)},this.getWrapBehavioursEnabled=function(){return this.getOption("wrapBehavioursEnabled")},this.setShowFoldWidgets=function(e){this.setOption("showFoldWidgets",e)},this.getShowFoldWidgets=function(){return this.getOption("showFoldWidgets")},this.setFadeFoldWidgets=function(e){this.setOption("fadeFoldWidgets",e)},this.getFadeFoldWidgets=function(){return this.getOption("fadeFoldWidgets")},this.remove=function(e){this.selection.isEmpty()&&(e=="left"?this.selection.selectLeft():this.selection.selectRight());var t=this.getSelectionRange();if(this.getBehavioursEnabled()){var n=this.session,r=n.getState(t.start.row),i=n.getMode().transformAction(r,"deletion",this,n,t);if(t.end.column===0){var s=n.getTextRange(t);if(s[s.length-1]=="\n"){var o=n.getLine(t.end.row);/^\s+$/.test(o)&&(t.end.column=o.length)}}i&&(t=i)}this.session.remove(t),this.clearSelection()},this.removeWordRight=function(){this.selection.isEmpty()&&this.selection.selectWordRight(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeWordLeft=function(){this.selection.isEmpty()&&this.selection.selectWordLeft(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineStart=function(){this.selection.isEmpty()&&this.selection.selectLineStart(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineEnd=function(){this.selection.isEmpty()&&this.selection.selectLineEnd();var e=this.getSelectionRange();e.start.column==e.end.column&&e.start.row==e.end.row&&(e.end.column=0,e.end.row++),this.session.remove(e),this.clearSelection()},this.splitLine=function(){this.selection.isEmpty()||(this.session.remove(this.getSelectionRange()),this.clearSelection());var e=this.getCursorPosition();this.insert("\n"),this.moveCursorToPosition(e)},this.transposeLetters=function(){if(!this.selection.isEmpty())return;var e=this.getCursorPosition(),t=e.column;if(t===0)return;var n=this.session.getLine(e.row),r,i;t<n.length?(r=n.charAt(t)+n.charAt(t-1),i=new p(e.row,t-1,e.row,t+1)):(r=n.charAt(t-1)+n.charAt(t-2),i=new p(e.row,t-2,e.row,t)),this.session.replace(i,r)},this.toLowerCase=function(){var e=this.getSelectionRange();this.selection.isEmpty()&&this.selection.selectWord();var t=this.getSelectionRange(),n=this.session.getTextRange(t);this.session.replace(t,n.toLowerCase()),this.selection.setSelectionRange(e)},this.toUpperCase=function(){var e=this.getSelectionRange();this.selection.isEmpty()&&this.selection.selectWord();var t=this.getSelectionRange(),n=this.session.getTextRange(t);this.session.replace(t,n.toUpperCase()),this.selection.setSelectionRange(e)},this.indent=function(){var e=this.session,t=this.getSelectionRange();if(t.start.row<t.end.row){var n=this.$getSelectedRows();e.indentRows(n.first,n.last," ");return}if(t.start.column<t.end.column){var r=e.getTextRange(t);if(!/^\s+$/.test(r)){var n=this.$getSelectedRows();e.indentRows(n.first,n.last," ");return}}var i=e.getLine(t.start.row),o=t.start,u=e.getTabSize(),a=e.documentToScreenColumn(o.row,o.column);if(this.session.getUseSoftTabs())var f=u-a%u,l=s.stringRepeat(" ",f);else{var f=a%u;while(i[t.start.column]==" "&&f)t.start.column--,f--;this.selection.setSelectionRange(t),l=" "}return this.insert(l)},this.blockIndent=function(){var e=this.$getSelectedRows();this.session.indentRows(e.first,e.last," ")},this.blockOutdent=function(){var e=this.session.getSelection();this.session.outdentRows(e.getRange())},this.sortLines=function(){var e=this.$getSelectedRows(),t=this.session,n=[];for(i=e.first;i<=e.last;i++)n.push(t.getLine(i));n.sort(function(e,t){return e.toLowerCase()<t.toLowerCase()?-1:e.toLowerCase()>t.toLowerCase()?1:0});var r=new p(0,0,0,0);for(var i=e.first;i<=e.last;i++){var s=t.getLine(i);r.start.row=i,r.end.row=i,r.end.column=s.length,t.replace(r,n[i-e.first])}},this.toggleCommentLines=function(){var e=this.session.getState(this.getCursorPosition().row),t=this.$getSelectedRows();this.session.getMode().toggleCommentLines(e,this.session,t.first,t.last)},this.toggleBlockComment=function(){var e=this.getCursorPosition(),t=this.session.getState(e.row),n=this.getSelectionRange();this.session.getMode().toggleBlockComment(t,this.session,n,e)},this.getNumberAt=function(e,t){var n=/[\-]?[0-9]+(?:\.[0-9]+)?/g;n.lastIndex=0;var r=this.session.getLine(e);while(n.lastIndex<t){var i=n.exec(r);if(i.index<=t&&i.index+i[0].length>=t){var s={value:i[0],start:i.index,end:i.index+i[0].length};return s}}return null},this.modifyNumber=function(e){var t=this.selection.getCursor().row,n=this.selection.getCursor().column,r=new p(t,n-1,t,n),i=this.session.getTextRange(r);if(!isNaN(parseFloat(i))&&isFinite(i)){var s=this.getNumberAt(t,n);if(s){var o=s.value.indexOf(".")>=0?s.start+s.value.indexOf(".")+1:s.end,u=s.start+s.value.length-o,a=parseFloat(s.value);a*=Math.pow(10,u),o!==s.end&&n<o?e*=Math.pow(10,s.end-n-1):e*=Math.pow(10,s.end-n),a+=e,a/=Math.pow(10,u);var f=a.toFixed(u),l=new p(t,s.start,t,s.end);this.session.replace(l,f),this.moveCursorTo(t,Math.max(s.start+1,n+f.length-s.value.length))}}},this.removeLines=function(){var e=this.$getSelectedRows();this.session.removeFullLines(e.first,e.last),this.clearSelection()},this.duplicateSelection=function(){var e=this.selection,t=this.session,n=e.getRange(),r=e.isBackwards();if(n.isEmpty()){var i=n.start.row;t.duplicateLines(i,i)}else{var s=r?n.start:n.end,o=t.insert(s,t.getTextRange(n),!1);n.start=s,n.end=o,e.setSelectionRange(n,r)}},this.moveLinesDown=function(){this.$moveLines(1,!1)},this.moveLinesUp=function(){this.$moveLines(-1,!1)},this.moveText=function(e,t,n){return this.session.moveText(e,t,n)},this.copyLinesUp=function(){this.$moveLines(-1,!0)},this.copyLinesDown=function(){this.$moveLines(1,!0)},this.$moveLines=function(e,t){var n,r,i=this.selection;if(!i.inMultiSelectMode||this.inVirtualSelectionMode){var s=i.toOrientedRange();n=this.$getSelectedRows(s),r=this.session.$moveLines(n.first,n.last,t?0:e),t&&e==-1&&(r=0),s.moveBy(r,0),i.fromOrientedRange(s)}else{var o=i.rangeList.ranges;i.rangeList.detach(this.session),this.inVirtualSelectionMode=!0;var u=0,a=0,f=o.length;for(var l=0;l<f;l++){var c=l;o[l].moveBy(u,0),n=this.$getSelectedRows(o[l]);var h=n.first,p=n.last;while(++l<f){a&&o[l].moveBy(a,0);var d=this.$getSelectedRows(o[l]);if(t&&d.first!=p)break;if(!t&&d.first>p+1)break;p=d.last}l--,u=this.session.$moveLines(h,p,t?0:e),t&&e==-1&&(c=l+1);while(c<=l)o[c].moveBy(u,0),c++;t||(u=0),a+=u}i.fromOrientedRange(i.ranges[0]),i.rangeList.attach(this.session),this.inVirtualSelectionMode=!1}},this.$getSelectedRows=function(e){return e=(e||this.getSelectionRange()).collapseRows(),{first:this.session.getRowFoldStart(e.start.row),last:this.session.getRowFoldEnd(e.end.row)}},this.onCompositionStart=function(e){this.renderer.showComposition(this.getCursorPosition())},this.onCompositionUpdate=function(e){this.renderer.setCompositionText(e)},this.onCompositionEnd=function(){this.renderer.hideComposition()},this.getFirstVisibleRow=function(){return this.renderer.getFirstVisibleRow()},this.getLastVisibleRow=function(){return this.renderer.getLastVisibleRow()},this.isRowVisible=function(e){return e>=this.getFirstVisibleRow()&&e<=this.getLastVisibleRow()},this.isRowFullyVisible=function(e){return e>=this.renderer.getFirstFullyVisibleRow()&&e<=this.renderer.getLastFullyVisibleRow()},this.$getVisibleRowCount=function(){return this.renderer.getScrollBottomRow()-this.renderer.getScrollTopRow()+1},this.$moveByPage=function(e,t){var n=this.renderer,r=this.renderer.layerConfig,i=e*Math.floor(r.height/r.lineHeight);this.$blockScrolling++,t===!0?this.selection.$moveSelection(function(){this.moveCursorBy(i,0)}):t===!1&&(this.selection.moveCursorBy(i,0),this.selection.clearSelection()),this.$blockScrolling--;var s=n.scrollTop;n.scrollBy(0,i*r.lineHeight),t!=null&&n.scrollCursorIntoView(null,.5),n.animateScrolling(s)},this.selectPageDown=function(){this.$moveByPage(1,!0)},this.selectPageUp=function(){this.$moveByPage(-1,!0)},this.gotoPageDown=function(){this.$moveByPage(1,!1)},this.gotoPageUp=function(){this.$moveByPage(-1,!1)},this.scrollPageDown=function(){this.$moveByPage(1)},this.scrollPageUp=function(){this.$moveByPage(-1)},this.scrollToRow=function(e){this.renderer.scrollToRow(e)},this.scrollToLine=function(e,t,n,r){this.renderer.scrollToLine(e,t,n,r)},this.centerSelection=function(){var e=this.getSelectionRange(),t={row:Math.floor(e.start.row+(e.end.row-e.start.row)/2),column:Math.floor(e.start.column+(e.end.column-e.start.column)/2)};this.renderer.alignCursor(t,.5)},this.getCursorPosition=function(){return this.selection.getCursor()},this.getCursorPositionScreen=function(){return this.session.documentToScreenPosition(this.getCursorPosition())},this.getSelectionRange=function(){return this.selection.getRange()},this.selectAll=function(){this.$blockScrolling+=1,this.selection.selectAll(),this.$blockScrolling-=1},this.clearSelection=function(){this.selection.clearSelection()},this.moveCursorTo=function(e,t){this.selection.moveCursorTo(e,t)},this.moveCursorToPosition=function(e){this.selection.moveCursorToPosition(e)},this.jumpToMatching=function(e,t){var n=this.getCursorPosition(),r=new y(this.session,n.row,n.column),i=r.getCurrentToken(),s=i||r.stepForward();if(!s)return;var o,u=!1,a={},f=n.column-s.start,l,c={")":"(","(":"(","]":"[","[":"[","{":"{","}":"{"};do{if(s.value.match(/[{}()\[\]]/g))for(;f<s.value.length&&!u;f++){if(!c[s.value[f]])continue;l=c[s.value[f]]+"."+s.type.replace("rparen","lparen"),isNaN(a[l])&&(a[l]=0);switch(s.value[f]){case"(":case"[":case"{":a[l]++;break;case")":case"]":case"}":a[l]--,a[l]===-1&&(o="bracket",u=!0)}}else s&&s.type.indexOf("tag-name")!==-1&&(isNaN(a[s.value])&&(a[s.value]=0),i.value==="<"?a[s.value]++:i.value==="</"&&a[s.value]--,a[s.value]===-1&&(o="tag",u=!0));u||(i=s,s=r.stepForward(),f=0)}while(s&&!u);if(!o)return;var h,d;if(o==="bracket"){h=this.session.getBracketRange(n);if(!h){h=new p(r.getCurrentTokenRow(),r.getCurrentTokenColumn()+f-1,r.getCurrentTokenRow(),r.getCurrentTokenColumn()+f-1),d=h.start;if(t||d.row===n.row&&Math.abs(d.column-n.column)<2)h=this.session.getBracketRange(d)}}else if(o==="tag"){if(!s||s.type.indexOf("tag-name")===-1)return;var v=s.value;h=new p(r.getCurrentTokenRow(),r.getCurrentTokenColumn()-2,r.getCurrentTokenRow(),r.getCurrentTokenColumn()-2);if(h.compare(n.row,n.column)===0){u=!1;do s=i,i=r.stepBackward(),i&&(i.type.indexOf("tag-close")!==-1&&h.setEnd(r.getCurrentTokenRow(),r.getCurrentTokenColumn()+1),s.value===v&&s.type.indexOf("tag-name")!==-1&&(i.value==="<"?a[v]++:i.value==="</"&&a[v]--,a[v]===0&&(u=!0)));while(i&&!u)}s&&s.type.indexOf("tag-name")&&(d=h.start,d.row==n.row&&Math.abs(d.column-n.column)<2&&(d=h.end))}d=h&&h.cursor||d,d&&(e?h&&t?this.selection.setRange(h):h&&h.isEqual(this.getSelectionRange())?this.clearSelection():this.selection.selectTo(d.row,d.column):this.selection.moveTo(d.row,d.column))},this.gotoLine=function(e,t,n){this.selection.clearSelection(),this.session.unfold({row:e-1,column:t||0}),this.$blockScrolling+=1,this.exitMultiSelectMode&&this.exitMultiSelectMode(),this.moveCursorTo(e-1,t||0),this.$blockScrolling-=1,this.isRowFullyVisible(e-1)||this.scrollToLine(e-1,!0,n)},this.navigateTo=function(e,t){this.selection.moveTo(e,t)},this.navigateUp=function(e){if(this.selection.isMultiLine()&&!this.selection.isBackwards()){var t=this.selection.anchor.getPosition();return this.moveCursorToPosition(t)}this.selection.clearSelection(),this.selection.moveCursorBy(-e||-1,0)},this.navigateDown=function(e){if(this.selection.isMultiLine()&&this.selection.isBackwards()){var t=this.selection.anchor.getPosition();return this.moveCursorToPosition(t)}this.selection.clearSelection(),this.selection.moveCursorBy(e||1,0)},this.navigateLeft=function(e){if(!this.selection.isEmpty()){var t=this.getSelectionRange().start;this.moveCursorToPosition(t)}else{e=e||1;while(e--)this.selection.moveCursorLeft()}this.clearSelection()},this.navigateRight=function(e){if(!this.selection.isEmpty()){var t=this.getSelectionRange().end;this.moveCursorToPosition(t)}else{e=e||1;while(e--)this.selection.moveCursorRight()}this.clearSelection()},this.navigateLineStart=function(){this.selection.moveCursorLineStart(),this.clearSelection()},this.navigateLineEnd=function(){this.selection.moveCursorLineEnd(),this.clearSelection()},this.navigateFileEnd=function(){this.selection.moveCursorFileEnd(),this.clearSelection()},this.navigateFileStart=function(){this.selection.moveCursorFileStart(),this.clearSelection()},this.navigateWordRight=function(){this.selection.moveCursorWordRight(),this.clearSelection()},this.navigateWordLeft=function(){this.selection.moveCursorWordLeft(),this.clearSelection()},this.replace=function(e,t){t&&this.$search.set(t);var n=this.$search.find(this.session),r=0;return n?(this.$tryReplace(n,e)&&(r=1),n!==null&&(this.selection.setSelectionRange(n),this.renderer.scrollSelectionIntoView(n.start,n.end)),r):r},this.replaceAll=function(e,t){t&&this.$search.set(t);var n=this.$search.findAll(this.session),r=0;if(!n.length)return r;this.$blockScrolling+=1;var i=this.getSelectionRange();this.selection.moveTo(0,0);for(var s=n.length-1;s>=0;--s)this.$tryReplace(n[s],e)&&r++;return this.selection.setSelectionRange(i),this.$blockScrolling-=1,r},this.$tryReplace=function(e,t){var n=this.session.getTextRange(e);return t=this.$search.replace(n,t),t!==null?(e.end=this.session.replace(e,t),e):null},this.getLastSearchOptions=function(){return this.$search.getOptions()},this.find=function(e,t,n){t||(t={}),typeof e=="string"||e instanceof RegExp?t.needle=e:typeof e=="object"&&r.mixin(t,e);var i=this.selection.getRange();t.needle==null&&(e=this.session.getTextRange(i)||this.$search.$options.needle,e||(i=this.session.getWordRange(i.start.row,i.start.column),e=this.session.getTextRange(i)),this.$search.set({needle:e})),this.$search.set(t),t.start||this.$search.set({start:i});var s=this.$search.find(this.session);if(t.preventScroll)return s;if(s)return this.revealRange(s,n),s;t.backwards?i.start=i.end:i.end=i.start,this.selection.setRange(i)},this.findNext=function(e,t){this.find({skipCurrent:!0,backwards:!1},e,t)},this.findPrevious=function(e,t){this.find(e,{skipCurrent:!0,backwards:!0},t)},this.revealRange=function(e,t){this.$blockScrolling+=1,this.session.unfold(e),this.selection.setSelectionRange(e),this.$blockScrolling-=1;var n=this.renderer.scrollTop;this.renderer.scrollSelectionIntoView(e.start,e.end,.5),t!==!1&&this.renderer.animateScrolling(n)},this.undo=function(){this.$blockScrolling++,this.session.getUndoManager().undo(),this.$blockScrolling--,this.renderer.scrollCursorIntoView(null,.5)},this.redo=function(){this.$blockScrolling++,this.session.getUndoManager().redo(),this.$blockScrolling--,this.renderer.scrollCursorIntoView(null,.5)},this.destroy=function(){this.renderer.destroy(),this._signal("destroy",this),this.session&&this.session.destroy()},this.setAutoScrollEditorIntoView=function(e){if(!e)return;var t,n=this,r=!1;this.$scrollAnchor||(this.$scrollAnchor=document.createElement("div"));var i=this.$scrollAnchor;i.style.cssText="position:absolute",this.container.insertBefore(i,this.container.firstChild);var s=this.on("changeSelection",function(){r=!0}),o=this.renderer.on("beforeRender",function(){r&&(t=n.renderer.container.getBoundingClientRect())}),u=this.renderer.on("afterRender",function(){if(r&&t&&(n.isFocused()||n.searchBox&&n.searchBox.isFocused())){var e=n.renderer,s=e.$cursorLayer.$pixelPos,o=e.layerConfig,u=s.top-o.offset;s.top>=0&&u+t.top<0?r=!0:s.top<o.height&&s.top+t.top+o.lineHeight>window.innerHeight?r=!1:r=null,r!=null&&(i.style.top=u+"px",i.style.left=s.left+"px",i.style.height=o.lineHeight+"px",i.scrollIntoView(r)),r=t=null}});this.setAutoScrollEditorIntoView=function(e){if(e)return;delete this.setAutoScrollEditorIntoView,this.off("changeSelection",s),this.renderer.off("afterRender",u),this.renderer.off("beforeRender",o)}},this.$resetCursorStyle=function(){var e=this.$cursorStyle||"ace",t=this.renderer.$cursorLayer;if(!t)return;t.setSmoothBlinking(/smooth/.test(e)),t.isBlinking=!this.$readOnly&&e!="wide",i.setCssClass(t.element,"ace_slim-cursors",/slim/.test(e))}}).call(b.prototype),g.defineOptions(b.prototype,"editor",{selectionStyle:{set:function(e){this.onSelectionChange(),this._signal("changeSelectionStyle",{data:e})},initialValue:"line"},highlightActiveLine:{set:function(){this.$updateHighlightActiveLine()},initialValue:!0},highlightSelectedWord:{set:function(e){this.$onSelectionChange()},initialValue:!0},readOnly:{set:function(e){this.$resetCursorStyle()},initialValue:!1},cursorStyle:{set:function(e){this.$resetCursorStyle()},values:["ace","slim","smooth","wide"],initialValue:"ace"},mergeUndoDeltas:{values:[!1,!0,"always"],initialValue:!0},behavioursEnabled:{initialValue:!0},wrapBehavioursEnabled:{initialValue:!0},autoScrollEditorIntoView:{set:function(e){this.setAutoScrollEditorIntoView(e)}},keyboardHandler:{set:function(e){this.setKeyboardHandler(e)},get:function(){return this.keybindingId},handlesSet:!0},hScrollBarAlwaysVisible:"renderer",vScrollBarAlwaysVisible:"renderer",highlightGutterLine:"renderer",animatedScroll:"renderer",showInvisibles:"renderer",showPrintMargin:"renderer",printMarginColumn:"renderer",printMargin:"renderer",fadeFoldWidgets:"renderer",showFoldWidgets:"renderer",showLineNumbers:"renderer",showGutter:"renderer",displayIndentGuides:"renderer",fontSize:"renderer",fontFamily:"renderer",maxLines:"renderer",minLines:"renderer",scrollPastEnd:"renderer",fixedWidthGutter:"renderer",theme:"renderer",scrollSpeed:"$mouseHandler",dragDelay:"$mouseHandler",dragEnabled:"$mouseHandler",focusTimout:"$mouseHandler",tooltipFollowsMouse:"$mouseHandler",firstLineNumber:"session",overwrite:"session",newLineMode:"session",useWorker:"session",useSoftTabs:"session",tabSize:"session",wrap:"session",indentedSoftWrap:"session",foldStyle:"session",mode:"session"}),t.Editor=b}),define("ace/undomanager",["require","exports","module"],function(e,t,n){"use strict";var r=function(){this.reset()};(function(){function e(e){return{action:e.action,start:e.start,end:e.end,lines:e.lines.length==1?null:e.lines,text:e.lines.length==1?e.lines[0]:null}}function t(e){return{action:e.action,start:e.start,end:e.end,lines:e.lines||[e.text]}}function n(e,t){var n=new Array(e.length);for(var r=0;r<e.length;r++){var i=e[r],s={group:i.group,deltas:new Array(i.length)};for(var o=0;o<i.deltas.length;o++){var u=i.deltas[o];s.deltas[o]=t(u)}n[r]=s}return n}this.execute=function(e){var t=e.args[0];this.$doc=e.args[1],e.merge&&this.hasUndo()&&(this.dirtyCounter--,t=this.$undoStack.pop().concat(t)),this.$undoStack.push(t),this.$redoStack=[],this.dirtyCounter<0&&(this.dirtyCounter=NaN),this.dirtyCounter++},this.undo=function(e){var t=this.$undoStack.pop(),n=null;return t&&(n=this.$doc.undoChanges(t,e),this.$redoStack.push(t),this.dirtyCounter--),n},this.redo=function(e){var t=this.$redoStack.pop(),n=null;return t&&(n=this.$doc.redoChanges(this.$deserializeDeltas(t),e),this.$undoStack.push(t),this.dirtyCounter++),n},this.reset=function(){this.$undoStack=[],this.$redoStack=[],this.dirtyCounter=0},this.hasUndo=function(){return this.$undoStack.length>0},this.hasRedo=function(){return this.$redoStack.length>0},this.markClean=function(){this.dirtyCounter=0},this.isClean=function(){return this.dirtyCounter===0},this.$serializeDeltas=function(t){return n(t,e)},this.$deserializeDeltas=function(e){return n(e,t)}}).call(r.prototype),t.UndoManager=r}),define("ace/layer/gutter",["require","exports","module","ace/lib/dom","ace/lib/oop","ace/lib/lang","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=e("../lib/oop"),s=e("../lib/lang"),o=e("../lib/event_emitter").EventEmitter,u=function(e){this.element=r.createElement("div"),this.element.className="ace_layer ace_gutter-layer",e.appendChild(this.element),this.setShowFoldWidgets(this.$showFoldWidgets),this.gutterWidth=0,this.$annotations=[],this.$updateAnnotations=this.$updateAnnotations.bind(this),this.$cells=[]};(function(){i.implement(this,o),this.setSession=function(e){this.session&&this.session.removeEventListener("change",this.$updateAnnotations),this.session=e,e&&e.on("change",this.$updateAnnotations)},this.addGutterDecoration=function(e,t){window.console&&console.warn&&console.warn("deprecated use session.addGutterDecoration"),this.session.addGutterDecoration(e,t)},this.removeGutterDecoration=function(e,t){window.console&&console.warn&&console.warn("deprecated use session.removeGutterDecoration"),this.session.removeGutterDecoration(e,t)},this.setAnnotations=function(e){this.$annotations=[];for(var t=0;t<e.length;t++){var n=e[t],r=n.row,i=this.$annotations[r];i||(i=this.$annotations[r]={text:[]});var o=n.text;o=o?s.escapeHTML(o):n.html||"",i.text.indexOf(o)===-1&&i.text.push(o);var u=n.type;u=="error"?i.className=" ace_error":u=="warning"&&i.className!=" ace_error"?i.className=" ace_warning":u=="info"&&!i.className&&(i.className=" ace_info")}},this.$updateAnnotations=function(e){if(!this.$annotations.length)return;var t=e.start.row,n=e.end.row-t;if(n!==0)if(e.action=="remove")this.$annotations.splice(t,n+1,null);else{var r=new Array(n+1);r.unshift(t,1),this.$annotations.splice.apply(this.$annotations,r)}},this.update=function(e){var t=this.session,n=e.firstRow,i=Math.min(e.lastRow+e.gutterOffset,t.getLength()-1),s=t.getNextFoldLine(n),o=s?s.start.row:Infinity,u=this.$showFoldWidgets&&t.foldWidgets,a=t.$breakpoints,f=t.$decorations,l=t.$firstLineNumber,c=0,h=t.gutterRenderer||this.$renderer,p=null,d=-1,v=n;for(;;){v>o&&(v=s.end.row+1,s=t.getNextFoldLine(v,s),o=s?s.start.row:Infinity);if(v>i){while(this.$cells.length>d+1)p=this.$cells.pop(),this.element.removeChild(p.element);break}p=this.$cells[++d],p||(p={element:null,textNode:null,foldWidget:null},p.element=r.createElement("div"),p.textNode=document.createTextNode(""),p.element.appendChild(p.textNode),this.element.appendChild(p.element),this.$cells[d]=p);var m="ace_gutter-cell ";a[v]&&(m+=a[v]),f[v]&&(m+=f[v]),this.$annotations[v]&&(m+=this.$annotations[v].className),p.element.className!=m&&(p.element.className=m);var g=t.getRowLength(v)*e.lineHeight+"px";g!=p.element.style.height&&(p.element.style.height=g);if(u){var y=u[v];y==null&&(y=u[v]=t.getFoldWidget(v))}if(y){p.foldWidget||(p.foldWidget=r.createElement("span"),p.element.appendChild(p.foldWidget));var m="ace_fold-widget ace_"+y;y=="start"&&v==o&&v<s.end.row?m+=" ace_closed":m+=" ace_open",p.foldWidget.className!=m&&(p.foldWidget.className=m);var g=e.lineHeight+"px";p.foldWidget.style.height!=g&&(p.foldWidget.style.height=g)}else p.foldWidget&&(p.element.removeChild(p.foldWidget),p.foldWidget=null);var b=c=h?h.getText(t,v):v+l;b!=p.textNode.data&&(p.textNode.data=b),v++}this.element.style.height=e.minHeight+"px";if(this.$fixedWidth||t.$useWrapMode)c=t.getLength()+l;var w=h?h.getWidth(t,c,e):c.toString().length*e.characterWidth,E=this.$padding||this.$computePadding();w+=E.left+E.right,w!==this.gutterWidth&&!isNaN(w)&&(this.gutterWidth=w,this.element.style.width=Math.ceil(this.gutterWidth)+"px",this._emit("changeGutterWidth",w))},this.$fixedWidth=!1,this.$showLineNumbers=!0,this.$renderer="",this.setShowLineNumbers=function(e){this.$renderer=!e&&{getWidth:function(){return""},getText:function(){return""}}},this.getShowLineNumbers=function(){return this.$showLineNumbers},this.$showFoldWidgets=!0,this.setShowFoldWidgets=function(e){e?r.addCssClass(this.element,"ace_folding-enabled"):r.removeCssClass(this.element,"ace_folding-enabled"),this.$showFoldWidgets=e,this.$padding=null},this.getShowFoldWidgets=function(){return this.$showFoldWidgets},this.$computePadding=function(){if(!this.element.firstChild)return{left:0,right:0};var e=r.computedStyle(this.element.firstChild);return this.$padding={},this.$padding.left=parseInt(e.paddingLeft)+1||0,this.$padding.right=parseInt(e.paddingRight)||0,this.$padding},this.getRegion=function(e){var t=this.$padding||this.$computePadding(),n=this.element.getBoundingClientRect();if(e.x<t.left+n.left)return"markers";if(this.$showFoldWidgets&&e.x>n.right-t.right)return"foldWidgets"}}).call(u.prototype),t.Gutter=u}),define("ace/layer/marker",["require","exports","module","ace/range","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../range").Range,i=e("../lib/dom"),s=function(e){this.element=i.createElement("div"),this.element.className="ace_layer ace_marker-layer",e.appendChild(this.element)};(function(){function e(e,t,n,r){return(e?1:0)|(t?2:0)|(n?4:0)|(r?8:0)}this.$padding=0,this.setPadding=function(e){this.$padding=e},this.setSession=function(e){this.session=e},this.setMarkers=function(e){this.markers=e},this.update=function(e){var e=e||this.config;if(!e)return;this.config=e;var t=[];for(var n in this.markers){var r=this.markers[n];if(!r.range){r.update(t,this,this.session,e);continue}var i=r.range.clipRows(e.firstRow,e.lastRow);if(i.isEmpty())continue;i=i.toScreenRange(this.session);if(r.renderer){var s=this.$getTop(i.start.row,e),o=this.$padding+i.start.column*e.characterWidth;r.renderer(t,i,o,s,e)}else r.type=="fullLine"?this.drawFullLineMarker(t,i,r.clazz,e):r.type=="screenLine"?this.drawScreenLineMarker(t,i,r.clazz,e):i.isMultiLine()?r.type=="text"?this.drawTextMarker(t,i,r.clazz,e):this.drawMultiLineMarker(t,i,r.clazz,e):this.drawSingleLineMarker(t,i,r.clazz+" ace_start"+" ace_br15",e)}this.element.innerHTML=t.join("")},this.$getTop=function(e,t){return(e-t.firstRowScreen)*t.lineHeight},this.drawTextMarker=function(t,n,i,s,o){var u=this.session,a=n.start.row,f=n.end.row,l=a,c=0,h=0,p=u.getScreenLastRowColumn(l),d=new r(l,n.start.column,l,h);for(;l<=f;l++)d.start.row=d.end.row=l,d.start.column=l==a?n.start.column:u.getRowWrapIndent(l),d.end.column=p,c=h,h=p,p=l+1<f?u.getScreenLastRowColumn(l+1):l==f?0:n.end.column,this.drawSingleLineMarker(t,d,i+(l==a?" ace_start":"")+" ace_br"+e(l==a||l==a+1&&n.start.column,c<h,h>p,l==f),s,l==f?0:1,o)},this.drawMultiLineMarker=function(e,t,n,r,i){var s=this.$padding,o=r.lineHeight,u=this.$getTop(t.start.row,r),a=s+t.start.column*r.characterWidth;i=i||"",e.push("<div class='",n," ace_br1 ace_start' style='","height:",o,"px;","right:0;","top:",u,"px;","left:",a,"px;",i,"'></div>"),u=this.$getTop(t.end.row,r);var f=t.end.column*r.characterWidth;e.push("<div class='",n," ace_br12' style='","height:",o,"px;","width:",f,"px;","top:",u,"px;","left:",s,"px;",i,"'></div>"),o=(t.end.row-t.start.row-1)*r.lineHeight;if(o<=0)return;u=this.$getTop(t.start.row+1,r);var l=(t.start.column?1:0)|(t.end.column?0:8);e.push("<div class='",n,l?" ace_br"+l:"","' style='","height:",o,"px;","right:0;","top:",u,"px;","left:",s,"px;",i,"'></div>")},this.drawSingleLineMarker=function(e,t,n,r,i,s){var o=r.lineHeight,u=(t.end.column+(i||0)-t.start.column)*r.characterWidth,a=this.$getTop(t.start.row,r),f=this.$padding+t.start.column*r.characterWidth;e.push("<div class='",n,"' style='","height:",o,"px;","width:",u,"px;","top:",a,"px;","left:",f,"px;",s||"","'></div>")},this.drawFullLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;t.start.row!=t.end.row&&(o+=this.$getTop(t.end.row,r)-s),e.push("<div class='",n,"' style='","height:",o,"px;","top:",s,"px;","left:0;right:0;",i||"","'></div>")},this.drawScreenLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;e.push("<div class='",n,"' style='","height:",o,"px;","top:",s,"px;","left:0;right:0;",i||"","'></div>")}}).call(s.prototype),t.Marker=s}),define("ace/layer/text",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/useragent","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../lib/dom"),s=e("../lib/lang"),o=e("../lib/useragent"),u=e("../lib/event_emitter").EventEmitter,a=function(e){this.element=i.createElement("div"),this.element.className="ace_layer ace_text-layer",e.appendChild(this.element),this.$updateEolChar=this.$updateEolChar.bind(this)};(function(){r.implement(this,u),this.EOF_CHAR="\u00b6",this.EOL_CHAR_LF="\u00ac",this.EOL_CHAR_CRLF="\u00a4",this.EOL_CHAR=this.EOL_CHAR_LF,this.TAB_CHAR="\u2014",this.SPACE_CHAR="\u00b7",this.$padding=0,this.$updateEolChar=function(){var e=this.session.doc.getNewLineCharacter()=="\n"?this.EOL_CHAR_LF:this.EOL_CHAR_CRLF;if(this.EOL_CHAR!=e)return this.EOL_CHAR=e,!0},this.setPadding=function(e){this.$padding=e,this.element.style.padding="0 "+e+"px"},this.getLineHeight=function(){return this.$fontMetrics.$characterSize.height||0},this.getCharacterWidth=function(){return this.$fontMetrics.$characterSize.width||0},this.$setFontMetrics=function(e){this.$fontMetrics=e,this.$fontMetrics.on("changeCharacterSize",function(e){this._signal("changeCharacterSize",e)}.bind(this)),this.$pollSizeChanges()},this.checkForSizeChanges=function(){this.$fontMetrics.checkForSizeChanges()},this.$pollSizeChanges=function(){return this.$pollSizeChangesTimer=this.$fontMetrics.$pollSizeChanges()},this.setSession=function(e){this.session=e,e&&this.$computeTabString()},this.showInvisibles=!1,this.setShowInvisibles=function(e){return this.showInvisibles==e?!1:(this.showInvisibles=e,this.$computeTabString(),!0)},this.displayIndentGuides=!0,this.setDisplayIndentGuides=function(e){return this.displayIndentGuides==e?!1:(this.displayIndentGuides=e,this.$computeTabString(),!0)},this.$tabStrings=[],this.onChangeTabSize=this.$computeTabString=function(){var e=this.session.getTabSize();this.tabSize=e;var t=this.$tabStrings=[0];for(var n=1;n<e+1;n++)this.showInvisibles?t.push("<span class='ace_invisible ace_invisible_tab'>"+s.stringRepeat(this.TAB_CHAR,n)+"</span>"):t.push(s.stringRepeat(" ",n));if(this.displayIndentGuides){this.$indentGuideRe=/\s\S| \t|\t |\s$/;var r="ace_indent-guide",i="",o="";if(this.showInvisibles){r+=" ace_invisible",i=" ace_invisible_space",o=" ace_invisible_tab";var u=s.stringRepeat(this.SPACE_CHAR,this.tabSize),a=s.stringRepeat(this.TAB_CHAR,this.tabSize)}else var u=s.stringRepeat(" ",this.tabSize),a=u;this.$tabStrings[" "]="<span class='"+r+i+"'>"+u+"</span>",this.$tabStrings[" "]="<span class='"+r+o+"'>"+a+"</span>"}},this.updateLines=function(e,t,n){(this.config.lastRow!=e.lastRow||this.config.firstRow!=e.firstRow)&&this.scrollLines(e),this.config=e;var r=Math.max(t,e.firstRow),i=Math.min(n,e.lastRow),s=this.element.childNodes,o=0;for(var u=e.firstRow;u<r;u++){var a=this.session.getFoldLine(u);if(a){if(a.containsRow(r)){r=a.start.row;break}u=a.end.row}o++}var u=r,a=this.session.getNextFoldLine(u),f=a?a.start.row:Infinity;for(;;){u>f&&(u=a.end.row+1,a=this.session.getNextFoldLine(u,a),f=a?a.start.row:Infinity);if(u>i)break;var l=s[o++];if(l){var c=[];this.$renderLine(c,u,!this.$useLineGroups(),u==f?a:!1),l.style.height=e.lineHeight*this.session.getRowLength(u)+"px",l.innerHTML=c.join("")}u++}},this.scrollLines=function(e){var t=this.config;this.config=e;if(!t||t.lastRow<e.firstRow)return this.update(e);if(e.lastRow<t.firstRow)return this.update(e);var n=this.element;if(t.firstRow<e.firstRow)for(var r=this.session.getFoldedRowCount(t.firstRow,e.firstRow-1);r>0;r--)n.removeChild(n.firstChild);if(t.lastRow>e.lastRow)for(var r=this.session.getFoldedRowCount(e.lastRow+1,t.lastRow);r>0;r--)n.removeChild(n.lastChild);if(e.firstRow<t.firstRow){var i=this.$renderLinesFragment(e,e.firstRow,t.firstRow-1);n.firstChild?n.insertBefore(i,n.firstChild):n.appendChild(i)}if(e.lastRow>t.lastRow){var i=this.$renderLinesFragment(e,t.lastRow+1,e.lastRow);n.appendChild(i)}},this.$renderLinesFragment=function(e,t,n){var r=this.element.ownerDocument.createDocumentFragment(),s=t,o=this.session.getNextFoldLine(s),u=o?o.start.row:Infinity;for(;;){s>u&&(s=o.end.row+1,o=this.session.getNextFoldLine(s,o),u=o?o.start.row:Infinity);if(s>n)break;var a=i.createElement("div"),f=[];this.$renderLine(f,s,!1,s==u?o:!1),a.innerHTML=f.join("");if(this.$useLineGroups())a.className="ace_line_group",r.appendChild(a),a.style.height=e.lineHeight*this.session.getRowLength(s)+"px";else while(a.firstChild)r.appendChild(a.firstChild);s++}return r},this.update=function(e){this.config=e;var t=[],n=e.firstRow,r=e.lastRow,i=n,s=this.session.getNextFoldLine(i),o=s?s.start.row:Infinity;for(;;){i>o&&(i=s.end.row+1,s=this.session.getNextFoldLine(i,s),o=s?s.start.row:Infinity);if(i>r)break;this.$useLineGroups()&&t.push("<div class='ace_line_group' style='height:",e.lineHeight*this.session.getRowLength(i),"px'>"),this.$renderLine(t,i,!1,i==o?s:!1),this.$useLineGroups()&&t.push("</div>"),i++}this.element.innerHTML=t.join("")},this.$textToken={text:!0,rparen:!0,lparen:!0},this.$renderToken=function(e,t,n,r){var i=this,o=/\t|&|<|>|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\u3000\uFEFF\uFFF9-\uFFFC])|[\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]/g,u=function(e,n,r,o,u){if(n)return i.showInvisibles?"<span class='ace_invisible ace_invisible_space'>"+s.stringRepeat(i.SPACE_CHAR,e.length)+"</span>":e;if(e=="&")return"&";if(e=="<")return"<";if(e==">")return">";if(e==" "){var a=i.session.getScreenTabSize(t+o);return t+=a-1,i.$tabStrings[a]}if(e=="\u3000"){var f=i.showInvisibles?"ace_cjk ace_invisible ace_invisible_space":"ace_cjk",l=i.showInvisibles?i.SPACE_CHAR:"";return t+=1,"<span class='"+f+"' style='width:"+i.config.characterWidth*2+"px'>"+l+"</span>"}return r?"<span class='ace_invisible ace_invisible_space ace_invalid'>"+i.SPACE_CHAR+"</span>":(t+=1,"<span class='ace_cjk' style='width:"+i.config.characterWidth*2+"px'>"+e+"</span>")},a=r.replace(o,u);if(!this.$textToken[n.type]){var f="ace_"+n.type.replace(/\./g," ace_"),l="";n.type=="fold"&&(l=" style='width:"+n.value.length*this.config.characterWidth+"px;' "),e.push("<span class='",f,"'",l,">",a,"</span>")}else e.push(a);return t+r.length},this.renderIndentGuide=function(e,t,n){var r=t.search(this.$indentGuideRe);return r<=0||r>=n?t:t[0]==" "?(r-=r%this.tabSize,e.push(s.stringRepeat(this.$tabStrings[" "],r/this.tabSize)),t.substr(r)):t[0]==" "?(e.push(s.stringRepeat(this.$tabStrings[" "],r)),t.substr(r)):t},this.$renderWrappedLine=function(e,t,n,r){var i=0,o=0,u=n[0],a=0;for(var f=0;f<t.length;f++){var l=t[f],c=l.value;if(f==0&&this.displayIndentGuides){i=c.length,c=this.renderIndentGuide(e,c,u);if(!c)continue;i-=c.length}if(i+c.length<u)a=this.$renderToken(e,a,l,c),i+=c.length;else{while(i+c.length>=u)a=this.$renderToken(e,a,l,c.substring(0,u-i)),c=c.substring(u-i),i=u,r||e.push("</div>","<div class='ace_line' style='height:",this.config.lineHeight,"px'>"),e.push(s.stringRepeat("\u00a0",n.indent)),o++,a=0,u=n[o]||Number.MAX_VALUE;c.length!=0&&(i+=c.length,a=this.$renderToken(e,a,l,c))}}},this.$renderSimpleLine=function(e,t){var n=0,r=t[0],i=r.value;this.displayIndentGuides&&(i=this.renderIndentGuide(e,i)),i&&(n=this.$renderToken(e,n,r,i));for(var s=1;s<t.length;s++)r=t[s],i=r.value,n=this.$renderToken(e,n,r,i)},this.$renderLine=function(e,t,n,r){!r&&r!=0&&(r=this.session.getFoldLine(t));if(r)var i=this.$getFoldLineTokens(t,r);else var i=this.session.getTokens(t);n||e.push("<div class='ace_line' style='height:",this.config.lineHeight*(this.$useLineGroups()?1:this.session.getRowLength(t)),"px'>");if(i.length){var s=this.session.getRowSplitData(t);s&&s.length?this.$renderWrappedLine(e,i,s,n):this.$renderSimpleLine(e,i)}this.showInvisibles&&(r&&(t=r.end.row),e.push("<span class='ace_invisible ace_invisible_eol'>",t==this.session.getLength()-1?this.EOF_CHAR:this.EOL_CHAR,"</span>")),n||e.push("</div>")},this.$getFoldLineTokens=function(e,t){function i(e,t,n){var i=0,s=0;while(s+e[i].value.length<t){s+=e[i].value.length,i++;if(i==e.length)return}if(s!=t){var o=e[i].value.substring(t-s);o.length>n-t&&(o=o.substring(0,n-t)),r.push({type:e[i].type,value:o}),s=t+o.length,i+=1}while(s<n&&i<e.length){var o=e[i].value;o.length+s>n?r.push({type:e[i].type,value:o.substring(0,n-s)}):r.push(e[i]),s+=o.length,i+=1}}var n=this.session,r=[],s=n.getTokens(e);return t.walk(function(e,t,o,u,a){e!=null?r.push({type:"fold",value:e}):(a&&(s=n.getTokens(t)),s.length&&i(s,u,o))},t.end.row,this.session.getLine(t.end.row).length),r},this.$useLineGroups=function(){return this.session.getUseWrapMode()},this.destroy=function(){clearInterval(this.$pollSizeChangesTimer),this.$measureNode&&this.$measureNode.parentNode.removeChild(this.$measureNode),delete this.$measureNode}}).call(a.prototype),t.Text=a}),define("ace/layer/cursor",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../lib/dom"),i,s=function(e){this.element=r.createElement("div"),this.element.className="ace_layer ace_cursor-layer",e.appendChild(this.element),i===undefined&&(i=!("opacity"in this.element.style)),this.isVisible=!1,this.isBlinking=!0,this.blinkInterval=1e3,this.smoothBlinking=!1,this.cursors=[],this.cursor=this.addCursor(),r.addCssClass(this.element,"ace_hidden-cursors"),this.$updateCursors=(i?this.$updateVisibility:this.$updateOpacity).bind(this)};(function(){this.$updateVisibility=function(e){var t=this.cursors;for(var n=t.length;n--;)t[n].style.visibility=e?"":"hidden"},this.$updateOpacity=function(e){var t=this.cursors;for(var n=t.length;n--;)t[n].style.opacity=e?"":"0"},this.$padding=0,this.setPadding=function(e){this.$padding=e},this.setSession=function(e){this.session=e},this.setBlinking=function(e){e!=this.isBlinking&&(this.isBlinking=e,this.restartTimer())},this.setBlinkInterval=function(e){e!=this.blinkInterval&&(this.blinkInterval=e,this.restartTimer())},this.setSmoothBlinking=function(e){e!=this.smoothBlinking&&!i&&(this.smoothBlinking=e,r.setCssClass(this.element,"ace_smooth-blinking",e),this.$updateCursors(!0),this.$updateCursors=this.$updateOpacity.bind(this),this.restartTimer())},this.addCursor=function(){var e=r.createElement("div");return e.className="ace_cursor",this.element.appendChild(e),this.cursors.push(e),e},this.removeCursor=function(){if(this.cursors.length>1){var e=this.cursors.pop();return e.parentNode.removeChild(e),e}},this.hideCursor=function(){this.isVisible=!1,r.addCssClass(this.element,"ace_hidden-cursors"),this.restartTimer()},this.showCursor=function(){this.isVisible=!0,r.removeCssClass(this.element,"ace_hidden-cursors"),this.restartTimer()},this.restartTimer=function(){var e=this.$updateCursors;clearInterval(this.intervalId),clearTimeout(this.timeoutId),this.smoothBlinking&&r.removeCssClass(this.element,"ace_smooth-blinking"),e(!0);if(!this.isBlinking||!this.blinkInterval||!this.isVisible)return;this.smoothBlinking&&setTimeout(function(){r.addCssClass(this.element,"ace_smooth-blinking")}.bind(this));var t=function(){this.timeoutId=setTimeout(function(){e(!1)},.6*this.blinkInterval)}.bind(this);this.intervalId=setInterval(function(){e(!0),t()},this.blinkInterval),t()},this.getPixelPosition=function(e,t){if(!this.config||!this.session)return{left:0,top:0};e||(e=this.session.selection.getCursor());var n=this.session.documentToScreenPosition(e),r=this.$padding+n.column*this.config.characterWidth,i=(n.row-(t?this.config.firstRowScreen:0))*this.config.lineHeight;return{left:r,top:i}},this.update=function(e){this.config=e;var t=this.session.$selectionMarkers,n=0,r=0;if(t===undefined||t.length===0)t=[{cursor:null}];for(var n=0,i=t.length;n<i;n++){var s=this.getPixelPosition(t[n].cursor,!0);if((s.top>e.height+e.offset||s.top<0)&&n>1)continue;var o=(this.cursors[r++]||this.addCursor()).style;this.drawCursor?this.drawCursor(o,s,e,t[n],this.session):(o.left=s.left+"px",o.top=s.top+"px",o.width=e.characterWidth+"px",o.height=e.lineHeight+"px")}while(this.cursors.length>r)this.removeCursor();var u=this.session.getOverwrite();this.$setOverwrite(u),this.$pixelPos=s,this.restartTimer()},this.drawCursor=null,this.$setOverwrite=function(e){e!=this.overwrite&&(this.overwrite=e,e?r.addCssClass(this.element,"ace_overwrite-cursors"):r.removeCssClass(this.element,"ace_overwrite-cursors"))},this.destroy=function(){clearInterval(this.intervalId),clearTimeout(this.timeoutId)}}).call(s.prototype),t.Cursor=s}),define("ace/scrollbar",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/event","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./lib/event"),o=e("./lib/event_emitter").EventEmitter,u=function(e){this.element=i.createElement("div"),this.element.className="ace_scrollbar ace_scrollbar"+this.classSuffix,this.inner=i.createElement("div"),this.inner.className="ace_scrollbar-inner",this.element.appendChild(this.inner),e.appendChild(this.element),this.setVisible(!1),this.skipEvent=!1,s.addListener(this.element,"scroll",this.onScroll.bind(this)),s.addListener(this.element,"mousedown",s.preventDefault)};(function(){r.implement(this,o),this.setVisible=function(e){this.element.style.display=e?"":"none",this.isVisible=e}}).call(u.prototype);var a=function(e,t){u.call(this,e),this.scrollTop=0,t.$scrollbarWidth=this.width=i.scrollbarWidth(e.ownerDocument),this.inner.style.width=this.element.style.width=(this.width||15)+5+"px"};r.inherits(a,u),function(){this.classSuffix="-v",this.onScroll=function(){this.skipEvent||(this.scrollTop=this.element.scrollTop,this._emit("scroll",{data:this.scrollTop})),this.skipEvent=!1},this.getWidth=function(){return this.isVisible?this.width:0},this.setHeight=function(e){this.element.style.height=e+"px"},this.setInnerHeight=function(e){this.inner.style.height=e+"px"},this.setScrollHeight=function(e){this.inner.style.height=e+"px"},this.setScrollTop=function(e){this.scrollTop!=e&&(this.skipEvent=!0,this.scrollTop=this.element.scrollTop=e)}}.call(a.prototype);var f=function(e,t){u.call(this,e),this.scrollLeft=0,this.height=t.$scrollbarWidth,this.inner.style.height=this.element.style.height=(this.height||15)+5+"px"};r.inherits(f,u),function(){this.classSuffix="-h",this.onScroll=function(){this.skipEvent||(this.scrollLeft=this.element.scrollLeft,this._emit("scroll",{data:this.scrollLeft})),this.skipEvent=!1},this.getHeight=function(){return this.isVisible?this.height:0},this.setWidth=function(e){this.element.style.width=e+"px"},this.setInnerWidth=function(e){this.inner.style.width=e+"px"},this.setScrollWidth=function(e){this.inner.style.width=e+"px"},this.setScrollLeft=function(e){this.scrollLeft!=e&&(this.skipEvent=!0,this.scrollLeft=this.element.scrollLeft=e)}}.call(f.prototype),t.ScrollBar=a,t.ScrollBarV=a,t.ScrollBarH=f,t.VScrollBar=a,t.HScrollBar=f}),define("ace/renderloop",["require","exports","module","ace/lib/event"],function(e,t,n){"use strict";var r=e("./lib/event"),i=function(e,t){this.onRender=e,this.pending=!1,this.changes=0,this.window=t||window};(function(){this.schedule=function(e){this.changes=this.changes|e;if(!this.pending&&this.changes){this.pending=!0;var t=this;r.nextFrame(function(){t.pending=!1;var e;while(e=t.changes)t.changes=0,t.onRender(e)},this.window)}}}).call(i.prototype),t.RenderLoop=i}),define("ace/layer/font_metrics",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/useragent","ace/lib/event_emitter"],function(e,t,n){var r=e("../lib/oop"),i=e("../lib/dom"),s=e("../lib/lang"),o=e("../lib/useragent"),u=e("../lib/event_emitter").EventEmitter,a=0,f=t.FontMetrics=function(e){this.el=i.createElement("div"),this.$setMeasureNodeStyles(this.el.style,!0),this.$main=i.createElement("div"),this.$setMeasureNodeStyles(this.$main.style),this.$measureNode=i.createElement("div"),this.$setMeasureNodeStyles(this.$measureNode.style),this.el.appendChild(this.$main),this.el.appendChild(this.$measureNode),e.appendChild(this.el),a||this.$testFractionalRect(),this.$measureNode.innerHTML=s.stringRepeat("X",a),this.$characterSize={width:0,height:0},this.checkForSizeChanges()};(function(){r.implement(this,u),this.$characterSize={width:0,height:0},this.$testFractionalRect=function(){var e=i.createElement("div");this.$setMeasureNodeStyles(e.style),e.style.width="0.2px",document.documentElement.appendChild(e);var t=e.getBoundingClientRect().width;t>0&&t<1?a=50:a=100,e.parentNode.removeChild(e)},this.$setMeasureNodeStyles=function(e,t){e.width=e.height="auto",e.left=e.top="0px",e.visibility="hidden",e.position="absolute",e.whiteSpace="pre",o.isIE<8?e["font-family"]="inherit":e.font="inherit",e.overflow=t?"hidden":"visible"},this.checkForSizeChanges=function(){var e=this.$measureSizes();if(e&&(this.$characterSize.width!==e.width||this.$characterSize.height!==e.height)){this.$measureNode.style.fontWeight="bold";var t=this.$measureSizes();this.$measureNode.style.fontWeight="",this.$characterSize=e,this.charSizes=Object.create(null),this.allowBoldFonts=t&&t.width===e.width&&t.height===e.height,this._emit("changeCharacterSize",{data:e})}},this.$pollSizeChanges=function(){if(this.$pollSizeChangesTimer)return this.$pollSizeChangesTimer;var e=this;return this.$pollSizeChangesTimer=setInterval(function(){e.checkForSizeChanges()},500)},this.setPolling=function(e){e?this.$pollSizeChanges():this.$pollSizeChangesTimer&&(clearInterval(this.$pollSizeChangesTimer),this.$pollSizeChangesTimer=0)},this.$measureSizes=function(){if(a===50){var e=null;try{e=this.$measureNode.getBoundingClientRect()}catch(t){e={width:0,height:0}}var n={height:e.height,width:e.width/a}}else var n={height:this.$measureNode.clientHeight,width:this.$measureNode.clientWidth/a};return n.width===0||n.height===0?null:n},this.$measureCharWidth=function(e){this.$main.innerHTML=s.stringRepeat(e,a);var t=this.$main.getBoundingClientRect();return t.width/a},this.getCharacterWidth=function(e){var t=this.charSizes[e];return t===undefined&&(t=this.charSizes[e]=this.$measureCharWidth(e)/this.$characterSize.width),t},this.destroy=function(){clearInterval(this.$pollSizeChangesTimer),this.el&&this.el.parentNode&&this.el.parentNode.removeChild(this.el)}}).call(f.prototype)}),define("ace/virtual_renderer",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/config","ace/lib/useragent","ace/layer/gutter","ace/layer/marker","ace/layer/text","ace/layer/cursor","ace/scrollbar","ace/scrollbar","ace/renderloop","ace/layer/font_metrics","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./config"),o=e("./lib/useragent"),u=e("./layer/gutter").Gutter,a=e("./layer/marker").Marker,f=e("./layer/text").Text,l=e("./layer/cursor").Cursor,c=e("./scrollbar").HScrollBar,h=e("./scrollbar").VScrollBar,p=e("./renderloop").RenderLoop,d=e("./layer/font_metrics").FontMetrics,v=e("./lib/event_emitter").EventEmitter,m='.ace_editor {position: relative;overflow: hidden;font: 12px/normal \'Monaco\', \'Menlo\', \'Ubuntu Mono\', \'Consolas\', \'source-code-pro\', monospace;direction: ltr;}.ace_scroller {position: absolute;overflow: hidden;top: 0;bottom: 0;background-color: inherit;-ms-user-select: none;-moz-user-select: none;-webkit-user-select: none;user-select: none;cursor: text;}.ace_content {position: absolute;-moz-box-sizing: border-box;-webkit-box-sizing: border-box;box-sizing: border-box;min-width: 100%;}.ace_dragging .ace_scroller:before{position: absolute;top: 0;left: 0;right: 0;bottom: 0;content: \'\';background: rgba(250, 250, 250, 0.01);z-index: 1000;}.ace_dragging.ace_dark .ace_scroller:before{background: rgba(0, 0, 0, 0.01);}.ace_selecting, .ace_selecting * {cursor: text !important;}.ace_gutter {position: absolute;overflow : hidden;width: auto;top: 0;bottom: 0;left: 0;cursor: default;z-index: 4;-ms-user-select: none;-moz-user-select: none;-webkit-user-select: none;user-select: none;}.ace_gutter-active-line {position: absolute;left: 0;right: 0;}.ace_scroller.ace_scroll-left {box-shadow: 17px 0 16px -16px rgba(0, 0, 0, 0.4) inset;}.ace_gutter-cell {padding-left: 19px;padding-right: 6px;background-repeat: no-repeat;}.ace_gutter-cell.ace_error {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABOFBMVEX/////////QRswFAb/Ui4wFAYwFAYwFAaWGAfDRymzOSH/PxswFAb/SiUwFAYwFAbUPRvjQiDllog5HhHdRybsTi3/Tyv9Tir+Syj/UC3////XurebMBIwFAb/RSHbPx/gUzfdwL3kzMivKBAwFAbbvbnhPx66NhowFAYwFAaZJg8wFAaxKBDZurf/RB6mMxb/SCMwFAYwFAbxQB3+RB4wFAb/Qhy4Oh+4QifbNRcwFAYwFAYwFAb/QRzdNhgwFAYwFAbav7v/Uy7oaE68MBK5LxLewr/r2NXewLswFAaxJw4wFAbkPRy2PyYwFAaxKhLm1tMwFAazPiQwFAaUGAb/QBrfOx3bvrv/VC/maE4wFAbRPBq6MRO8Qynew8Dp2tjfwb0wFAbx6eju5+by6uns4uH9/f36+vr/GkHjAAAAYnRSTlMAGt+64rnWu/bo8eAA4InH3+DwoN7j4eLi4xP99Nfg4+b+/u9B/eDs1MD1mO7+4PHg2MXa347g7vDizMLN4eG+Pv7i5evs/v79yu7S3/DV7/498Yv24eH+4ufQ3Ozu/v7+y13sRqwAAADLSURBVHjaZc/XDsFgGIBhtDrshlitmk2IrbHFqL2pvXf/+78DPokj7+Fz9qpU/9UXJIlhmPaTaQ6QPaz0mm+5gwkgovcV6GZzd5JtCQwgsxoHOvJO15kleRLAnMgHFIESUEPmawB9ngmelTtipwwfASilxOLyiV5UVUyVAfbG0cCPHig+GBkzAENHS0AstVF6bacZIOzgLmxsHbt2OecNgJC83JERmePUYq8ARGkJx6XtFsdddBQgZE2nPR6CICZhawjA4Fb/chv+399kfR+MMMDGOQAAAABJRU5ErkJggg==");background-repeat: no-repeat;background-position: 2px center;}.ace_gutter-cell.ace_warning {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAmVBMVEX///8AAAD///8AAAAAAABPSzb/5sAAAAB/blH/73z/ulkAAAAAAAD85pkAAAAAAAACAgP/vGz/rkDerGbGrV7/pkQICAf////e0IsAAAD/oED/qTvhrnUAAAD/yHD/njcAAADuv2r/nz//oTj/p064oGf/zHAAAAA9Nir/tFIAAAD/tlTiuWf/tkIAAACynXEAAAAAAAAtIRW7zBpBAAAAM3RSTlMAABR1m7RXO8Ln31Z36zT+neXe5OzooRDfn+TZ4p3h2hTf4t3k3ucyrN1K5+Xaks52Sfs9CXgrAAAAjklEQVR42o3PbQ+CIBQFYEwboPhSYgoYunIqqLn6/z8uYdH8Vmdnu9vz4WwXgN/xTPRD2+sgOcZjsge/whXZgUaYYvT8QnuJaUrjrHUQreGczuEafQCO/SJTufTbroWsPgsllVhq3wJEk2jUSzX3CUEDJC84707djRc5MTAQxoLgupWRwW6UB5fS++NV8AbOZgnsC7BpEAAAAABJRU5ErkJggg==");background-position: 2px center;}.ace_gutter-cell.ace_info {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAJ0Uk5TAAB2k804AAAAPklEQVQY02NgIB68QuO3tiLznjAwpKTgNyDbMegwisCHZUETUZV0ZqOquBpXj2rtnpSJT1AEnnRmL2OgGgAAIKkRQap2htgAAAAASUVORK5CYII=");background-position: 2px center;}.ace_dark .ace_gutter-cell.ace_info {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJFBMVEUAAAChoaGAgIAqKiq+vr6tra1ZWVmUlJSbm5s8PDxubm56enrdgzg3AAAAAXRSTlMAQObYZgAAAClJREFUeNpjYMAPdsMYHegyJZFQBlsUlMFVCWUYKkAZMxZAGdxlDMQBAG+TBP4B6RyJAAAAAElFTkSuQmCC");}.ace_scrollbar {position: absolute;right: 0;bottom: 0;z-index: 6;}.ace_scrollbar-inner {position: absolute;cursor: text;left: 0;top: 0;}.ace_scrollbar-v{overflow-x: hidden;overflow-y: scroll;top: 0;}.ace_scrollbar-h {overflow-x: scroll;overflow-y: hidden;left: 0;}.ace_print-margin {position: absolute;height: 100%;}.ace_text-input {position: absolute;z-index: 0;width: 0.5em;height: 1em;opacity: 0;background: transparent;-moz-appearance: none;appearance: none;border: none;resize: none;outline: none;overflow: hidden;font: inherit;padding: 0 1px;margin: 0 -1px;text-indent: -1em;-ms-user-select: text;-moz-user-select: text;-webkit-user-select: text;user-select: text;white-space: pre!important;}.ace_text-input.ace_composition {background: inherit;color: inherit;z-index: 1000;opacity: 1;text-indent: 0;}.ace_layer {z-index: 1;position: absolute;overflow: hidden;word-wrap: normal;white-space: pre;height: 100%;width: 100%;-moz-box-sizing: border-box;-webkit-box-sizing: border-box;box-sizing: border-box;pointer-events: none;}.ace_gutter-layer {position: relative;width: auto;text-align: right;pointer-events: auto;}.ace_text-layer {font: inherit !important;}.ace_cjk {display: inline-block;text-align: center;}.ace_cursor-layer {z-index: 4;}.ace_cursor {z-index: 4;position: absolute;-moz-box-sizing: border-box;-webkit-box-sizing: border-box;box-sizing: border-box;border-left: 2px solid;transform: translatez(0);}.ace_slim-cursors .ace_cursor {border-left-width: 1px;}.ace_overwrite-cursors .ace_cursor {border-left-width: 0;border-bottom: 1px solid;}.ace_hidden-cursors .ace_cursor {opacity: 0.2;}.ace_smooth-blinking .ace_cursor {-webkit-transition: opacity 0.18s;transition: opacity 0.18s;}.ace_editor.ace_multiselect .ace_cursor {border-left-width: 1px;}.ace_marker-layer .ace_step, .ace_marker-layer .ace_stack {position: absolute;z-index: 3;}.ace_marker-layer .ace_selection {position: absolute;z-index: 5;}.ace_marker-layer .ace_bracket {position: absolute;z-index: 6;}.ace_marker-layer .ace_active-line {position: absolute;z-index: 2;}.ace_marker-layer .ace_selected-word {position: absolute;z-index: 4;-moz-box-sizing: border-box;-webkit-box-sizing: border-box;box-sizing: border-box;}.ace_line .ace_fold {-moz-box-sizing: border-box;-webkit-box-sizing: border-box;box-sizing: border-box;display: inline-block;height: 11px;margin-top: -2px;vertical-align: middle;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACJJREFUeNpi+P//fxgTAwPDBxDxD078RSX+YeEyDFMCIMAAI3INmXiwf2YAAAAASUVORK5CYII=");background-repeat: no-repeat, repeat-x;background-position: center center, top left;color: transparent;border: 1px solid black;border-radius: 2px;cursor: pointer;pointer-events: auto;}.ace_dark .ace_fold {}.ace_fold:hover{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACBJREFUeNpi+P//fz4TAwPDZxDxD5X4i5fLMEwJgAADAEPVDbjNw87ZAAAAAElFTkSuQmCC");}.ace_tooltip {background-color: #FFF;background-image: -webkit-linear-gradient(top, transparent, rgba(0, 0, 0, 0.1));background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));border: 1px solid gray;border-radius: 1px;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);color: black;max-width: 100%;padding: 3px 4px;position: fixed;z-index: 999999;-moz-box-sizing: border-box;-webkit-box-sizing: border-box;box-sizing: border-box;cursor: default;white-space: pre;word-wrap: break-word;line-height: normal;font-style: normal;font-weight: normal;letter-spacing: normal;pointer-events: none;}.ace_folding-enabled > .ace_gutter-cell {padding-right: 13px;}.ace_fold-widget {-moz-box-sizing: border-box;-webkit-box-sizing: border-box;box-sizing: border-box;margin: 0 -12px 0 1px;display: none;width: 11px;vertical-align: top;background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42mWKsQ0AMAzC8ixLlrzQjzmBiEjp0A6WwBCSPgKAXoLkqSot7nN3yMwR7pZ32NzpKkVoDBUxKAAAAABJRU5ErkJggg==");background-repeat: no-repeat;background-position: center;border-radius: 3px;border: 1px solid transparent;cursor: pointer;}.ace_folding-enabled .ace_fold-widget {display: inline-block; }.ace_fold-widget.ace_end {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42m3HwQkAMAhD0YzsRchFKI7sAikeWkrxwScEB0nh5e7KTPWimZki4tYfVbX+MNl4pyZXejUO1QAAAABJRU5ErkJggg==");}.ace_fold-widget.ace_closed {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAGCAYAAAAG5SQMAAAAOUlEQVR42jXKwQkAMAgDwKwqKD4EwQ26sSOkVWjgIIHAzPiCgaqiqnJHZnKICBERHN194O5b9vbLuAVRL+l0YWnZAAAAAElFTkSuQmCCXA==");}.ace_fold-widget:hover {border: 1px solid rgba(0, 0, 0, 0.3);background-color: rgba(255, 255, 255, 0.2);box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);}.ace_fold-widget:active {border: 1px solid rgba(0, 0, 0, 0.4);background-color: rgba(0, 0, 0, 0.05);box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);}.ace_dark .ace_fold-widget {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHklEQVQIW2P4//8/AzoGEQ7oGCaLLAhWiSwB146BAQCSTPYocqT0AAAAAElFTkSuQmCC");}.ace_dark .ace_fold-widget.ace_end {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAH0lEQVQIW2P4//8/AxQ7wNjIAjDMgC4AxjCVKBirIAAF0kz2rlhxpAAAAABJRU5ErkJggg==");}.ace_dark .ace_fold-widget.ace_closed {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAHElEQVQIW2P4//+/AxAzgDADlOOAznHAKgPWAwARji8UIDTfQQAAAABJRU5ErkJggg==");}.ace_dark .ace_fold-widget:hover {box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);background-color: rgba(255, 255, 255, 0.1);}.ace_dark .ace_fold-widget:active {box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);}.ace_fold-widget.ace_invalid {background-color: #FFB4B4;border-color: #DE5555;}.ace_fade-fold-widgets .ace_fold-widget {-webkit-transition: opacity 0.4s ease 0.05s;transition: opacity 0.4s ease 0.05s;opacity: 0;}.ace_fade-fold-widgets:hover .ace_fold-widget {-webkit-transition: opacity 0.05s ease 0.05s;transition: opacity 0.05s ease 0.05s;opacity:1;}.ace_underline {text-decoration: underline;}.ace_bold {font-weight: bold;}.ace_nobold .ace_bold {font-weight: normal;}.ace_italic {font-style: italic;}.ace_error-marker {background-color: rgba(255, 0, 0,0.2);position: absolute;z-index: 9;}.ace_highlight-marker {background-color: rgba(255, 255, 0,0.2);position: absolute;z-index: 8;}.ace_br1 {border-top-left-radius : 3px;}.ace_br2 {border-top-right-radius : 3px;}.ace_br3 {border-top-left-radius : 3px; border-top-right-radius: 3px;}.ace_br4 {border-bottom-right-radius: 3px;}.ace_br5 {border-top-left-radius : 3px; border-bottom-right-radius: 3px;}.ace_br6 {border-top-right-radius : 3px; border-bottom-right-radius: 3px;}.ace_br7 {border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px;}.ace_br8 {border-bottom-left-radius : 3px;}.ace_br9 {border-top-left-radius : 3px; border-bottom-left-radius: 3px;}.ace_br10{border-top-right-radius : 3px; border-bottom-left-radius: 3px;}.ace_br11{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br12{border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br13{border-top-left-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br14{border-top-right-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br15{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}';i.importCssString(m,"ace_editor.css");var g=function(e,t){var n=this;this.container=e||i.createElement("div"),this.$keepTextAreaAtCursor=!o.isOldIE,i.addCssClass(this.container,"ace_editor"),this.setTheme(t),this.$gutter=i.createElement("div"),this.$gutter.className="ace_gutter",this.container.appendChild(this.$gutter),this.scroller=i.createElement("div"),this.scroller.className="ace_scroller",this.container.appendChild(this.scroller),this.content=i.createElement("div"),this.content.className="ace_content",this.scroller.appendChild(this.content),this.$gutterLayer=new u(this.$gutter),this.$gutterLayer.on("changeGutterWidth",this.onGutterResize.bind(this)),this.$markerBack=new a(this.content);var r=this.$textLayer=new f(this.content);this.canvas=r.element,this.$markerFront=new a(this.content),this.$cursorLayer=new l(this.content),this.$horizScroll=!1,this.$vScroll=!1,this.scrollBar=this.scrollBarV=new h(this.container,this),this.scrollBarH=new c(this.container,this),this.scrollBarV.addEventListener("scroll",function(e){n.$scrollAnimation||n.session.setScrollTop(e.data-n.scrollMargin.top)}),this.scrollBarH.addEventListener("scroll",function(e){n.$scrollAnimation||n.session.setScrollLeft(e.data-n.scrollMargin.left)}),this.scrollTop=0,this.scrollLeft=0,this.cursorPos={row:0,column:0},this.$fontMetrics=new d(this.container),this.$textLayer.$setFontMetrics(this.$fontMetrics),this.$textLayer.addEventListener("changeCharacterSize",function(e){n.updateCharacterSize(),n.onResize(!0,n.gutterWidth,n.$size.width,n.$size.height),n._signal("changeCharacterSize",e)}),this.$size={width:0,height:0,scrollerHeight:0,scrollerWidth:0,$dirty:!0},this.layerConfig={width:1,padding:0,firstRow:0,firstRowScreen:0,lastRow:0,lineHeight:0,characterWidth:0,minHeight:1,maxHeight:1,offset:0,height:1,gutterOffset:1},this.scrollMargin={left:0,right:0,top:0,bottom:0,v:0,h:0},this.$loop=new p(this.$renderChanges.bind(this),this.container.ownerDocument.defaultView),this.$loop.schedule(this.CHANGE_FULL),this.updateCharacterSize(),this.setPadding(4),s.resetOptions(this),s._emit("renderer",this)};(function(){this.CHANGE_CURSOR=1,this.CHANGE_MARKER=2,this.CHANGE_GUTTER=4,this.CHANGE_SCROLL=8,this.CHANGE_LINES=16,this.CHANGE_TEXT=32,this.CHANGE_SIZE=64,this.CHANGE_MARKER_BACK=128,this.CHANGE_MARKER_FRONT=256,this.CHANGE_FULL=512,this.CHANGE_H_SCROLL=1024,r.implement(this,v),this.updateCharacterSize=function(){this.$textLayer.allowBoldFonts!=this.$allowBoldFonts&&(this.$allowBoldFonts=this.$textLayer.allowBoldFonts,this.setStyle("ace_nobold",!this.$allowBoldFonts)),this.layerConfig.characterWidth=this.characterWidth=this.$textLayer.getCharacterWidth(),this.layerConfig.lineHeight=this.lineHeight=this.$textLayer.getLineHeight(),this.$updatePrintMargin()},this.setSession=function(e){this.session&&this.session.doc.off("changeNewLineMode",this.onChangeNewLineMode),this.session=e,e&&this.scrollMargin.top&&e.getScrollTop()<=0&&e.setScrollTop(-this.scrollMargin.top),this.$cursorLayer.setSession(e),this.$markerBack.setSession(e),this.$markerFront.setSession(e),this.$gutterLayer.setSession(e),this.$textLayer.setSession(e);if(!e)return;this.$loop.schedule(this.CHANGE_FULL),this.session.$setFontMetrics(this.$fontMetrics),this.onChangeNewLineMode=this.onChangeNewLineMode.bind(this),this.onChangeNewLineMode(),this.session.doc.on("changeNewLineMode",this.onChangeNewLineMode)},this.updateLines=function(e,t,n){t===undefined&&(t=Infinity),this.$changedLines?(this.$changedLines.firstRow>e&&(this.$changedLines.firstRow=e),this.$changedLines.lastRow<t&&(this.$changedLines.lastRow=t)):this.$changedLines={firstRow:e,lastRow:t};if(this.$changedLines.lastRow<this.layerConfig.firstRow){if(!n)return;this.$changedLines.lastRow=this.layerConfig.lastRow}if(this.$changedLines.firstRow>this.layerConfig.lastRow)return;this.$loop.schedule(this.CHANGE_LINES)},this.onChangeNewLineMode=function(){this.$loop.schedule(this.CHANGE_TEXT),this.$textLayer.$updateEolChar()},this.onChangeTabSize=function(){this.$loop.schedule(this.CHANGE_TEXT|this.CHANGE_MARKER),this.$textLayer.onChangeTabSize()},this.updateText=function(){this.$loop.schedule(this.CHANGE_TEXT)},this.updateFull=function(e){e?this.$renderChanges(this.CHANGE_FULL,!0):this.$loop.schedule(this.CHANGE_FULL)},this.updateFontSize=function(){this.$textLayer.checkForSizeChanges()},this.$changes=0,this.$updateSizeAsync=function(){this.$loop.pending?this.$size.$dirty=!0:this.onResize()},this.onResize=function(e,t,n,r){if(this.resizing>2)return;this.resizing>0?this.resizing++:this.resizing=e?1:0;var i=this.container;r||(r=i.clientHeight||i.scrollHeight),n||(n=i.clientWidth||i.scrollWidth);var s=this.$updateCachedSize(e,t,n,r);if(!this.$size.scrollerHeight||!n&&!r)return this.resizing=0;e&&(this.$gutterLayer.$padding=null),e?this.$renderChanges(s|this.$changes,!0):this.$loop.schedule(s|this.$changes),this.resizing&&(this.resizing=0),this.scrollBarV.scrollLeft=this.scrollBarV.scrollTop=null},this.$updateCachedSize=function(e,t,n,r){r-=this.$extraHeight||0;var i=0,s=this.$size,o={width:s.width,height:s.height,scrollerHeight:s.scrollerHeight,scrollerWidth:s.scrollerWidth};r&&(e||s.height!=r)&&(s.height=r,i|=this.CHANGE_SIZE,s.scrollerHeight=s.height,this.$horizScroll&&(s.scrollerHeight-=this.scrollBarH.getHeight()),this.scrollBarV.element.style.bottom=this.scrollBarH.getHeight()+"px",i|=this.CHANGE_SCROLL);if(n&&(e||s.width!=n)){i|=this.CHANGE_SIZE,s.width=n,t==null&&(t=this.$showGutter?this.$gutter.offsetWidth:0),this.gutterWidth=t,this.scrollBarH.element.style.left=this.scroller.style.left=t+"px",s.scrollerWidth=Math.max(0,n-t-this.scrollBarV.getWidth()),this.scrollBarH.element.style.right=this.scroller.style.right=this.scrollBarV.getWidth()+"px",this.scroller.style.bottom=this.scrollBarH.getHeight()+"px";if(this.session&&this.session.getUseWrapMode()&&this.adjustWrapLimit()||e)i|=this.CHANGE_FULL}return s.$dirty=!n||!r,i&&this._signal("resize",o),i},this.onGutterResize=function(){var e=this.$showGutter?this.$gutter.offsetWidth:0;e!=this.gutterWidth&&(this.$changes|=this.$updateCachedSize(!0,e,this.$size.width,this.$size.height)),this.session.getUseWrapMode()&&this.adjustWrapLimit()?this.$loop.schedule(this.CHANGE_FULL):this.$size.$dirty?this.$loop.schedule(this.CHANGE_FULL):(this.$computeLayerConfig(),this.$loop.schedule(this.CHANGE_MARKER))},this.adjustWrapLimit=function(){var e=this.$size.scrollerWidth-this.$padding*2,t=Math.floor(e/this.characterWidth);return this.session.adjustWrapLimit(t,this.$showPrintMargin&&this.$printMarginColumn)},this.setAnimatedScroll=function(e){this.setOption("animatedScroll",e)},this.getAnimatedScroll=function(){return this.$animatedScroll},this.setShowInvisibles=function(e){this.setOption("showInvisibles",e)},this.getShowInvisibles=function(){return this.getOption("showInvisibles")},this.getDisplayIndentGuides=function(){return this.getOption("displayIndentGuides")},this.setDisplayIndentGuides=function(e){this.setOption("displayIndentGuides",e)},this.setShowPrintMargin=function(e){this.setOption("showPrintMargin",e)},this.getShowPrintMargin=function(){return this.getOption("showPrintMargin")},this.setPrintMarginColumn=function(e){this.setOption("printMarginColumn",e)},this.getPrintMarginColumn=function(){return this.getOption("printMarginColumn")},this.getShowGutter=function(){return this.getOption("showGutter")},this.setShowGutter=function(e){return this.setOption("showGutter",e)},this.getFadeFoldWidgets=function(){return this.getOption("fadeFoldWidgets")},this.setFadeFoldWidgets=function(e){this.setOption("fadeFoldWidgets",e)},this.setHighlightGutterLine=function(e){this.setOption("highlightGutterLine",e)},this.getHighlightGutterLine=function(){return this.getOption("highlightGutterLine")},this.$updateGutterLineHighlight=function(){var e=this.$cursorLayer.$pixelPos,t=this.layerConfig.lineHeight;if(this.session.getUseWrapMode()){var n=this.session.selection.getCursor();n.column=0,e=this.$cursorLayer.getPixelPosition(n,!0),t*=this.session.getRowLength(n.row)}this.$gutterLineHighlight.style.top=e.top-this.layerConfig.offset+"px",this.$gutterLineHighlight.style.height=t+"px"},this.$updatePrintMargin=function(){if(!this.$showPrintMargin&&!this.$printMarginEl)return;if(!this.$printMarginEl){var e=i.createElement("div");e.className="ace_layer ace_print-margin-layer",this.$printMarginEl=i.createElement("div"),this.$printMarginEl.className="ace_print-margin",e.appendChild(this.$printMarginEl),this.content.insertBefore(e,this.content.firstChild)}var t=this.$printMarginEl.style;t.left=this.characterWidth*this.$printMarginColumn+this.$padding+"px",t.visibility=this.$showPrintMargin?"visible":"hidden",this.session&&this.session.$wrap==-1&&this.adjustWrapLimit()},this.getContainerElement=function(){return this.container},this.getMouseEventTarget=function(){return this.scroller},this.getTextAreaContainer=function(){return this.container},this.$moveTextAreaToCursor=function(){if(!this.$keepTextAreaAtCursor)return;var e=this.layerConfig,t=this.$cursorLayer.$pixelPos.top,n=this.$cursorLayer.$pixelPos.left;t-=e.offset;var r=this.textarea.style,i=this.lineHeight;if(t<0||t>e.height-i){r.top=r.left="0";return}var s=this.characterWidth;if(this.$composition){var o=this.textarea.value.replace(/^\x01+/,"");s*=this.session.$getStringScreenWidth(o)[0]+2,i+=2}n-=this.scrollLeft,n>this.$size.scrollerWidth-s&&(n=this.$size.scrollerWidth-s),n+=this.gutterWidth,r.height=i+"px",r.width=s+"px",r.left=Math.min(n,this.$size.scrollerWidth-s)+"px",r.top=Math.min(t,this.$size.height-i)+"px"},this.getFirstVisibleRow=function(){return this.layerConfig.firstRow},this.getFirstFullyVisibleRow=function(){return this.layerConfig.firstRow+(this.layerConfig.offset===0?0:1)},this.getLastFullyVisibleRow=function(){var e=this.layerConfig,t=e.lastRow,n=this.session.documentToScreenRow(t,0)*e.lineHeight;return n-this.session.getScrollTop()>e.height-e.lineHeight?t-1:t},this.getLastVisibleRow=function(){return this.layerConfig.lastRow},this.$padding=null,this.setPadding=function(e){this.$padding=e,this.$textLayer.setPadding(e),this.$cursorLayer.setPadding(e),this.$markerFront.setPadding(e),this.$markerBack.setPadding(e),this.$loop.schedule(this.CHANGE_FULL),this.$updatePrintMargin()},this.setScrollMargin=function(e,t,n,r){var i=this.scrollMargin;i.top=e|0,i.bottom=t|0,i.right=r|0,i.left=n|0,i.v=i.top+i.bottom,i.h=i.left+i.right,i.top&&this.scrollTop<=0&&this.session&&this.session.setScrollTop(-i.top),this.updateFull()},this.getHScrollBarAlwaysVisible=function(){return this.$hScrollBarAlwaysVisible},this.setHScrollBarAlwaysVisible=function(e){this.setOption("hScrollBarAlwaysVisible",e)},this.getVScrollBarAlwaysVisible=function(){return this.$vScrollBarAlwaysVisible},this.setVScrollBarAlwaysVisible=function(e){this.setOption("vScrollBarAlwaysVisible",e)},this.$updateScrollBarV=function(){var e=this.layerConfig.maxHeight,t=this.$size.scrollerHeight;!this.$maxLines&&this.$scrollPastEnd&&(e-=(t-this.lineHeight)*this.$scrollPastEnd,this.scrollTop>e-t&&(e=this.scrollTop+t,this.scrollBarV.scrollTop=null)),this.scrollBarV.setScrollHeight(e+this.scrollMargin.v),this.scrollBarV.setScrollTop(this.scrollTop+this.scrollMargin.top)},this.$updateScrollBarH=function(){this.scrollBarH.setScrollWidth(this.layerConfig.width+2*this.$padding+this.scrollMargin.h),this.scrollBarH.setScrollLeft(this.scrollLeft+this.scrollMargin.left)},this.$frozen=!1,this.freeze=function(){this.$frozen=!0},this.unfreeze=function(){this.$frozen=!1},this.$renderChanges=function(e,t){this.$changes&&(e|=this.$changes,this.$changes=0);if(!this.session||!this.container.offsetWidth||this.$frozen||!e&&!t){this.$changes|=e;return}if(this.$size.$dirty)return this.$changes|=e,this.onResize(!0);this.lineHeight||this.$textLayer.checkForSizeChanges(),this._signal("beforeRender");var n=this.layerConfig;if(e&this.CHANGE_FULL||e&this.CHANGE_SIZE||e&this.CHANGE_TEXT||e&this.CHANGE_LINES||e&this.CHANGE_SCROLL||e&this.CHANGE_H_SCROLL){e|=this.$computeLayerConfig();if(n.firstRow!=this.layerConfig.firstRow&&n.firstRowScreen==this.layerConfig.firstRowScreen){var r=this.scrollTop+(n.firstRow-this.layerConfig.firstRow)*this.lineHeight;r>0&&(this.scrollTop=r,e|=this.CHANGE_SCROLL,e|=this.$computeLayerConfig())}n=this.layerConfig,this.$updateScrollBarV(),e&this.CHANGE_H_SCROLL&&this.$updateScrollBarH(),this.$gutterLayer.element.style.marginTop=-n.offset+"px",this.content.style.marginTop=-n.offset+"px",this.content.style.width=n.width+2*this.$padding+"px",this.content.style.height=n.minHeight+"px"}e&this.CHANGE_H_SCROLL&&(this.content.style.marginLeft=-this.scrollLeft+"px",this.scroller.className=this.scrollLeft<=0?"ace_scroller":"ace_scroller ace_scroll-left");if(e&this.CHANGE_FULL){this.$textLayer.update(n),this.$showGutter&&this.$gutterLayer.update(n),this.$markerBack.update(n),this.$markerFront.update(n),this.$cursorLayer.update(n),this.$moveTextAreaToCursor(),this.$highlightGutterLine&&this.$updateGutterLineHighlight(),this._signal("afterRender");return}if(e&this.CHANGE_SCROLL){e&this.CHANGE_TEXT||e&this.CHANGE_LINES?this.$textLayer.update(n):this.$textLayer.scrollLines(n),this.$showGutter&&this.$gutterLayer.update(n),this.$markerBack.update(n),this.$markerFront.update(n),this.$cursorLayer.update(n),this.$highlightGutterLine&&this.$updateGutterLineHighlight(),this.$moveTextAreaToCursor(),this._signal("afterRender");return}e&this.CHANGE_TEXT?(this.$textLayer.update(n),this.$showGutter&&this.$gutterLayer.update(n)):e&this.CHANGE_LINES?(this.$updateLines()||e&this.CHANGE_GUTTER&&this.$showGutter)&&this.$gutterLayer.update(n):(e&this.CHANGE_TEXT||e&this.CHANGE_GUTTER)&&this.$showGutter&&this.$gutterLayer.update(n),e&this.CHANGE_CURSOR&&(this.$cursorLayer.update(n),this.$moveTextAreaToCursor(),this.$highlightGutterLine&&this.$updateGutterLineHighlight()),e&(this.CHANGE_MARKER|this.CHANGE_MARKER_FRONT)&&this.$markerFront.update(n),e&(this.CHANGE_MARKER|this.CHANGE_MARKER_BACK)&&this.$markerBack.update(n),this._signal("afterRender")},this.$autosize=function(){var e=this.session.getScreenLength()*this.lineHeight,t=this.$maxLines*this.lineHeight,n=Math.min(t,Math.max((this.$minLines||1)*this.lineHeight,e))+this.scrollMargin.v+(this.$extraHeight||0);this.$horizScroll&&(n+=this.scrollBarH.getHeight()),this.$maxPixelHeight&&n>this.$maxPixelHeight&&(n=this.$maxPixelHeight);var r=e>t;if(n!=this.desiredHeight||this.$size.height!=this.desiredHeight||r!=this.$vScroll){r!=this.$vScroll&&(this.$vScroll=r,this.scrollBarV.setVisible(r));var i=this.container.clientWidth;this.container.style.height=n+"px",this.$updateCachedSize(!0,this.$gutterWidth,i,n),this.desiredHeight=n,this._signal("autosize")}},this.$computeLayerConfig=function(){var e=this.session,t=this.$size,n=t.height<=2*this.lineHeight,r=this.session.getScreenLength(),i=r*this.lineHeight,s=this.$getLongestLine(),o=!n&&(this.$hScrollBarAlwaysVisible||t.scrollerWidth-s-2*this.$padding<0),u=this.$horizScroll!==o;u&&(this.$horizScroll=o,this.scrollBarH.setVisible(o));var a=this.$vScroll;this.$maxLines&&this.lineHeight>1&&this.$autosize();var f=this.scrollTop%this.lineHeight,l=t.scrollerHeight+this.lineHeight,c=!this.$maxLines&&this.$scrollPastEnd?(t.scrollerHeight-this.lineHeight)*this.$scrollPastEnd:0;i+=c;var h=this.scrollMargin;this.session.setScrollTop(Math.max(-h.top,Math.min(this.scrollTop,i-t.scrollerHeight+h.bottom))),this.session.setScrollLeft(Math.max(-h.left,Math.min(this.scrollLeft,s+2*this.$padding-t.scrollerWidth+h.right)));var p=!n&&(this.$vScrollBarAlwaysVisible||t.scrollerHeight-i+c<0||this.scrollTop>h.top),d=a!==p;d&&(this.$vScroll=p,this.scrollBarV.setVisible(p));var v=Math.ceil(l/this.lineHeight)-1,m=Math.max(0,Math.round((this.scrollTop-f)/this.lineHeight)),g=m+v,y,b,w=this.lineHeight;m=e.screenToDocumentRow(m,0);var E=e.getFoldLine(m);E&&(m=E.start.row),y=e.documentToScreenRow(m,0),b=e.getRowLength(m)*w,g=Math.min(e.screenToDocumentRow(g,0),e.getLength()-1),l=t.scrollerHeight+e.getRowLength(g)*w+b,f=this.scrollTop-y*w;var S=0;this.layerConfig.width!=s&&(S=this.CHANGE_H_SCROLL);if(u||d)S=this.$updateCachedSize(!0,this.gutterWidth,t.width,t.height),this._signal("scrollbarVisibilityChanged"),d&&(s=this.$getLongestLine());return this.layerConfig={width:s,padding:this.$padding,firstRow:m,firstRowScreen:y,lastRow:g,lineHeight:w,characterWidth:this.characterWidth,minHeight:l,maxHeight:i,offset:f,gutterOffset:Math.max(0,Math.ceil((f+t.height-t.scrollerHeight)/w)),height:this.$size.scrollerHeight},S},this.$updateLines=function(){var e=this.$changedLines.firstRow,t=this.$changedLines.lastRow;this.$changedLines=null;var n=this.layerConfig;if(e>n.lastRow+1)return;if(t<n.firstRow)return;if(t===Infinity){this.$showGutter&&this.$gutterLayer.update(n),this.$textLayer.update(n);return}return this.$textLayer.updateLines(n,e,t),!0},this.$getLongestLine=function(){var e=this.session.getScreenWidth();return this.showInvisibles&&!this.session.$useWrapMode&&(e+=1),Math.max(this.$size.scrollerWidth-2*this.$padding,Math.round(e*this.characterWidth))},this.updateFrontMarkers=function(){this.$markerFront.setMarkers(this.session.getMarkers(!0)),this.$loop.schedule(this.CHANGE_MARKER_FRONT)},this.updateBackMarkers=function(){this.$markerBack.setMarkers(this.session.getMarkers()),this.$loop.schedule(this.CHANGE_MARKER_BACK)},this.addGutterDecoration=function(e,t){this.$gutterLayer.addGutterDecoration(e,t)},this.removeGutterDecoration=function(e,t){this.$gutterLayer.removeGutterDecoration(e,t)},this.updateBreakpoints=function(e){this.$loop.schedule(this.CHANGE_GUTTER)},this.setAnnotations=function(e){this.$gutterLayer.setAnnotations(e),this.$loop.schedule(this.CHANGE_GUTTER)},this.updateCursor=function(){this.$loop.schedule(this.CHANGE_CURSOR)},this.hideCursor=function(){this.$cursorLayer.hideCursor()},this.showCursor=function(){this.$cursorLayer.showCursor()},this.scrollSelectionIntoView=function(e,t,n){this.scrollCursorIntoView(e,n),this.scrollCursorIntoView(t,n)},this.scrollCursorIntoView=function(e,t,n){if(this.$size.scrollerHeight===0)return;var r=this.$cursorLayer.getPixelPosition(e),i=r.left,s=r.top,o=n&&n.top||0,u=n&&n.bottom||0,a=this.$scrollAnimation?this.session.getScrollTop():this.scrollTop;a+o>s?(t&&a+o>s+this.lineHeight&&(s-=t*this.$size.scrollerHeight),s===0&&(s=-this.scrollMargin.top),this.session.setScrollTop(s)):a+this.$size.scrollerHeight-u<s+this.lineHeight&&(t&&a+this.$size.scrollerHeight-u<s-this.lineHeight&&(s+=t*this.$size.scrollerHeight),this.session.setScrollTop(s+this.lineHeight-this.$size.scrollerHeight));var f=this.scrollLeft;f>i?(i<this.$padding+2*this.layerConfig.characterWidth&&(i=-this.scrollMargin.left),this.session.setScrollLeft(i)):f+this.$size.scrollerWidth<i+this.characterWidth?this.session.setScrollLeft(Math.round(i+this.characterWidth-this.$size.scrollerWidth)):f<=this.$padding&&i-f<this.characterWidth&&this.session.setScrollLeft(0)},this.getScrollTop=function(){return this.session.getScrollTop()},this.getScrollLeft=function(){return this.session.getScrollLeft()},this.getScrollTopRow=function(){return this.scrollTop/this.lineHeight},this.getScrollBottomRow=function(){return Math.max(0,Math.floor((this.scrollTop+this.$size.scrollerHeight)/this.lineHeight)-1)},this.scrollToRow=function(e){this.session.setScrollTop(e*this.lineHeight)},this.alignCursor=function(e,t){typeof e=="number"&&(e={row:e,column:0});var n=this.$cursorLayer.getPixelPosition(e),r=this.$size.scrollerHeight-this.lineHeight,i=n.top-r*(t||0);return this.session.setScrollTop(i),i},this.STEPS=8,this.$calcSteps=function(e,t){var n=0,r=this.STEPS,i=[],s=function(e,t,n){return n*(Math.pow(e-1,3)+1)+t};for(n=0;n<r;++n)i.push(s(n/this.STEPS,e,t-e));return i},this.scrollToLine=function(e,t,n,r){var i=this.$cursorLayer.getPixelPosition({row:e,column:0}),s=i.top;t&&(s-=this.$size.scrollerHeight/2);var o=this.scrollTop;this.session.setScrollTop(s),n!==!1&&this.animateScrolling(o,r)},this.animateScrolling=function(e,t){var n=this.scrollTop;if(!this.$animatedScroll)return;var r=this;if(e==n)return;if(this.$scrollAnimation){var i=this.$scrollAnimation.steps;if(i.length){e=i[0];if(e==n)return}}var s=r.$calcSteps(e,n);this.$scrollAnimation={from:e,to:n,steps:s},clearInterval(this.$timer),r.session.setScrollTop(s.shift()),r.session.$scrollTop=n,this.$timer=setInterval(function(){s.length?(r.session.setScrollTop(s.shift()),r.session.$scrollTop=n):n!=null?(r.session.$scrollTop=-1,r.session.setScrollTop(n),n=null):(r.$timer=clearInterval(r.$timer),r.$scrollAnimation=null,t&&t())},10)},this.scrollToY=function(e){this.scrollTop!==e&&(this.$loop.schedule(this.CHANGE_SCROLL),this.scrollTop=e)},this.scrollToX=function(e){this.scrollLeft!==e&&(this.scrollLeft=e),this.$loop.schedule(this.CHANGE_H_SCROLL)},this.scrollTo=function(e,t){this.session.setScrollTop(t),this.session.setScrollLeft(t)},this.scrollBy=function(e,t){t&&this.session.setScrollTop(this.session.getScrollTop()+t),e&&this.session.setScrollLeft(this.session.getScrollLeft()+e)},this.isScrollableBy=function(e,t){if(t<0&&this.session.getScrollTop()>=1-this.scrollMargin.top)return!0;if(t>0&&this.session.getScrollTop()+this.$size.scrollerHeight-this.layerConfig.maxHeight<-1+this.scrollMargin.bottom)return!0;if(e<0&&this.session.getScrollLeft()>=1-this.scrollMargin.left)return!0;if(e>0&&this.session.getScrollLeft()+this.$size.scrollerWidth-this.layerConfig.width<-1+this.scrollMargin.right)return!0},this.pixelToScreenCoordinates=function(e,t){var n=this.scroller.getBoundingClientRect(),r=(e+this.scrollLeft-n.left-this.$padding)/this.characterWidth,i=Math.floor((t+this.scrollTop-n.top)/this.lineHeight),s=Math.round(r);return{row:i,column:s,side:r-s>0?1:-1}},this.screenToTextCoordinates=function(e,t){var n=this.scroller.getBoundingClientRect(),r=Math.round((e+this.scrollLeft-n.left-this.$padding)/this.characterWidth),i=(t+this.scrollTop-n.top)/this.lineHeight;return this.session.screenToDocumentPosition(i,Math.max(r,0))},this.textToScreenCoordinates=function(e,t){var n=this.scroller.getBoundingClientRect(),r=this.session.documentToScreenPosition(e,t),i=this.$padding+Math.round(r.column*this.characterWidth),s=r.row*this.lineHeight;return{pageX:n.left+i-this.scrollLeft,pageY:n.top+s-this.scrollTop}},this.visualizeFocus=function(){i.addCssClass(this.container,"ace_focus")},this.visualizeBlur=function(){i.removeCssClass(this.container,"ace_focus")},this.showComposition=function(e){this.$composition||(this.$composition={keepTextAreaAtCursor:this.$keepTextAreaAtCursor,cssText:this.textarea.style.cssText}),this.$keepTextAreaAtCursor=!0,i.addCssClass(this.textarea,"ace_composition"),this.textarea.style.cssText="",this.$moveTextAreaToCursor()},this.setCompositionText=function(e){this.$moveTextAreaToCursor()},this.hideComposition=function(){if(!this.$composition)return;i.removeCssClass(this.textarea,"ace_composition"),this.$keepTextAreaAtCursor=this.$composition.keepTextAreaAtCursor,this.textarea.style.cssText=this.$composition.cssText,this.$composition=null},this.setTheme=function(e,t){function o(r){if(n.$themeId!=e)return t&&t();if(!r.cssClass)return;i.importCssString(r.cssText,r.cssClass,n.container.ownerDocument),n.theme&&i.removeCssClass(n.container,n.theme.cssClass);var s="padding"in r?r.padding:"padding"in(n.theme||{})?4:n.$padding;n.$padding&&s!=n.$padding&&n.setPadding(s),n.$theme=r.cssClass,n.theme=r,i.addCssClass(n.container,r.cssClass),i.setCssClass(n.container,"ace_dark",r.isDark),n.$size&&(n.$size.width=0,n.$updateSizeAsync()),n._dispatchEvent("themeLoaded",{theme:r}),t&&t()}var n=this;this.$themeId=e,n._dispatchEvent("themeChange",{theme:e});if(!e||typeof e=="string"){var r=e||this.$options.theme.initialValue;s.loadModule(["theme",r],o)}else o(e)},this.getTheme=function(){return this.$themeId},this.setStyle=function(e,t){i.setCssClass(this.container,e,t!==!1)},this.unsetStyle=function(e){i.removeCssClass(this.container,e)},this.setCursorStyle=function(e){this.scroller.style.cursor!=e&&(this.scroller.style.cursor=e)},this.setMouseCursor=function(e){this.scroller.style.cursor=e},this.destroy=function(){this.$textLayer.destroy(),this.$cursorLayer.destroy()}}).call(g.prototype),s.defineOptions(g.prototype,"renderer",{animatedScroll:{initialValue:!1},showInvisibles:{set:function(e){this.$textLayer.setShowInvisibles(e)&&this.$loop.schedule(this.CHANGE_TEXT)},initialValue:!1},showPrintMargin:{set:function(){this.$updatePrintMargin()},initialValue:!0},printMarginColumn:{set:function(){this.$updatePrintMargin()},initialValue:80},printMargin:{set:function(e){typeof e=="number"&&(this.$printMarginColumn=e),this.$showPrintMargin=!!e,this.$updatePrintMargin()},get:function(){return this.$showPrintMargin&&this.$printMarginColumn}},showGutter:{set:function(e){this.$gutter.style.display=e?"block":"none",this.$loop.schedule(this.CHANGE_FULL),this.onGutterResize()},initialValue:!0},fadeFoldWidgets:{set:function(e){i.setCssClass(this.$gutter,"ace_fade-fold-widgets",e)},initialValue:!1},showFoldWidgets:{set:function(e){this.$gutterLayer.setShowFoldWidgets(e)},initialValue:!0},showLineNumbers:{set:function(e){this.$gutterLayer.setShowLineNumbers(e),this.$loop.schedule(this.CHANGE_GUTTER)},initialValue:!0},displayIndentGuides:{set:function(e){this.$textLayer.setDisplayIndentGuides(e)&&this.$loop.schedule(this.CHANGE_TEXT)},initialValue:!0},highlightGutterLine:{set:function(e){if(!this.$gutterLineHighlight){this.$gutterLineHighlight=i.createElement("div"),this.$gutterLineHighlight.className="ace_gutter-active-line",this.$gutter.appendChild(this.$gutterLineHighlight);return}this.$gutterLineHighlight.style.display=e?"":"none",this.$cursorLayer.$pixelPos&&this.$updateGutterLineHighlight()},initialValue:!1,value:!0},hScrollBarAlwaysVisible:{set:function(e){(!this.$hScrollBarAlwaysVisible||!this.$horizScroll)&&this.$loop.schedule(this.CHANGE_SCROLL)},initialValue:!1},vScrollBarAlwaysVisible:{set:function(e){(!this.$vScrollBarAlwaysVisible||!this.$vScroll)&&this.$loop.schedule(this.CHANGE_SCROLL)},initialValue:!1},fontSize:{set:function(e){typeof e=="number"&&(e+="px"),this.container.style.fontSize=e,this.updateFontSize()},initialValue:12},fontFamily:{set:function(e){this.container.style.fontFamily=e,this.updateFontSize()}},maxLines:{set:function(e){this.updateFull()}},minLines:{set:function(e){this.updateFull()}},maxPixelHeight:{set:function(e){this.updateFull()},initialValue:0},scrollPastEnd:{set:function(e){e=+e||0;if(this.$scrollPastEnd==e)return;this.$scrollPastEnd=e,this.$loop.schedule(this.CHANGE_SCROLL)},initialValue:0,handlesSet:!0},fixedWidthGutter:{set:function(e){this.$gutterLayer.$fixedWidth=!!e,this.$loop.schedule(this.CHANGE_GUTTER)}},theme:{set:function(e){this.setTheme(e)},get:function(){return this.$themeId||this.theme},initialValue:"./theme/textmate",handlesSet:!0}}),t.VirtualRenderer=g}),define("ace/worker/worker_client",["require","exports","module","ace/lib/oop","ace/lib/net","ace/lib/event_emitter","ace/config"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../lib/net"),s=e("../lib/event_emitter").EventEmitter,o=e("../config"),u=function(t,n,r,i){this.$sendDeltaQueue=this.$sendDeltaQueue.bind(this),this.changeListener=this.changeListener.bind(this),this.onMessage=this.onMessage.bind(this),e.nameToUrl&&!e.toUrl&&(e.toUrl=e.nameToUrl);if(o.get("packaged")||!e.toUrl)i=i||o.moduleUrl(n,"worker");else{var s=this.$normalizePath;i=i||s(e.toUrl("ace/worker/worker.js",null,"_"));var u={};t.forEach(function(t){u[t]=s(e.toUrl(t,null,"_").replace(/(\.js)?(\?.*)?$/,""))})}try{this.$worker=new Worker(i)}catch(a){if(!(a instanceof window.DOMException))throw a;var f=this.$workerBlob(i),l=window.URL||window.webkitURL,c=l.createObjectURL(f);this.$worker=new Worker(c),l.revokeObjectURL(c)}this.$worker.postMessage({init:!0,tlns:u,module:n,classname:r}),this.callbackId=1,this.callbacks={},this.$worker.onmessage=this.onMessage};(function(){r.implement(this,s),this.onMessage=function(e){var t=e.data;switch(t.type){case"event":this._signal(t.name,{data:t.data});break;case"call":var n=this.callbacks[t.id];n&&(n(t.data),delete this.callbacks[t.id]);break;case"error":this.reportError(t.data);break;case"log":window.console&&console.log&&console.log.apply(console,t.data)}},this.reportError=function(e){window.console&&console.error&&console.error(e)},this.$normalizePath=function(e){return i.qualifyURL(e)},this.terminate=function(){this._signal("terminate",{}),this.deltaQueue=null,this.$worker.terminate(),this.$worker=null,this.$doc&&this.$doc.off("change",this.changeListener),this.$doc=null},this.send=function(e,t){this.$worker.postMessage({command:e,args:t})},this.call=function(e,t,n){if(n){var r=this.callbackId++;this.callbacks[r]=n,t.push(r)}this.send(e,t)},this.emit=function(e,t){try{this.$worker.postMessage({event:e,data:{data:t.data}})}catch(n){console.error(n.stack)}},this.attachToDocument=function(e){this.$doc&&this.terminate(),this.$doc=e,this.call("setValue",[e.getValue()]),e.on("change",this.changeListener)},this.changeListener=function(e){this.deltaQueue||(this.deltaQueue=[],setTimeout(this.$sendDeltaQueue,0)),e.action=="insert"?this.deltaQueue.push(e.start,e.lines):this.deltaQueue.push(e.start,e.end)},this.$sendDeltaQueue=function(){var e=this.deltaQueue;if(!e)return;this.deltaQueue=null,e.length>50&&e.length>this.$doc.getLength()>>1?this.call("setValue",[this.$doc.getValue()]):this.emit("change",{data:e})},this.$workerBlob=function(e){var t="importScripts('"+i.qualifyURL(e)+"');";try{return new Blob([t],{type:"application/javascript"})}catch(n){var r=window.BlobBuilder||window.WebKitBlobBuilder||window.MozBlobBuilder,s=new r;return s.append(t),s.getBlob("application/javascript")}}}).call(u.prototype);var a=function(e,t,n){this.$sendDeltaQueue=this.$sendDeltaQueue.bind(this),this.changeListener=this.changeListener.bind(this),this.callbackId=1,this.callbacks={},this.messageBuffer=[];var r=null,i=!1,u=Object.create(s),a=this;this.$worker={},this.$worker.terminate=function(){},this.$worker.postMessage=function(e){a.messageBuffer.push(e),r&&(i?setTimeout(f):f())},this.setEmitSync=function(e){i=e};var f=function(){var e=a.messageBuffer.shift();e.command?r[e.command].apply(r,e.args):e.event&&u._signal(e.event,e.data)};u.postMessage=function(e){a.onMessage({data:e})},u.callback=function(e,t){this.postMessage({type:"call",id:t,data:e})},u.emit=function(e,t){this.postMessage({type:"event",name:e,data:t})},o.loadModule(["worker",t],function(e){r=new e[n](u);while(a.messageBuffer.length)f()})};a.prototype=u.prototype,t.UIWorkerClient=a,t.WorkerClient=u}),define("ace/placeholder",["require","exports","module","ace/range","ace/lib/event_emitter","ace/lib/oop"],function(e,t,n){"use strict";var r=e("./range").Range,i=e("./lib/event_emitter").EventEmitter,s=e("./lib/oop"),o=function(e,t,n,r,i,s){var o=this;this.length=t,this.session=e,this.doc=e.getDocument(),this.mainClass=i,this.othersClass=s,this.$onUpdate=this.onUpdate.bind(this),this.doc.on("change",this.$onUpdate),this.$others=r,this.$onCursorChange=function(){setTimeout(function(){o.onCursorChange()})},this.$pos=n;var u=e.getUndoManager().$undoStack||e.getUndoManager().$undostack||{length:-1};this.$undoStackDepth=u.length,this.setup(),e.selection.on("changeCursor",this.$onCursorChange)};(function(){s.implement(this,i),this.setup=function(){var e=this,t=this.doc,n=this.session;this.selectionBefore=n.selection.toJSON(),n.selection.inMultiSelectMode&&n.selection.toSingleRange(),this.pos=t.createAnchor(this.$pos.row,this.$pos.column);var i=this.pos;i.$insertRight=!0,i.detach(),i.markerId=n.addMarker(new r(i.row,i.column,i.row,i.column+this.length),this.mainClass,null,!1),this.others=[],this.$others.forEach(function(n){var r=t.createAnchor(n.row,n.column);r.$insertRight=!0,r.detach(),e.others.push(r)}),n.setUndoSelect(!1)},this.showOtherMarkers=function(){if(this.othersActive)return;var e=this.session,t=this;this.othersActive=!0,this.others.forEach(function(n){n.markerId=e.addMarker(new r(n.row,n.column,n.row,n.column+t.length),t.othersClass,null,!1)})},this.hideOtherMarkers=function(){if(!this.othersActive)return;this.othersActive=!1;for(var e=0;e<this.others.length;e++)this.session.removeMarker(this.others[e].markerId)},this.onUpdate=function(e){if(this.$updating)return this.updateAnchors(e);var t=e;if(t.start.row!==t.end.row)return;if(t.start.row!==this.pos.row)return;this.$updating=!0;var n=e.action==="insert"?t.end.column-t.start.column:t.start.column-t.end.column,i=t.start.column>=this.pos.column&&t.start.column<=this.pos.column+this.length+1,s=t.start.column-this.pos.column;this.updateAnchors(e),i&&(this.length+=n);if(i&&!this.session.$fromUndo)if(e.action==="insert")for(var o=this.others.length-1;o>=0;o--){var u=this.others[o],a={row:u.row,column:u.column+s};this.doc.insertMergedLines(a,e.lines)}else if(e.action==="remove")for(var o=this.others.length-1;o>=0;o--){var u=this.others[o],a={row:u.row,column:u.column+s};this.doc.remove(new r(a.row,a.column,a.row,a.column-n))}this.$updating=!1,this.updateMarkers()},this.updateAnchors=function(e){this.pos.onChange(e);for(var t=this.others.length;t--;)this.others[t].onChange(e);this.updateMarkers()},this.updateMarkers=function(){if(this.$updating)return;var e=this,t=this.session,n=function(n,i){t.removeMarker(n.markerId),n.markerId=t.addMarker(new r(n.row,n.column,n.row,n.column+e.length),i,null,!1)};n(this.pos,this.mainClass);for(var i=this.others.length;i--;)n(this.others[i],this.othersClass)},this.onCursorChange=function(e){if(this.$updating||!this.session)return;var t=this.session.selection.getCursor();t.row===this.pos.row&&t.column>=this.pos.column&&t.column<=this.pos.column+this.length?(this.showOtherMarkers(),this._emit("cursorEnter",e)):(this.hideOtherMarkers(),this._emit("cursorLeave",e))},this.detach=function(){this.session.removeMarker(this.pos&&this.pos.markerId),this.hideOtherMarkers(),this.doc.removeEventListener("change",this.$onUpdate),this.session.selection.removeEventListener("changeCursor",this.$onCursorChange),this.session.setUndoSelect(!0),this.session=null},this.cancel=function(){if(this.$undoStackDepth===-1)return;var e=this.session.getUndoManager(),t=(e.$undoStack||e.$undostack).length-this.$undoStackDepth;for(var n=0;n<t;n++)e.undo(!0);this.selectionBefore&&this.session.selection.fromJSON(this.selectionBefore)}}).call(o.prototype),t.PlaceHolder=o}),define("ace/mouse/multi_select_handler",["require","exports","module","ace/lib/event","ace/lib/useragent"],function(e,t,n){function s(e,t){return e.row==t.row&&e.column==t.column}function o(e){var t=e.domEvent,n=t.altKey,o=t.shiftKey,u=t.ctrlKey,a=e.getAccelKey(),f=e.getButton();u&&i.isMac&&(f=t.button);if(e.editor.inMultiSelectMode&&f==2){e.editor.textInput.onContextMenu(e.domEvent);return}if(!u&&!n&&!a){f===0&&e.editor.inMultiSelectMode&&e.editor.exitMultiSelectMode();return}if(f!==0)return;var l=e.editor,c=l.selection,h=l.inMultiSelectMode,p=e.getDocumentPosition(),d=c.getCursor(),v=e.inSelection()||c.isEmpty()&&s(p,d),m=e.x,g=e.y,y=function(e){m=e.clientX,g=e.clientY},b=l.session,w=l.renderer.pixelToScreenCoordinates(m,g),E=w,S;if(l.$mouseHandler.$enableJumpToDef)u&&n||a&&n?S=o?"block":"add":n&&l.$blockSelectEnabled&&(S="block");else if(a&&!n){S="add";if(!h&&o)return}else n&&l.$blockSelectEnabled&&(S="block");S&&i.isMac&&t.ctrlKey&&l.$mouseHandler.cancelContextMenu();if(S=="add"){if(!h&&v)return;if(!h){var x=c.toOrientedRange();l.addSelectionMarker(x)}var T=c.rangeList.rangeAtPoint(p);l.$blockScrolling++,l.inVirtualSelectionMode=!0,o&&(T=null,x=c.ranges[0]||x,l.removeSelectionMarker(x)),l.once("mouseup",function(){var e=c.toOrientedRange();T&&e.isEmpty()&&s(T.cursor,e.cursor)?c.substractPoint(e.cursor):(o?c.substractPoint(x.cursor):x&&(l.removeSelectionMarker(x),c.addRange(x)),c.addRange(e)),l.$blockScrolling--,l.inVirtualSelectionMode=!1})}else if(S=="block"){e.stop(),l.inVirtualSelectionMode=!0;var N,C=[],k=function(){var e=l.renderer.pixelToScreenCoordinates(m,g),t=b.screenToDocumentPosition(e.row,e.column);if(s(E,e)&&s(t,c.lead))return;E=e,l.$blockScrolling++,l.selection.moveToPosition(t),l.renderer.scrollCursorIntoView(),l.removeSelectionMarkers(C),C=c.rectangularRangeBlock(E,w),l.$mouseHandler.$clickSelection&&C.length==1&&C[0].isEmpty()&&(C[0]=l.$mouseHandler.$clickSelection.clone()),C.forEach(l.addSelectionMarker,l),l.updateSelectionMarkers(),l.$blockScrolling--};l.$blockScrolling++,h&&!a?c.toSingleRange():!h&&a&&(N=c.toOrientedRange(),l.addSelectionMarker(N)),o?w=b.documentToScreenPosition(c.lead):c.moveToPosition(p),l.$blockScrolling--,E={row:-1,column:-1};var L=function(e){clearInterval(O),l.removeSelectionMarkers(C),C.length||(C=[c.toOrientedRange()]),l.$blockScrolling++,N&&(l.removeSelectionMarker(N),c.toSingleRange(N));for(var t=0;t<C.length;t++)c.addRange(C[t]);l.inVirtualSelectionMode=!1,l.$mouseHandler.$clickSelection=null,l.$blockScrolling--},A=k;r.capture(l.container,y,L);var O=setInterval(function(){A()},20);return e.preventDefault()}}var r=e("../lib/event"),i=e("../lib/useragent");t.onMouseDown=o}),define("ace/commands/multi_select_commands",["require","exports","module","ace/keyboard/hash_handler"],function(e,t,n){t.defaultCommands=[{name:"addCursorAbove",exec:function(e){e.selectMoreLines(-1)},bindKey:{win:"Ctrl-Alt-Up",mac:"Ctrl-Alt-Up"},scrollIntoView:"cursor",readOnly:!0},{name:"addCursorBelow",exec:function(e){e.selectMoreLines(1)},bindKey:{win:"Ctrl-Alt-Down",mac:"Ctrl-Alt-Down"},scrollIntoView:"cursor",readOnly:!0},{name:"addCursorAboveSkipCurrent",exec:function(e){e.selectMoreLines(-1,!0)},bindKey:{win:"Ctrl-Alt-Shift-Up",mac:"Ctrl-Alt-Shift-Up"},scrollIntoView:"cursor",readOnly:!0},{name:"addCursorBelowSkipCurrent",exec:function(e){e.selectMoreLines(1,!0)},bindKey:{win:"Ctrl-Alt-Shift-Down",mac:"Ctrl-Alt-Shift-Down"},scrollIntoView:"cursor",readOnly:!0},{name:"selectMoreBefore",exec:function(e){e.selectMore(-1)},bindKey:{win:"Ctrl-Alt-Left",mac:"Ctrl-Alt-Left"},scrollIntoView:"cursor",readOnly:!0},{name:"selectMoreAfter",exec:function(e){e.selectMore(1)},bindKey:{win:"Ctrl-Alt-Right",mac:"Ctrl-Alt-Right"},scrollIntoView:"cursor",readOnly:!0},{name:"selectNextBefore",exec:function(e){e.selectMore(-1,!0)},bindKey:{win:"Ctrl-Alt-Shift-Left",mac:"Ctrl-Alt-Shift-Left"},scrollIntoView:"cursor",readOnly:!0},{name:"selectNextAfter",exec:function(e){e.selectMore(1,!0)},bindKey:{win:"Ctrl-Alt-Shift-Right",mac:"Ctrl-Alt-Shift-Right"},scrollIntoView:"cursor",readOnly:!0},{name:"splitIntoLines",exec:function(e){e.multiSelect.splitIntoLines()},bindKey:{win:"Ctrl-Alt-L",mac:"Ctrl-Alt-L"},readOnly:!0},{name:"alignCursors",exec:function(e){e.alignCursors()},bindKey:{win:"Ctrl-Alt-A",mac:"Ctrl-Alt-A"},scrollIntoView:"cursor"},{name:"findAll",exec:function(e){e.findAll()},bindKey:{win:"Ctrl-Alt-K",mac:"Ctrl-Alt-G"},scrollIntoView:"cursor",readOnly:!0}],t.multiSelectCommands=[{name:"singleSelection",bindKey:"esc",exec:function(e){e.exitMultiSelectMode()},scrollIntoView:"cursor",readOnly:!0,isAvailable:function(e){return e&&e.inMultiSelectMode}}];var r=e("../keyboard/hash_handler").HashHandler;t.keyboardHandler=new r(t.multiSelectCommands)}),define("ace/multi_select",["require","exports","module","ace/range_list","ace/range","ace/selection","ace/mouse/multi_select_handler","ace/lib/event","ace/lib/lang","ace/commands/multi_select_commands","ace/search","ace/edit_session","ace/editor","ace/config"],function(e,t,n){function h(e,t,n){return c.$options.wrap=!0,c.$options.needle=t,c.$options.backwards=n==-1,c.find(e)}function v(e,t){return e.row==t.row&&e.column==t.column}function m(e){if(e.$multiselectOnSessionChange)return;e.$onAddRange=e.$onAddRange.bind(e),e.$onRemoveRange=e.$onRemoveRange.bind(e),e.$onMultiSelect=e.$onMultiSelect.bind(e),e.$onSingleSelect=e.$onSingleSelect.bind(e),e.$multiselectOnSessionChange=t.onSessionChange.bind(e),e.$checkMultiselectChange=e.$checkMultiselectChange.bind(e),e.$multiselectOnSessionChange(e),e.on("changeSession",e.$multiselectOnSessionChange),e.on("mousedown",o),e.commands.addCommands(f.defaultCommands),g(e)}function g(e){function r(t){n&&(e.renderer.setMouseCursor(""),n=!1)}var t=e.textInput.getElement(),n=!1;u.addListener(t,"keydown",function(t){var i=t.keyCode==18&&!(t.ctrlKey||t.shiftKey||t.metaKey);e.$blockSelectEnabled&&i?n||(e.renderer.setMouseCursor("crosshair"),n=!0):n&&r()}),u.addListener(t,"keyup",r),u.addListener(t,"blur",r)}var r=e("./range_list").RangeList,i=e("./range").Range,s=e("./selection").Selection,o=e("./mouse/multi_select_handler").onMouseDown,u=e("./lib/event"),a=e("./lib/lang"),f=e("./commands/multi_select_commands");t.commands=f.defaultCommands.concat(f.multiSelectCommands);var l=e("./search").Search,c=new l,p=e("./edit_session").EditSession;(function(){this.getSelectionMarkers=function(){return this.$selectionMarkers}}).call(p.prototype),function(){this.ranges=null,this.rangeList=null,this.addRange=function(e,t){if(!e)return;if(!this.inMultiSelectMode&&this.rangeCount===0){var n=this.toOrientedRange();this.rangeList.add(n),this.rangeList.add(e);if(this.rangeList.ranges.length!=2)return this.rangeList.removeAll(),t||this.fromOrientedRange(e);this.rangeList.removeAll(),this.rangeList.add(n),this.$onAddRange(n)}e.cursor||(e.cursor=e.end);var r=this.rangeList.add(e);return this.$onAddRange(e),r.length&&this.$onRemoveRange(r),this.rangeCount>1&&!this.inMultiSelectMode&&(this._signal("multiSelect"),this.inMultiSelectMode=!0,this.session.$undoSelect=!1,this.rangeList.attach(this.session)),t||this.fromOrientedRange(e)},this.toSingleRange=function(e){e=e||this.ranges[0];var t=this.rangeList.removeAll();t.length&&this.$onRemoveRange(t),e&&this.fromOrientedRange(e)},this.substractPoint=function(e){var t=this.rangeList.substractPoint(e);if(t)return this.$onRemoveRange(t),t[0]},this.mergeOverlappingRanges=function(){var e=this.rangeList.merge();e.length?this.$onRemoveRange(e):this.ranges[0]&&this.fromOrientedRange(this.ranges[0])},this.$onAddRange=function(e){this.rangeCount=this.rangeList.ranges.length,this.ranges.unshift(e),this._signal("addRange",{range:e})},this.$onRemoveRange=function(e){this.rangeCount=this.rangeList.ranges.length;if(this.rangeCount==1&&this.inMultiSelectMode){var t=this.rangeList.ranges.pop();e.push(t),this.rangeCount=0}for(var n=e.length;n--;){var r=this.ranges.indexOf(e[n]);this.ranges.splice(r,1)}this._signal("removeRange",{ranges:e}),this.rangeCount===0&&this.inMultiSelectMode&&(this.inMultiSelectMode=!1,this._signal("singleSelect"),this.session.$undoSelect=!0,this.rangeList.detach(this.session)),t=t||this.ranges[0],t&&!t.isEqual(this.getRange())&&this.fromOrientedRange(t)},this.$initRangeList=function(){if(this.rangeList)return;this.rangeList=new r,this.ranges=[],this.rangeCount=0},this.getAllRanges=function(){return this.rangeCount?this.rangeList.ranges.concat():[this.getRange()]},this.splitIntoLines=function(){if(this.rangeCount>1){var e=this.rangeList.ranges,t=e[e.length-1],n=i.fromPoints(e[0].start,t.end);this.toSingleRange(),this.setSelectionRange(n,t.cursor==t.start)}else{var n=this.getRange(),r=this.isBackwards(),s=n.start.row,o=n.end.row;if(s==o){if(r)var u=n.end,a=n.start;else var u=n.start,a=n.end;this.addRange(i.fromPoints(a,a)),this.addRange(i.fromPoints(u,u));return}var f=[],l=this.getLineRange(s,!0);l.start.column=n.start.column,f.push(l);for(var c=s+1;c<o;c++)f.push(this.getLineRange(c,!0));l=this.getLineRange(o,!0),l.end.column=n.end.column,f.push(l),f.forEach(this.addRange,this)}},this.toggleBlockSelection=function(){if(this.rangeCount>1){var e=this.rangeList.ranges,t=e[e.length-1],n=i.fromPoints(e[0].start,t.end);this.toSingleRange(),this.setSelectionRange(n,t.cursor==t.start)}else{var r=this.session.documentToScreenPosition(this.selectionLead),s=this.session.documentToScreenPosition(this.selectionAnchor),o=this.rectangularRangeBlock(r,s);o.forEach(this.addRange,this)}},this.rectangularRangeBlock=function(e,t,n){var r=[],s=e.column<t.column;if(s)var o=e.column,u=t.column;else var o=t.column,u=e.column;var a=e.row<t.row;if(a)var f=e.row,l=t.row;else var f=t.row,l=e.row;o<0&&(o=0),f<0&&(f=0),f==l&&(n=!0);for(var c=f;c<=l;c++){var h=i.fromPoints(this.session.screenToDocumentPosition(c,o),this.session.screenToDocumentPosition(c,u));if(h.isEmpty()){if(p&&v(h.end,p))break;var p=h.end}h.cursor=s?h.start:h.end,r.push(h)}a&&r.reverse();if(!n){var d=r.length-1;while(r[d].isEmpty()&&d>0)d--;if(d>0){var m=0;while(r[m].isEmpty())m++}for(var g=d;g>=m;g--)r[g].isEmpty()&&r.splice(g,1)}return r}}.call(s.prototype);var d=e("./editor").Editor;(function(){this.updateSelectionMarkers=function(){this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.addSelectionMarker=function(e){e.cursor||(e.cursor=e.end);var t=this.getSelectionStyle();return e.marker=this.session.addMarker(e,"ace_selection",t),this.session.$selectionMarkers.push(e),this.session.selectionMarkerCount=this.session.$selectionMarkers.length,e},this.removeSelectionMarker=function(e){if(!e.marker)return;this.session.removeMarker(e.marker);var t=this.session.$selectionMarkers.indexOf(e);t!=-1&&this.session.$selectionMarkers.splice(t,1),this.session.selectionMarkerCount=this.session.$selectionMarkers.length},this.removeSelectionMarkers=function(e){var t=this.session.$selectionMarkers;for(var n=e.length;n--;){var r=e[n];if(!r.marker)continue;this.session.removeMarker(r.marker);var i=t.indexOf(r);i!=-1&&t.splice(i,1)}this.session.selectionMarkerCount=t.length},this.$onAddRange=function(e){this.addSelectionMarker(e.range),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onRemoveRange=function(e){this.removeSelectionMarkers(e.ranges),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onMultiSelect=function(e){if(this.inMultiSelectMode)return;this.inMultiSelectMode=!0,this.setStyle("ace_multiselect"),this.keyBinding.addKeyboardHandler(f.keyboardHandler),this.commands.setDefaultHandler("exec",this.$onMultiSelectExec),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onSingleSelect=function(e){if(this.session.multiSelect.inVirtualMode)return;this.inMultiSelectMode=!1,this.unsetStyle("ace_multiselect"),this.keyBinding.removeKeyboardHandler(f.keyboardHandler),this.commands.removeDefaultHandler("exec",this.$onMultiSelectExec),this.renderer.updateCursor(),this.renderer.updateBackMarkers(),this._emit("changeSelection")},this.$onMultiSelectExec=function(e){var t=e.command,n=e.editor;if(!n.multiSelect)return;if(!t.multiSelectAction){var r=t.exec(n,e.args||{});n.multiSelect.addRange(n.multiSelect.toOrientedRange()),n.multiSelect.mergeOverlappingRanges()}else t.multiSelectAction=="forEach"?r=n.forEachSelection(t,e.args):t.multiSelectAction=="forEachLine"?r=n.forEachSelection(t,e.args,!0):t.multiSelectAction=="single"?(n.exitMultiSelectMode(),r=t.exec(n,e.args||{})):r=t.multiSelectAction(n,e.args||{});return r},this.forEachSelection=function(e,t,n){if(this.inVirtualSelectionMode)return;var r=n&&n.keepOrder,i=n==1||n&&n.$byLines,o=this.session,u=this.selection,a=u.rangeList,f=(r?u:a).ranges,l;if(!f.length)return e.exec?e.exec(this,t||{}):e(this,t||{});var c=u._eventRegistry;u._eventRegistry={};var h=new s(o);this.inVirtualSelectionMode=!0;for(var p=f.length;p--;){if(i)while(p>0&&f[p].start.row==f[p-1].end.row)p--;h.fromOrientedRange(f[p]),h.index=p,this.selection=o.selection=h;var d=e.exec?e.exec(this,t||{}):e(this,t||{});!l&&d!==undefined&&(l=d),h.toOrientedRange(f[p])}h.detach(),this.selection=o.selection=u,this.inVirtualSelectionMode=!1,u._eventRegistry=c,u.mergeOverlappingRanges();var v=this.renderer.$scrollAnimation;return this.onCursorChange(),this.onSelectionChange(),v&&v.from==v.to&&this.renderer.animateScrolling(v.from),l},this.exitMultiSelectMode=function(){if(!this.inMultiSelectMode||this.inVirtualSelectionMode)return;this.multiSelect.toSingleRange()},this.getSelectedText=function(){var e="";if(this.inMultiSelectMode&&!this.inVirtualSelectionMode){var t=this.multiSelect.rangeList.ranges,n=[];for(var r=0;r<t.length;r++)n.push(this.session.getTextRange(t[r]));var i=this.session.getDocument().getNewLineCharacter();e=n.join(i),e.length==(n.length-1)*i.length&&(e="")}else this.selection.isEmpty()||(e=this.session.getTextRange(this.getSelectionRange()));return e},this.$checkMultiselectChange=function(e,t){if(this.inMultiSelectMode&&!this.inVirtualSelectionMode){var n=this.multiSelect.ranges[0];if(this.multiSelect.isEmpty()&&t==this.multiSelect.anchor)return;var r=t==this.multiSelect.anchor?n.cursor==n.start?n.end:n.start:n.cursor;(r.row!=t.row||this.session.$clipPositionToDocument(r.row,r.column).column!=t.column)&&this.multiSelect.toSingleRange(this.multiSelect.toOrientedRange())}},this.findAll=function(e,t,n){t=t||{},t.needle=e||t.needle;if(t.needle==undefined){var r=this.selection.isEmpty()?this.selection.getWordRange():this.selection.getRange();t.needle=this.session.getTextRange(r)}this.$search.set(t);var i=this.$search.findAll(this.session);if(!i.length)return 0;this.$blockScrolling+=1;var s=this.multiSelect;n||s.toSingleRange(i[0]);for(var o=i.length;o--;)s.addRange(i[o],!0);return r&&s.rangeList.rangeAtPoint(r.start)&&s.addRange(r,!0),this.$blockScrolling-=1,i.length},this.selectMoreLines=function(e,t){var n=this.selection.toOrientedRange(),r=n.cursor==n.end,s=this.session.documentToScreenPosition(n.cursor);this.selection.$desiredColumn&&(s.column=this.selection.$desiredColumn);var o=this.session.screenToDocumentPosition(s.row+e,s.column);if(!n.isEmpty())var u=this.session.documentToScreenPosition(r?n.end:n.start),a=this.session.screenToDocumentPosition(u.row+e,u.column);else var a=o;if(r){var f=i.fromPoints(o,a);f.cursor=f.start}else{var f=i.fromPoints(a,o);f.cursor=f.end}f.desiredColumn=s.column;if(!this.selection.inMultiSelectMode)this.selection.addRange(n);else if(t)var l=n.cursor;this.selection.addRange(f),l&&this.selection.substractPoint(l)},this.transposeSelections=function(e){var t=this.session,n=t.multiSelect,r=n.ranges;for(var i=r.length;i--;){var s=r[i];if(s.isEmpty()){var o=t.getWordRange(s.start.row,s.start.column);s.start.row=o.start.row,s.start.column=o.start.column,s.end.row=o.end.row,s.end.column=o.end.column}}n.mergeOverlappingRanges();var u=[];for(var i=r.length;i--;){var s=r[i];u.unshift(t.getTextRange(s))}e<0?u.unshift(u.pop()):u.push(u.shift());for(var i=r.length;i--;){var s=r[i],o=s.clone();t.replace(s,u[i]),s.start.row=o.start.row,s.start.column=o.start.column}},this.selectMore=function(e,t,n){var r=this.session,i=r.multiSelect,s=i.toOrientedRange();if(s.isEmpty()){s=r.getWordRange(s.start.row,s.start.column),s.cursor=e==-1?s.start:s.end,this.multiSelect.addRange(s);if(n)return}var o=r.getTextRange(s),u=h(r,o,e);u&&(u.cursor=e==-1?u.start:u.end,this.$blockScrolling+=1,this.session.unfold(u),this.multiSelect.addRange(u),this.$blockScrolling-=1,this.renderer.scrollCursorIntoView(null,.5)),t&&this.multiSelect.substractPoint(s.cursor)},this.alignCursors=function(){var e=this.session,t=e.multiSelect,n=t.ranges,r=-1,s=n.filter(function(e){if(e.cursor.row==r)return!0;r=e.cursor.row});if(!n.length||s.length==n.length-1){var o=this.selection.getRange(),u=o.start.row,f=o.end.row,l=u==f;if(l){var c=this.session.getLength(),h;do h=this.session.getLine(f);while(/[=:]/.test(h)&&++f<c);do h=this.session.getLine(u);while(/[=:]/.test(h)&&--u>0);u<0&&(u=0),f>=c&&(f=c-1)}var p=this.session.removeFullLines(u,f);p=this.$reAlignText(p,l),this.session.insert({row:u,column:0},p.join("\n")+"\n"),l||(o.start.column=0,o.end.column=p[p.length-1].length),this.selection.setRange(o)}else{s.forEach(function(e){t.substractPoint(e.cursor)});var d=0,v=Infinity,m=n.map(function(t){var n=t.cursor,r=e.getLine(n.row),i=r.substr(n.column).search(/\S/g);return i==-1&&(i=0),n.column>d&&(d=n.column),i<v&&(v=i),i});n.forEach(function(t,n){var r=t.cursor,s=d-r.column,o=m[n]-v;s>o?e.insert(r,a.stringRepeat(" ",s-o)):e.remove(new i(r.row,r.column,r.row,r.column-s+o)),t.start.column=t.end.column=d,t.start.row=t.end.row=r.row,t.cursor=t.end}),t.fromOrientedRange(n[0]),this.renderer.updateCursor(),this.renderer.updateBackMarkers()}},this.$reAlignText=function(e,t){function u(e){return a.stringRepeat(" ",e)}function f(e){return e[2]?u(i)+e[2]+u(s-e[2].length+o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}function l(e){return e[2]?u(i+s-e[2].length)+e[2]+u(o," ")+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}function c(e){return e[2]?u(i)+e[2]+u(o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}var n=!0,r=!0,i,s,o;return e.map(function(e){var t=e.match(/(\s*)(.*?)(\s*)([=:].*)/);return t?i==null?(i=t[1].length,s=t[2].length,o=t[3].length,t):(i+s+o!=t[1].length+t[2].length+t[3].length&&(r=!1),i!=t[1].length&&(n=!1),i>t[1].length&&(i=t[1].length),s<t[2].length&&(s=t[2].length),o>t[3].length&&(o=t[3].length),t):[e]}).map(t?f:n?r?l:f:c)}}).call(d.prototype),t.onSessionChange=function(e){var t=e.session;t&&!t.multiSelect&&(t.$selectionMarkers=[],t.selection.$initRangeList(),t.multiSelect=t.selection),this.multiSelect=t&&t.multiSelect;var n=e.oldSession;n&&(n.multiSelect.off("addRange",this.$onAddRange),n.multiSelect.off("removeRange",this.$onRemoveRange),n.multiSelect.off("multiSelect",this.$onMultiSelect),n.multiSelect.off("singleSelect",this.$onSingleSelect),n.multiSelect.lead.off("change",this.$checkMultiselectChange),n.multiSelect.anchor.off("change",this.$checkMultiselectChange)),t&&(t.multiSelect.on("addRange",this.$onAddRange),t.multiSelect.on("removeRange",this.$onRemoveRange),t.multiSelect.on("multiSelect",this.$onMultiSelect),t.multiSelect.on("singleSelect",this.$onSingleSelect),t.multiSelect.lead.on("change",this.$checkMultiselectChange),t.multiSelect.anchor.on("change",this.$checkMultiselectChange)),t&&this.inMultiSelectMode!=t.selection.inMultiSelectMode&&(t.selection.inMultiSelectMode?this.$onMultiSelect():this.$onSingleSelect())},t.MultiSelect=m,e("./config").defineOptions(d.prototype,"editor",{enableMultiselect:{set:function(e){m(this),e?(this.on("changeSession",this.$multiselectOnSessionChange),this.on("mousedown",o)):(this.off("changeSession",this.$multiselectOnSessionChange),this.off("mousedown",o))},value:!0},enableBlockSelect:{set:function(e){this.$blockSelectEnabled=e},value:!0}})}),define("ace/mode/folding/fold_mode",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("../../range").Range,i=t.FoldMode=function(){};(function(){this.foldingStartMarker=null,this.foldingStopMarker=null,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);return this.foldingStartMarker.test(r)?"start":t=="markbeginend"&&this.foldingStopMarker&&this.foldingStopMarker.test(r)?"end":""},this.getFoldWidgetRange=function(e,t,n){return null},this.indentationBlock=function(e,t,n){var i=/\S/,s=e.getLine(t),o=s.search(i);if(o==-1)return;var u=n||s.length,a=e.getLength(),f=t,l=t;while(++t<a){var c=e.getLine(t).search(i);if(c==-1)continue;if(c<=o)break;l=t}if(l>f){var h=e.getLine(l).length;return new r(f,u,l,h)}},this.openingBracketBlock=function(e,t,n,i,s){var o={row:n,column:i+1},u=e.$findClosingBracket(t,o,s);if(!u)return;var a=e.foldWidgets[u.row];return a==null&&(a=e.getFoldWidget(u.row)),a=="start"&&u.row>o.row&&(u.row--,u.column=e.getLine(u.row).length),r.fromPoints(o,u)},this.closingBracketBlock=function(e,t,n,i,s){var o={row:n,column:i},u=e.$findOpeningBracket(t,o);if(!u)return;return u.column++,o.column--,r.fromPoints(u,o)}}).call(i.prototype)}),define("ace/theme/textmate",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";t.isDark=!1,t.cssClass="ace-tm",t.cssText='.ace-tm .ace_gutter {background: #f0f0f0;color: #333;}.ace-tm .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-tm .ace_fold {background-color: #6B72E6;}.ace-tm {background-color: #FFFFFF;color: black;}.ace-tm .ace_cursor {color: black;}.ace-tm .ace_invisible {color: rgb(191, 191, 191);}.ace-tm .ace_storage,.ace-tm .ace_keyword {color: blue;}.ace-tm .ace_constant {color: rgb(197, 6, 11);}.ace-tm .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-tm .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-tm .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-tm .ace_invalid {background-color: rgba(255, 0, 0, 0.1);color: red;}.ace-tm .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-tm .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-tm .ace_support.ace_type,.ace-tm .ace_support.ace_class {color: rgb(109, 121, 222);}.ace-tm .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-tm .ace_string {color: rgb(3, 106, 7);}.ace-tm .ace_comment {color: rgb(76, 136, 107);}.ace-tm .ace_comment.ace_doc {color: rgb(0, 102, 255);}.ace-tm .ace_comment.ace_doc.ace_tag {color: rgb(128, 159, 191);}.ace-tm .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-tm .ace_variable {color: rgb(49, 132, 149);}.ace-tm .ace_xml-pe {color: rgb(104, 104, 91);}.ace-tm .ace_entity.ace_name.ace_function {color: #0000A2;}.ace-tm .ace_heading {color: rgb(12, 7, 255);}.ace-tm .ace_list {color:rgb(185, 6, 144);}.ace-tm .ace_meta.ace_tag {color:rgb(0, 22, 142);}.ace-tm .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-tm .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-tm.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px white;}.ace-tm .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-tm .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-tm .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-tm .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-tm .ace_gutter-active-line {background-color : #dcdcdc;}.ace-tm .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-tm .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}';var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}),define("ace/line_widgets",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/range"],function(e,t,n){"use strict";function o(e){this.session=e,this.session.widgetManager=this,this.session.getRowLength=this.getRowLength,this.session.$getWidgetScreenLength=this.$getWidgetScreenLength,this.updateOnChange=this.updateOnChange.bind(this),this.renderWidgets=this.renderWidgets.bind(this),this.measureWidgets=this.measureWidgets.bind(this),this.session._changedWidgets=[],this.$onChangeEditor=this.$onChangeEditor.bind(this),this.session.on("change",this.updateOnChange),this.session.on("changeFold",this.updateOnFold),this.session.on("changeEditor",this.$onChangeEditor)}var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./range").Range;(function(){this.getRowLength=function(e){var t;return this.lineWidgets?t=this.lineWidgets[e]&&this.lineWidgets[e].rowCount||0:t=0,!this.$useWrapMode||!this.$wrapData[e]?1+t:this.$wrapData[e].length+1+t},this.$getWidgetScreenLength=function(){var e=0;return this.lineWidgets.forEach(function(t){t&&t.rowCount&&!t.hidden&&(e+=t.rowCount)}),e},this.$onChangeEditor=function(e){this.attach(e.editor)},this.attach=function(e){e&&e.widgetManager&&e.widgetManager!=this&&e.widgetManager.detach();if(this.editor==e)return;this.detach(),this.editor=e,e&&(e.widgetManager=this,e.renderer.on("beforeRender",this.measureWidgets),e.renderer.on("afterRender",this.renderWidgets))},this.detach=function(e){var t=this.editor;if(!t)return;this.editor=null,t.widgetManager=null,t.renderer.off("beforeRender",this.measureWidgets),t.renderer.off("afterRender",this.renderWidgets);var n=this.session.lineWidgets;n&&n.forEach(function(e){e&&e.el&&e.el.parentNode&&(e._inDocument=!1,e.el.parentNode.removeChild(e.el))})},this.updateOnFold=function(e,t){var n=t.lineWidgets;if(!n||!e.action)return;var r=e.data,i=r.start.row,s=r.end.row,o=e.action=="add";for(var u=i+1;u<s;u++)n[u]&&(n[u].hidden=o);n[s]&&(o?n[i]?n[s].hidden=o:n[i]=n[s]:(n[i]==n[s]&&(n[i]=undefined),n[s].hidden=o))},this.updateOnChange=function(e){var t=this.session.lineWidgets;if(!t)return;var n=e.start.row,r=e.end.row-n;if(r!==0)if(e.action=="remove"){var i=t.splice(n+1,r);i.forEach(function(e){e&&this.removeLineWidget(e)},this),this.$updateRows()}else{var s=new Array(r);s.unshift(n,0),t.splice.apply(t,s),this.$updateRows()}},this.$updateRows=function(){var e=this.session.lineWidgets;if(!e)return;var t=!0;e.forEach(function(e,n){if(e){t=!1,e.row=n;while(e.$oldWidget)e.$oldWidget.row=n,e=e.$oldWidget}}),t&&(this.session.lineWidgets=null)},this.addLineWidget=function(e){this.session.lineWidgets||(this.session.lineWidgets=new Array(this.session.getLength()));var t=this.session.lineWidgets[e.row];t&&(e.$oldWidget=t,t.el&&t.el.parentNode&&(t.el.parentNode.removeChild(t.el),t._inDocument=!1)),this.session.lineWidgets[e.row]=e,e.session=this.session;var n=this.editor.renderer;e.html&&!e.el&&(e.el=i.createElement("div"),e.el.innerHTML=e.html),e.el&&(i.addCssClass(e.el,"ace_lineWidgetContainer"),e.el.style.position="absolute",e.el.style.zIndex=5,n.container.appendChild(e.el),e._inDocument=!0),e.coverGutter||(e.el.style.zIndex=3),e.pixelHeight||(e.pixelHeight=e.el.offsetHeight),e.rowCount==null&&(e.rowCount=e.pixelHeight/n.layerConfig.lineHeight);var r=this.session.getFoldAt(e.row,0);e.$fold=r;if(r){var s=this.session.lineWidgets;e.row==r.end.row&&!s[r.start.row]?s[r.start.row]=e:e.hidden=!0}return this.session._emit("changeFold",{data:{start:{row:e.row}}}),this.$updateRows(),this.renderWidgets(null,n),this.onWidgetChanged(e),e},this.removeLineWidget=function(e){e._inDocument=!1,e.session=null,e.el&&e.el.parentNode&&e.el.parentNode.removeChild(e.el);if(e.editor&&e.editor.destroy)try{e.editor.destroy()}catch(t){}if(this.session.lineWidgets){var n=this.session.lineWidgets[e.row];if(n==e)this.session.lineWidgets[e.row]=e.$oldWidget,e.$oldWidget&&this.onWidgetChanged(e.$oldWidget);else while(n){if(n.$oldWidget==e){n.$oldWidget=e.$oldWidget;break}n=n.$oldWidget}}this.session._emit("changeFold",{data:{start:{row:e.row}}}),this.$updateRows()},this.getWidgetsAtRow=function(e){var t=this.session.lineWidgets,n=t&&t[e],r=[];while(n)r.push(n),n=n.$oldWidget;return r},this.onWidgetChanged=function(e){this.session._changedWidgets.push(e),this.editor&&this.editor.renderer.updateFull()},this.measureWidgets=function(e,t){var n=this.session._changedWidgets,r=t.layerConfig;if(!n||!n.length)return;var i=Infinity;for(var s=0;s<n.length;s++){var o=n[s];if(!o||!o.el)continue;if(o.session!=this.session)continue;if(!o._inDocument){if(this.session.lineWidgets[o.row]!=o)continue;o._inDocument=!0,t.container.appendChild(o.el)}o.h=o.el.offsetHeight,o.fixedWidth||(o.w=o.el.offsetWidth,o.screenWidth=Math.ceil(o.w/r.characterWidth));var u=o.h/r.lineHeight;o.coverLine&&(u-=this.session.getRowLineCount(o.row),u<0&&(u=0)),o.rowCount!=u&&(o.rowCount=u,o.row<i&&(i=o.row))}i!=Infinity&&(this.session._emit("changeFold",{data:{start:{row:i}}}),this.session.lineWidgetWidth=null),this.session._changedWidgets=[]},this.renderWidgets=function(e,t){var n=t.layerConfig,r=this.session.lineWidgets;if(!r)return;var i=Math.min(this.firstRow,n.firstRow),s=Math.max(this.lastRow,n.lastRow,r.length);while(i>0&&!r[i])i--;this.firstRow=n.firstRow,this.lastRow=n.lastRow,t.$cursorLayer.config=n;for(var o=i;o<=s;o++){var u=r[o];if(!u||!u.el)continue;if(u.hidden){u.el.style.top=-100-(u.pixelHeight||0)+"px";continue}u._inDocument||(u._inDocument=!0,t.container.appendChild(u.el));var a=t.$cursorLayer.getPixelPosition({row:o,column:0},!0).top;u.coverLine||(a+=n.lineHeight*this.session.getRowLineCount(u.row)),u.el.style.top=a-n.offset+"px";var f=u.coverGutter?0:t.gutterWidth;u.fixedWidth||(f-=t.scrollLeft),u.el.style.left=f+"px",u.fullWidth&&u.screenWidth&&(u.el.style.minWidth=n.width+2*n.padding+"px"),u.fixedWidth?u.el.style.right=t.scrollBar.getWidth()+"px":u.el.style.right=""}}}).call(o.prototype),t.LineWidgets=o}),define("ace/ext/error_marker",["require","exports","module","ace/line_widgets","ace/lib/dom","ace/range"],function(e,t,n){"use strict";function o(e,t,n){var r=0,i=e.length-1;while(r<=i){var s=r+i>>1,o=n(t,e[s]);if(o>0)r=s+1;else{if(!(o<0))return s;i=s-1}}return-(r+1)}function u(e,t,n){var r=e.getAnnotations().sort(s.comparePoints);if(!r.length)return;var i=o(r,{row:t,column:-1},s.comparePoints);i<0&&(i=-i-1),i>=r.length?i=n>0?0:r.length-1:i===0&&n<0&&(i=r.length-1);var u=r[i];if(!u||!n)return;if(u.row===t){do u=r[i+=n];while(u&&u.row===t);if(!u)return r.slice()}var a=[];t=u.row;do a[n<0?"unshift":"push"](u),u=r[i+=n];while(u&&u.row==t);return a.length&&a}var r=e("../line_widgets").LineWidgets,i=e("../lib/dom"),s=e("../range").Range;t.showErrorMarker=function(e,t){var n=e.session;n.widgetManager||(n.widgetManager=new r(n),n.widgetManager.attach(e));var s=e.getCursorPosition(),o=s.row,a=n.widgetManager.getWidgetsAtRow(o).filter(function(e){return e.type=="errorMarker"})[0];a?a.destroy():o-=t;var f=u(n,o,t),l;if(f){var c=f[0];s.column=(c.pos&&typeof c.column!="number"?c.pos.sc:c.column)||0,s.row=c.row,l=e.renderer.$gutterLayer.$annotations[s.row]}else{if(a)return;l={text:["Looks good!"],className:"ace_ok"}}e.session.unfold(s.row),e.selection.moveToPosition(s);var h={row:s.row,fixedWidth:!0,coverGutter:!0,el:i.createElement("div"),type:"errorMarker"},p=h.el.appendChild(i.createElement("div")),d=h.el.appendChild(i.createElement("div"));d.className="error_widget_arrow "+l.className;var v=e.renderer.$cursorLayer.getPixelPosition(s).left;d.style.left=v+e.renderer.gutterWidth-5+"px",h.el.className="error_widget_wrapper",p.className="error_widget "+l.className,p.innerHTML=l.text.join("<br>"),p.appendChild(i.createElement("div"));var m=function(e,t,n){if(t===0&&(n==="esc"||n==="return"))return h.destroy(),{command:"null"}};h.destroy=function(){if(e.$mouseHandler.isMousePressed)return;e.keyBinding.removeKeyboardHandler(m),n.widgetManager.removeLineWidget(h),e.off("changeSelection",h.destroy),e.off("changeSession",h.destroy),e.off("mouseup",h.destroy),e.off("change",h.destroy)},e.keyBinding.addKeyboardHandler(m),e.on("changeSelection",h.destroy),e.on("changeSession",h.destroy),e.on("mouseup",h.destroy),e.on("change",h.destroy),e.session.widgetManager.addLineWidget(h),h.el.onmousedown=e.focus.bind(e),e.renderer.scrollCursorIntoView(null,.5,{bottom:h.el.offsetHeight})},i.importCssString(" .error_widget_wrapper { background: inherit; color: inherit; border:none } .error_widget { border-top: solid 2px; border-bottom: solid 2px; margin: 5px 0; padding: 10px 40px; white-space: pre-wrap; } .error_widget.ace_error, .error_widget_arrow.ace_error{ border-color: #ff5a5a } .error_widget.ace_warning, .error_widget_arrow.ace_warning{ border-color: #F1D817 } .error_widget.ace_info, .error_widget_arrow.ace_info{ border-color: #5a5a5a } .error_widget.ace_ok, .error_widget_arrow.ace_ok{ border-color: #5aaa5a } .error_widget_arrow { position: absolute; border: solid 5px; border-top-color: transparent!important; border-right-color: transparent!important; border-left-color: transparent!important; top: -5px; }","")}),define("ace/ace",["require","exports","module","ace/lib/fixoldbrowsers","ace/lib/dom","ace/lib/event","ace/editor","ace/edit_session","ace/undomanager","ace/virtual_renderer","ace/worker/worker_client","ace/keyboard/hash_handler","ace/placeholder","ace/multi_select","ace/mode/folding/fold_mode","ace/theme/textmate","ace/ext/error_marker","ace/config"],function(e,t,n){"use strict";e("./lib/fixoldbrowsers");var r=e("./lib/dom"),i=e("./lib/event"),s=e("./editor").Editor,o=e("./edit_session").EditSession,u=e("./undomanager").UndoManager,a=e("./virtual_renderer").VirtualRenderer;e("./worker/worker_client"),e("./keyboard/hash_handler"),e("./placeholder"),e("./multi_select"),e("./mode/folding/fold_mode"),e("./theme/textmate"),e("./ext/error_marker"),t.config=e("./config"),t.require=e,t.edit=function(e){if(typeof e=="string"){var n=e;e=document.getElementById(n);if(!e)throw new Error("ace.edit can't find div #"+n)}if(e&&e.env&&e.env.editor instanceof s)return e.env.editor;var o="";if(e&&/input|textarea/i.test(e.tagName)){var u=e;o=u.value,e=r.createElement("pre"),u.parentNode.replaceChild(e,u)}else e&&(o=r.getInnerText(e),e.innerHTML="");var f=t.createEditSession(o),l=new s(new a(e));l.setSession(f);var c={document:f,editor:l,onResize:l.resize.bind(l,null)};return u&&(c.textarea=u),i.addListener(window,"resize",c.onResize),l.on("destroy",function(){i.removeListener(window,"resize",c.onResize),c.editor.container.env=null}),l.container.env=l.env=c,l},t.createEditSession=function(e,t){var n=new o(e,t);return n.setUndoManager(new u),n},t.EditSession=o,t.UndoManager=u,t.version="1.2.3"});
+ (function() {
+ window.require(["ace/ace"], function(a) {
+ a && a.config.init(true);
+ if (!window.ace)
+ window.ace = a;
+ for (var key in a) if (a.hasOwnProperty(key))
+ window.ace[key] = a[key];
+ });
+ })();
+
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/mode-javascript.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/mode-javascript.js
new file mode 100644
index 0000000..d2b278f
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/mode-javascript.js
@@ -0,0 +1 @@
+define("ace/mode/doc_comment_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},s.getTagRule(),{defaultToken:"comment.doc",caseInsensitive:!0}]}};r.inherits(s,i),s.getTagRule=function(e){return{token:"comment.doc.tag.storage.type",regex:"\\b(?:TODO|FIXME|XXX|HACK)\\b"}},s.getStartRule=function(e){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:e}},s.getEndRule=function(e){return{token:"comment.doc",regex:"\\*\\/",next:e}},t.DocCommentHighlightRules=s}),define("ace/mode/javascript_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";function a(){var e=o.replace("\\d","\\d\\-"),t={onMatch:function(e,t,n){var r=e.charAt(1)=="/"?2:1;if(r==1)t!=this.nextState?n.unshift(this.next,this.nextState,0):n.unshift(this.next),n[2]++;else if(r==2&&t==this.nextState){n[1]--;if(!n[1]||n[1]<0)n.shift(),n.shift()}return[{type:"meta.tag.punctuation."+(r==1?"":"end-")+"tag-open.xml",value:e.slice(0,r)},{type:"meta.tag.tag-name.xml",value:e.substr(r)}]},regex:"</?"+e+"",next:"jsxAttributes",nextState:"jsx"};this.$rules.start.unshift(t);var n={regex:"{",token:"paren.quasi.start",push:"start"};this.$rules.jsx=[n,t,{include:"reference"},{defaultToken:"string"}],this.$rules.jsxAttributes=[{token:"meta.tag.punctuation.tag-close.xml",regex:"/?>",onMatch:function(e,t,n){return t==n[0]&&n.shift(),e.length==2&&(n[0]==this.nextState&&n[1]--,(!n[1]||n[1]<0)&&n.splice(0,2)),this.next=n[0]||"start",[{type:this.token,value:e}]},nextState:"jsx"},n,f("jsxAttributes"),{token:"entity.other.attribute-name.xml",regex:e},{token:"keyword.operator.attribute-equals.xml",regex:"="},{token:"text.tag-whitespace.xml",regex:"\\s+"},{token:"string.attribute-value.xml",regex:"'",stateName:"jsx_attr_q",push:[{token:"string.attribute-value.xml",regex:"'",next:"pop"},{include:"reference"},{defaultToken:"string.attribute-value.xml"}]},{token:"string.attribute-value.xml",regex:'"',stateName:"jsx_attr_qq",push:[{token:"string.attribute-value.xml",regex:'"',next:"pop"},{include:"reference"},{defaultToken:"string.attribute-value.xml"}]},t],this.$rules.reference=[{token:"constant.language.escape.reference.xml",regex:"(?:&#[0-9]+;)|(?:&#x[0-9a-fA-F]+;)|(?:&[a-zA-Z0-9_:\\.-]+;)"}]}function f(e){return[{token:"comment",regex:/\/\*/,next:[i.getTagRule(),{token:"comment",regex:"\\*\\/",next:e||"pop"},{defaultToken:"comment",caseInsensitive:!0}]},{token:"comment",regex:"\\/\\/",next:[i.getTagRule(),{token:"comment",regex:"$|^",next:e||"pop"},{defaultToken:"comment",caseInsensitive:!0}]}]}var r=e("../lib/oop"),i=e("./doc_comment_highlight_rules").DocCommentHighlightRules,s=e("./text_highlight_rules").TextHighlightRules,o="[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*",u=function(e){var t=this.createKeywordMapper({"variable.language":"Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|Namespace|QName|XML|XMLList|ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|Error|EvalError|InternalError|RangeError|ReferenceError|StopIteration|SyntaxError|TypeError|URIError|decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|isNaN|parseFloat|parseInt|JSON|Math|this|arguments|prototype|window|document",keyword:"const|yield|import|get|set|async|await|break|case|catch|continue|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|debugger|__parent__|__count__|escape|unescape|with|__proto__|class|enum|extends|super|export|implements|private|public|interface|package|protected|static","storage.type":"const|let|var|function","constant.language":"null|Infinity|NaN|undefined","support.function":"alert","constant.language.boolean":"true|false"},"identifier"),n="case|do|else|finally|in|instanceof|return|throw|try|typeof|yield|void",r="\\\\(?:x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|u{[0-9a-fA-F]{1,6}}|[0-2][0-7]{0,2}|3[0-7][0-7]?|[4-7][0-7]?|.)";this.$rules={no_regex:[i.getStartRule("doc-start"),f("no_regex"),{token:"string",regex:"'(?=.)",next:"qstring"},{token:"string",regex:'"(?=.)',next:"qqstring"},{token:"constant.numeric",regex:/0(?:[xX][0-9a-fA-F]+|[bB][01]+)\b/},{token:"constant.numeric",regex:/[+-]?\d[\d_]*(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?\b/},{token:["storage.type","punctuation.operator","support.function","punctuation.operator","entity.name.function","text","keyword.operator"],regex:"("+o+")(\\.)(prototype)(\\.)("+o+")(\\s*)(=)",next:"function_arguments"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","paren.lparen"],regex:"("+o+")(\\.)("+o+")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["entity.name.function","text","keyword.operator","text","storage.type","text","paren.lparen"],regex:"("+o+")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","entity.name.function","text","paren.lparen"],regex:"("+o+")(\\.)("+o+")(\\s*)(=)(\\s*)(function)(\\s+)(\\w+)(\\s*)(\\()",next:"function_arguments"},{token:["storage.type","text","entity.name.function","text","paren.lparen"],regex:"(function)(\\s+)("+o+")(\\s*)(\\()",next:"function_arguments"},{token:["entity.name.function","text","punctuation.operator","text","storage.type","text","paren.lparen"],regex:"("+o+")(\\s*)(:)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:["text","text","storage.type","text","paren.lparen"],regex:"(:)(\\s*)(function)(\\s*)(\\()",next:"function_arguments"},{token:"keyword",regex:"(?:"+n+")\\b",next:"start"},{token:["support.constant"],regex:/that\b/},{token:["storage.type","punctuation.operator","support.function.firebug"],regex:/(console)(\.)(warn|info|log|error|time|trace|timeEnd|assert)\b/},{token:t,regex:o},{token:"punctuation.operator",regex:/[.](?![.])/,next:"property"},{token:"keyword.operator",regex:/--|\+\+|\.{3}|===|==|=|!=|!==|<+=?|>+=?|!|&&|\|\||\?\:|[!$%&*+\-~\/^]=?/,next:"start"},{token:"punctuation.operator",regex:/[?:,;.]/,next:"start"},{token:"paren.lparen",regex:/[\[({]/,next:"start"},{token:"paren.rparen",regex:/[\])}]/},{token:"comment",regex:/^#!.*$/}],property:[{token:"text",regex:"\\s+"},{token:["storage.type","punctuation.operator","entity.name.function","text","keyword.operator","text","storage.type","text","entity.name.function","text","paren.lparen"],regex:"("+o+")(\\.)("+o+")(\\s*)(=)(\\s*)(function)(?:(\\s+)(\\w+))?(\\s*)(\\()",next:"function_arguments"},{token:"punctuation.operator",regex:/[.](?![.])/},{token:"support.function",regex:/(s(?:h(?:ift|ow(?:Mod(?:elessDialog|alDialog)|Help))|croll(?:X|By(?:Pages|Lines)?|Y|To)?|t(?:op|rike)|i(?:n|zeToContent|debar|gnText)|ort|u(?:p|b(?:str(?:ing)?)?)|pli(?:ce|t)|e(?:nd|t(?:Re(?:sizable|questHeader)|M(?:i(?:nutes|lliseconds)|onth)|Seconds|Ho(?:tKeys|urs)|Year|Cursor|Time(?:out)?|Interval|ZOptions|Date|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Date|FullYear)|FullYear|Active)|arch)|qrt|lice|avePreferences|mall)|h(?:ome|andleEvent)|navigate|c(?:har(?:CodeAt|At)|o(?:s|n(?:cat|textual|firm)|mpile)|eil|lear(?:Timeout|Interval)?|a(?:ptureEvents|ll)|reate(?:StyleSheet|Popup|EventObject))|t(?:o(?:GMTString|S(?:tring|ource)|U(?:TCString|pperCase)|Lo(?:caleString|werCase))|est|a(?:n|int(?:Enabled)?))|i(?:s(?:NaN|Finite)|ndexOf|talics)|d(?:isableExternalCapture|ump|etachEvent)|u(?:n(?:shift|taint|escape|watch)|pdateCommands)|j(?:oin|avaEnabled)|p(?:o(?:p|w)|ush|lugins.refresh|a(?:ddings|rse(?:Int|Float)?)|r(?:int|ompt|eference))|e(?:scape|nableExternalCapture|val|lementFromPoint|x(?:p|ec(?:Script|Command)?))|valueOf|UTC|queryCommand(?:State|Indeterm|Enabled|Value)|f(?:i(?:nd|le(?:ModifiedDate|Size|CreatedDate|UpdatedDate)|xed)|o(?:nt(?:size|color)|rward)|loor|romCharCode)|watch|l(?:ink|o(?:ad|g)|astIndexOf)|a(?:sin|nchor|cos|t(?:tachEvent|ob|an(?:2)?)|pply|lert|b(?:s|ort))|r(?:ou(?:nd|teEvents)|e(?:size(?:By|To)|calc|turnValue|place|verse|l(?:oad|ease(?:Capture|Events)))|andom)|g(?:o|et(?:ResponseHeader|M(?:i(?:nutes|lliseconds)|onth)|Se(?:conds|lection)|Hours|Year|Time(?:zoneOffset)?|Da(?:y|te)|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Da(?:y|te)|FullYear)|FullYear|A(?:ttention|llResponseHeaders)))|m(?:in|ove(?:B(?:y|elow)|To(?:Absolute)?|Above)|ergeAttributes|a(?:tch|rgins|x))|b(?:toa|ig|o(?:ld|rderWidths)|link|ack))\b(?=\()/},{token:"support.function.dom",regex:/(s(?:ub(?:stringData|mit)|plitText|e(?:t(?:NamedItem|Attribute(?:Node)?)|lect))|has(?:ChildNodes|Feature)|namedItem|c(?:l(?:ick|o(?:se|neNode))|reate(?:C(?:omment|DATASection|aption)|T(?:Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(?:ntityReference|lement)|Attribute))|tabIndex|i(?:nsert(?:Row|Before|Cell|Data)|tem)|open|delete(?:Row|C(?:ell|aption)|T(?:Head|Foot)|Data)|focus|write(?:ln)?|a(?:dd|ppend(?:Child|Data))|re(?:set|place(?:Child|Data)|move(?:NamedItem|Child|Attribute(?:Node)?)?)|get(?:NamedItem|Element(?:sBy(?:Name|TagName|ClassName)|ById)|Attribute(?:Node)?)|blur)\b(?=\()/},{token:"support.constant",regex:/(s(?:ystemLanguage|cr(?:ipts|ollbars|een(?:X|Y|Top|Left))|t(?:yle(?:Sheets)?|atus(?:Text|bar)?)|ibling(?:Below|Above)|ource|uffixes|e(?:curity(?:Policy)?|l(?:ection|f)))|h(?:istory|ost(?:name)?|as(?:h|Focus))|y|X(?:MLDocument|SLDocument)|n(?:ext|ame(?:space(?:s|URI)|Prop))|M(?:IN_VALUE|AX_VALUE)|c(?:haracterSet|o(?:n(?:structor|trollers)|okieEnabled|lorDepth|mp(?:onents|lete))|urrent|puClass|l(?:i(?:p(?:boardData)?|entInformation)|osed|asses)|alle(?:e|r)|rypto)|t(?:o(?:olbar|p)|ext(?:Transform|Indent|Decoration|Align)|ags)|SQRT(?:1_2|2)|i(?:n(?:ner(?:Height|Width)|put)|ds|gnoreCase)|zIndex|o(?:scpu|n(?:readystatechange|Line)|uter(?:Height|Width)|p(?:sProfile|ener)|ffscreenBuffering)|NEGATIVE_INFINITY|d(?:i(?:splay|alog(?:Height|Top|Width|Left|Arguments)|rectories)|e(?:scription|fault(?:Status|Ch(?:ecked|arset)|View)))|u(?:ser(?:Profile|Language|Agent)|n(?:iqueID|defined)|pdateInterval)|_content|p(?:ixelDepth|ort|ersonalbar|kcs11|l(?:ugins|atform)|a(?:thname|dding(?:Right|Bottom|Top|Left)|rent(?:Window|Layer)?|ge(?:X(?:Offset)?|Y(?:Offset)?))|r(?:o(?:to(?:col|type)|duct(?:Sub)?|mpter)|e(?:vious|fix)))|e(?:n(?:coding|abledPlugin)|x(?:ternal|pando)|mbeds)|v(?:isibility|endor(?:Sub)?|Linkcolor)|URLUnencoded|P(?:I|OSITIVE_INFINITY)|f(?:ilename|o(?:nt(?:Size|Family|Weight)|rmName)|rame(?:s|Element)|gColor)|E|whiteSpace|l(?:i(?:stStyleType|n(?:eHeight|kColor))|o(?:ca(?:tion(?:bar)?|lName)|wsrc)|e(?:ngth|ft(?:Context)?)|a(?:st(?:M(?:odified|atch)|Index|Paren)|yer(?:s|X)|nguage))|a(?:pp(?:MinorVersion|Name|Co(?:deName|re)|Version)|vail(?:Height|Top|Width|Left)|ll|r(?:ity|guments)|Linkcolor|bove)|r(?:ight(?:Context)?|e(?:sponse(?:XML|Text)|adyState))|global|x|m(?:imeTypes|ultiline|enubar|argin(?:Right|Bottom|Top|Left))|L(?:N(?:10|2)|OG(?:10E|2E))|b(?:o(?:ttom|rder(?:Width|RightWidth|BottomWidth|Style|Color|TopWidth|LeftWidth))|ufferDepth|elow|ackground(?:Color|Image)))\b/},{token:"identifier",regex:o},{regex:"",token:"empty",next:"no_regex"}],start:[i.getStartRule("doc-start"),f("start"),{token:"string.regexp",regex:"\\/",next:"regex"},{token:"text",regex:"\\s+|^$",next:"start"},{token:"empty",regex:"",next:"no_regex"}],regex:[{token:"regexp.keyword.operator",regex:"\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"},{token:"string.regexp",regex:"/[sxngimy]*",next:"no_regex"},{token:"invalid",regex:/\{\d+\b,?\d*\}[+*]|[+*$^?][+*]|[$^][?]|\?{3,}/},{token:"constant.language.escape",regex:/\(\?[:=!]|\)|\{\d+\b,?\d*\}|[+*]\?|[()$^+*?.]/},{token:"constant.language.delimiter",regex:/\|/},{token:"constant.language.escape",regex:/\[\^?/,next:"regex_character_class"},{token:"empty",regex:"$",next:"no_regex"},{defaultToken:"string.regexp"}],regex_character_class:[{token:"regexp.charclass.keyword.operator",regex:"\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"},{token:"constant.language.escape",regex:"]",next:"regex"},{token:"constant.language.escape",regex:"-"},{token:"empty",regex:"$",next:"no_regex"},{defaultToken:"string.regexp.charachterclass"}],function_arguments:[{token:"variable.parameter",regex:o},{token:"punctuation.operator",regex:"[, ]+"},{token:"punctuation.operator",regex:"$"},{token:"empty",regex:"",next:"no_regex"}],qqstring:[{token:"constant.language.escape",regex:r},{token:"string",regex:"\\\\$",next:"qqstring"},{token:"string",regex:'"|$',next:"no_regex"},{defaultToken:"string"}],qstring:[{token:"constant.language.escape",regex:r},{token:"string",regex:"\\\\$",next:"qstring"},{token:"string",regex:"'|$",next:"no_regex"},{defaultToken:"string"}]};if(!e||!e.noES6)this.$rules.no_regex.unshift({regex:"[{}]",onMatch:function(e,t,n){this.next=e=="{"?this.nextState:"";if(e=="{"&&n.length)n.unshift("start",t);else if(e=="}"&&n.length){n.shift(),this.next=n.shift();if(this.next.indexOf("string")!=-1||this.next.indexOf("jsx")!=-1)return"paren.quasi.end"}return e=="{"?"paren.lparen":"paren.rparen"},nextState:"start"},{token:"string.quasi.start",regex:/`/,push:[{token:"constant.language.escape",regex:r},{token:"paren.quasi.start",regex:/\${/,push:"start"},{token:"string.quasi.end",regex:/`/,next:"pop"},{defaultToken:"string.quasi"}]}),(!e||!e.noJSX)&&a.call(this);this.embedRules(i,"doc-",[i.getEndRule("no_regex")]),this.normalizeRules()};r.inherits(u,s),t.JavaScriptHighlightRules=u}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("../range").Range,i=function(){};(function(){this.checkOutdent=function(e,t){return/^\s+$/.test(e)?/^\s*\}/.test(t):!1},this.autoOutdent=function(e,t){var n=e.getLine(t),i=n.match(/^(\s*\})/);if(!i)return 0;var s=i[1].length,o=e.findMatchingBracket({row:t,column:s});if(!o||o.row==t)return 0;var u=this.$getIndent(e.getLine(o.row));e.replace(new r(t,0,t,s-1),u)},this.$getIndent=function(e){return e.match(/^\s*/)[0]}}).call(i.prototype),t.MatchingBraceOutdent=i}),define("ace/mode/behaviour/cstyle",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../behaviour").Behaviour,s=e("../../token_iterator").TokenIterator,o=e("../../lib/lang"),u=["text","paren.rparen","punctuation.operator"],a=["text","paren.rparen","punctuation.operator","comment"],f,l={},c=function(e){var t=-1;e.multiSelect&&(t=e.selection.index,l.rangeCount!=e.multiSelect.rangeCount&&(l={rangeCount:e.multiSelect.rangeCount}));if(l[t])return f=l[t];f=l[t]={autoInsertedBrackets:0,autoInsertedRow:-1,autoInsertedLineEnd:"",maybeInsertedBrackets:0,maybeInsertedRow:-1,maybeInsertedLineStart:"",maybeInsertedLineEnd:""}},h=function(e,t,n,r){var i=e.end.row-e.start.row;return{text:n+t+r,selection:[0,e.start.column+1,i,e.end.column+(i?0:1)]}},p=function(){this.add("braces","insertion",function(e,t,n,r,i){var s=n.getCursorPosition(),u=r.doc.getLine(s.row);if(i=="{"){c(n);var a=n.getSelectionRange(),l=r.doc.getTextRange(a);if(l!==""&&l!=="{"&&n.getWrapBehavioursEnabled())return h(a,l,"{","}");if(p.isSaneInsertion(n,r))return/[\]\}\)]/.test(u[s.column])||n.inMultiSelectMode?(p.recordAutoInsert(n,r,"}"),{text:"{}",selection:[1,1]}):(p.recordMaybeInsert(n,r,"{"),{text:"{",selection:[1,1]})}else if(i=="}"){c(n);var d=u.substring(s.column,s.column+1);if(d=="}"){var v=r.$findOpeningBracket("}",{column:s.column+1,row:s.row});if(v!==null&&p.isAutoInsertedClosing(s,u,i))return p.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}else{if(i=="\n"||i=="\r\n"){c(n);var m="";p.isMaybeInsertedClosing(s,u)&&(m=o.stringRepeat("}",f.maybeInsertedBrackets),p.clearMaybeInsertedClosing());var d=u.substring(s.column,s.column+1);if(d==="}"){var g=r.findMatchingBracket({row:s.row,column:s.column+1},"}");if(!g)return null;var y=this.$getIndent(r.getLine(g.row))}else{if(!m){p.clearMaybeInsertedClosing();return}var y=this.$getIndent(u)}var b=y+r.getTabString();return{text:"\n"+b+"\n"+y+m,selection:[1,b.length,1,b.length]}}p.clearMaybeInsertedClosing()}}),this.add("braces","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="{"){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.end.column,i.end.column+1);if(u=="}")return i.end.column++,i;f.maybeInsertedBrackets--}}),this.add("parens","insertion",function(e,t,n,r,i){if(i=="("){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return h(s,o,"(",")");if(p.isSaneInsertion(n,r))return p.recordAutoInsert(n,r,")"),{text:"()",selection:[1,1]}}else if(i==")"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f==")"){var l=r.$findOpeningBracket(")",{column:u.column+1,row:u.row});if(l!==null&&p.isAutoInsertedClosing(u,a,i))return p.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("parens","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="("){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==")")return i.end.column++,i}}),this.add("brackets","insertion",function(e,t,n,r,i){if(i=="["){c(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return h(s,o,"[","]");if(p.isSaneInsertion(n,r))return p.recordAutoInsert(n,r,"]"),{text:"[]",selection:[1,1]}}else if(i=="]"){c(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f=="]"){var l=r.$findOpeningBracket("]",{column:u.column+1,row:u.row});if(l!==null&&p.isAutoInsertedClosing(u,a,i))return p.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("brackets","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="["){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u=="]")return i.end.column++,i}}),this.add("string_dquotes","insertion",function(e,t,n,r,i){if(i=='"'||i=="'"){c(n);var s=i,o=n.getSelectionRange(),u=r.doc.getTextRange(o);if(u!==""&&u!=="'"&&u!='"'&&n.getWrapBehavioursEnabled())return h(o,u,s,s);if(!u){var a=n.getCursorPosition(),f=r.doc.getLine(a.row),l=f.substring(a.column-1,a.column),p=f.substring(a.column,a.column+1),d=r.getTokenAt(a.row,a.column),v=r.getTokenAt(a.row,a.column+1);if(l=="\\"&&d&&/escape/.test(d.type))return null;var m=d&&/string|escape/.test(d.type),g=!v||/string|escape/.test(v.type),y;if(p==s)y=m!==g;else{if(m&&!g)return null;if(m&&g)return null;var b=r.$mode.tokenRe;b.lastIndex=0;var w=b.test(l);b.lastIndex=0;var E=b.test(l);if(w||E)return null;if(p&&!/[\s;,.})\]\\]/.test(p))return null;y=!0}return{text:y?s+s:"",selection:[1,1]}}}}),this.add("string_dquotes","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&(s=='"'||s=="'")){c(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==s)return i.end.column++,i}})};p.isSaneInsertion=function(e,t){var n=e.getCursorPosition(),r=new s(t,n.row,n.column);if(!this.$matchTokenType(r.getCurrentToken()||"text",u)){var i=new s(t,n.row,n.column+1);if(!this.$matchTokenType(i.getCurrentToken()||"text",u))return!1}return r.stepForward(),r.getCurrentTokenRow()!==n.row||this.$matchTokenType(r.getCurrentToken()||"text",a)},p.$matchTokenType=function(e,t){return t.indexOf(e.type||e)>-1},p.recordAutoInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isAutoInsertedClosing(r,i,f.autoInsertedLineEnd[0])||(f.autoInsertedBrackets=0),f.autoInsertedRow=r.row,f.autoInsertedLineEnd=n+i.substr(r.column),f.autoInsertedBrackets++},p.recordMaybeInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isMaybeInsertedClosing(r,i)||(f.maybeInsertedBrackets=0),f.maybeInsertedRow=r.row,f.maybeInsertedLineStart=i.substr(0,r.column)+n,f.maybeInsertedLineEnd=i.substr(r.column),f.maybeInsertedBrackets++},p.isAutoInsertedClosing=function(e,t,n){return f.autoInsertedBrackets>0&&e.row===f.autoInsertedRow&&n===f.autoInsertedLineEnd[0]&&t.substr(e.column)===f.autoInsertedLineEnd},p.isMaybeInsertedClosing=function(e,t){return f.maybeInsertedBrackets>0&&e.row===f.maybeInsertedRow&&t.substr(e.column)===f.maybeInsertedLineEnd&&t.substr(0,e.column)==f.maybeInsertedLineStart},p.popAutoInsertedClosing=function(){f.autoInsertedLineEnd=f.autoInsertedLineEnd.substr(1),f.autoInsertedBrackets--},p.clearMaybeInsertedClosing=function(){f&&(f.maybeInsertedBrackets=0,f.maybeInsertedRow=-1)},r.inherits(p,i),t.CstyleBehaviour=p}),define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.singleLineBlockCommentRe=/^\s*(\/\*).*\*\/\s*$/,this.tripleStarBlockCommentRe=/^\s*(\/\*\*\*).*\*\/\s*$/,this.startRegionRe=/^\s*(\/\*|\/\/)#?region\b/,this._getFoldWidgetBase=this.getFoldWidget,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);if(this.singleLineBlockCommentRe.test(r)&&!this.startRegionRe.test(r)&&!this.tripleStarBlockCommentRe.test(r))return"";var i=this._getFoldWidgetBase(e,t,n);return!i&&this.startRegionRe.test(r)?"start":i},this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n);if(this.startRegionRe.test(i))return this.getCommentRegionBlock(e,i,n);var s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++t<a){n=e.getLine(t);var f=n.search(/\S/);if(f===-1)continue;if(r>f)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)},this.getCommentRegionBlock=function(e,t,n){var r=t.search(/\s*$/),s=e.getLength(),o=n,u=/^\s*(?:\/\*|\/\/|--)#?(end)?region\b/,a=1;while(++n<s){t=e.getLine(n);var f=u.exec(t);if(!f)continue;f[1]?a--:a++;if(!a)break}var l=n;if(l>o)return new i(o,r,l,t.length)}}.call(o.prototype)}),define("ace/mode/javascript",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/worker/worker_client","ace/mode/behaviour/cstyle","ace/mode/folding/cstyle"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./javascript_highlight_rules").JavaScriptHighlightRules,o=e("./matching_brace_outdent").MatchingBraceOutdent,u=e("../range").Range,a=e("../worker/worker_client").WorkerClient,f=e("./behaviour/cstyle").CstyleBehaviour,l=e("./folding/cstyle").FoldMode,c=function(){this.HighlightRules=s,this.$outdent=new o,this.$behaviour=new f,this.foldingRules=new l};r.inherits(c,i),function(){this.lineCommentStart="//",this.blockComment={start:"/*",end:"*/"},this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t),i=this.getTokenizer().getLineTokens(t,e),s=i.tokens,o=i.state;if(s.length&&s[s.length-1].type=="comment")return r;if(e=="start"||e=="no_regex"){var u=t.match(/^.*(?:\bcase\b.*\:|[\{\(\[])\s*$/);u&&(r+=n)}else if(e=="doc-start"){if(o=="start"||o=="no_regex")return"";var u=t.match(/^\s*(\/?)\*/);u&&(u[1]&&(r+=" "),r+="* ")}return r},this.checkOutdent=function(e,t,n){return this.$outdent.checkOutdent(t,n)},this.autoOutdent=function(e,t,n){this.$outdent.autoOutdent(t,n)},this.createWorker=function(e){var t=new a(["ace"],"ace/mode/javascript_worker","JavaScriptWorker");return t.attachToDocument(e.getDocument()),t.on("annotate",function(t){e.setAnnotations(t.data)}),t.on("terminate",function(){e.clearAnnotations()}),t},this.$id="ace/mode/javascript"}.call(c.prototype),t.Mode=c})
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/ui-ace.min.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/ui-ace.min.js
new file mode 100644
index 0000000..b914093
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/lib/ace/ui-ace.min.js
@@ -0,0 +1,7 @@
+/**
+ * angular-ui-ace - This directive allows you to add ACE editor elements.
+ * @version v0.2.3 - 2015-01-29
+ * @link http://angular-ui.github.com
+ * @license MIT
+ */
+"use strict";angular.module("ui.ace",[]).constant("uiAceConfig",{}).directive("uiAce",["uiAceConfig",function(a){if(angular.isUndefined(window.ace))throw new Error("ui-ace need ace to work... (o rly?)");var b=function(a,b,c){if(angular.isDefined(c.workerPath)){var d=window.ace.require("ace/config");d.set("workerPath",c.workerPath)}angular.isDefined(c.require)&&c.require.forEach(function(a){window.ace.require(a)}),angular.isDefined(c.showGutter)&&a.renderer.setShowGutter(c.showGutter),angular.isDefined(c.useWrapMode)&&b.setUseWrapMode(c.useWrapMode),angular.isDefined(c.showInvisibles)&&a.renderer.setShowInvisibles(c.showInvisibles),angular.isDefined(c.showIndentGuides)&&a.renderer.setDisplayIndentGuides(c.showIndentGuides),angular.isDefined(c.useSoftTabs)&&b.setUseSoftTabs(c.useSoftTabs),angular.isDefined(c.showPrintMargin)&&a.setShowPrintMargin(c.showPrintMargin),angular.isDefined(c.disableSearch)&&c.disableSearch&&a.commands.addCommands([{name:"unfind",bindKey:{win:"Ctrl-F",mac:"Command-F"},exec:function(){return!1},readOnly:!0}]),angular.isString(c.theme)&&a.setTheme("ace/theme/"+c.theme),angular.isString(c.mode)&&b.setMode("ace/mode/"+c.mode),angular.isDefined(c.firstLineNumber)&&(angular.isNumber(c.firstLineNumber)?b.setOption("firstLineNumber",c.firstLineNumber):angular.isFunction(c.firstLineNumber)&&b.setOption("firstLineNumber",c.firstLineNumber()));var e,f;if(angular.isDefined(c.advanced))for(e in c.advanced)f={name:e,value:c.advanced[e]},a.setOption(f.name,f.value);if(angular.isDefined(c.rendererOptions))for(e in c.rendererOptions)f={name:e,value:c.rendererOptions[e]},a.renderer.setOption(f.name,f.value);angular.forEach(c.callbacks,function(b){angular.isFunction(b)&&b(a)})};return{restrict:"EA",require:"?ngModel",link:function(c,d,e,f){var g,h,i=a.ace||{},j=angular.extend({},i,c.$eval(e.uiAce)),k=window.ace.edit(d[0]),l=k.getSession(),m=function(){var a=arguments[0],b=Array.prototype.slice.call(arguments,1);angular.isDefined(a)&&c.$evalAsync(function(){if(!angular.isFunction(a))throw new Error("ui-ace use a function as callback.");a(b)})},n={onChange:function(a){return function(b){var d=l.getValue();!f||d===f.$viewValue||c.$$phase||c.$root.$$phase||c.$evalAsync(function(){f.$setViewValue(d)}),m(a,b,k)}},onBlur:function(a){return function(){m(a,k)}}};e.$observe("readonly",function(a){k.setReadOnly(!!a||""===a)}),f&&(f.$formatters.push(function(a){if(angular.isUndefined(a)||null===a)return"";if(angular.isObject(a)||angular.isArray(a))throw new Error("ui-ace cannot use an object or an array as a model");return a}),f.$render=function(){l.setValue(f.$viewValue)});var o=function(a,d){a!==d&&(j=angular.extend({},i,c.$eval(e.uiAce)),j.callbacks=[j.onLoad],j.onLoad!==i.onLoad&&j.callbacks.unshift(i.onLoad),l.removeListener("change",g),g=n.onChange(j.onChange),l.on("change",g),k.removeListener("blur",h),h=n.onBlur(j.onBlur),k.on("blur",h),b(k,l,j))};c.$watch(e.uiAce,o,!0),o(i),d.on("$destroy",function(){k.session.$stopWorker(),k.destroy()}),c.$watch(function(){return[d[0].offsetWidth,d[0].offsetHeight]},function(){k.resize(),k.renderer.updateFull()},!0)}}}]);
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 01472c4..a5c229a 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -970,8 +970,19 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
return false;
}
+ function configureAuthorizationServices() {
+ if ($scope.client.authorizationServicesEnabled) {
+ if ($scope.accessType == 'public') {
+ $scope.accessType = 'confidential';
+ }
+ $scope.client.publicClient = false;
+ $scope.client.serviceAccountsEnabled = true;
+ }
+ }
+
$scope.$watch('client', function() {
$scope.changed = isChanged();
+ configureAuthorizationServices();
}, true);
$scope.$watch('newRedirectUri', function() {
@@ -1067,9 +1078,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
realm : realm.realm,
client : client.id
}, $scope.client, function() {
- $scope.changed = false;
- client = angular.copy($scope.client);
- $location.url("/realms/" + realm.realm + "/clients/" + client.id);
+ $route.reload();
Notifications.success("Your changes have been saved to the client.");
});
}
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 54c8000..65f45b6 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -44,6 +44,10 @@ module.controller('GlobalCtrl', function($scope, $http, Auth, Current, $location
return getAccess('view-identity-providers') || getAccess('manage-identity-providers') || this.manageIdentityProviders;
},
+ get viewAuthorization() {
+ return getAccess('view-authorization') || this.manageAuthorization;
+ },
+
get manageRealm() {
return getAccess('manage-realm');
},
@@ -64,6 +68,10 @@ module.controller('GlobalCtrl', function($scope, $http, Auth, Current, $location
return getAccess('manage-identity-providers');
},
+ get manageAuthorization() {
+ return getAccess('manage-authorization');
+ },
+
get impersonation() {
return getAccess('impersonation');
}
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-resource-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-resource-detail.html
new file mode 100644
index 0000000..9b05faf
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-resource-detail.html
@@ -0,0 +1,101 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission">Permissions</a></li>
+ <li data-ng-show="create">Add Resource Permission</li>
+ <li data-ng-hide="create">{{policy.name}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">Add Resource Permission</h1>
+ <h1 data-ng-hide="create">{{policy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name <span class="required">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required>
+ </div>
+ <kc-tooltip>The name of this permission.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Description </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+ </div>
+ <kc-tooltip>A description for this permission.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="policy.config.default">Apply to Resource Type</label>
+ <div class="col-md-6">
+ <input ng-model="policy.config.default" id="policy.config.default" onoffswitch data-ng-click="applyToResourceType()"/>
+ </div>
+ <kc-tooltip>Specifies if this permission would be applied to all resources with a given type. In this case, this permission will be evaluated for all instances
+ of a given resource type.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-hide="policy.config.default">
+ <label class="col-md-2 control-label" for="reqActions">Resources <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2="{ minimumInputLength: 1}" id="reqActions" data-ng-model="policy.config.resources" data-placeholder="Select a resource..." multiple data-ng-required="!policy.config.default">
+ <option ng-repeat="resource in resources" value="{{resource._id}}" ng-selected="true">{{resource.name}}</option>
+ </select>
+ </div>
+ <kc-tooltip>Specifies that this permission must be applied to a specific resource instance.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="policy.config.default">
+ <label class="col-md-2 control-label" for="policy.config.defaultResourceType">Resource Type <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <input class="form-control" type="text" id="policy.config.defaultResourceType" name="policy.config.defaultResourceType" data-ng-model="policy.config.defaultResourceType" data-ng-required="policy.config.default">
+ </div>
+
+ <kc-tooltip>Specifies that this permission must be applied to all resources instances of a given type.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="reqActions">Apply Policy <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2="{ minimumInputLength: 1}" id="reqActions" data-ng-model="policy.config.applyPolicies" data-placeholder="Select a policy..." multiple required>
+ <option ng-repeat="policy in policies" value="{{policy.id}}" ng-selected="true">{{policy.name}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>Specifies all the policies that must be applied to the resource type or instances defined by this permission.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="policy.decisionStrategy">Decision Strategy</label>
+
+ <div class="col-md-6">
+ <select class="form-control" id="policy.decisionStrategy"
+ data-ng-model="policy.decisionStrategy"
+ ng-change="selectDecisionStrategy()">
+ <option ng-repeat="strategy in decisionStrategies" value="{{strategy}}">{{strategy | toCamelCase}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The decision strategy dictates how the policies associated with a given permission are evaluated and how a final decision is obtained.
+ 'Affirmative' means that at least one policy must evaluate to a positive decision in order to the overall decision be also positive.
+ 'Unanimous' means that all policies must evaluate to a positive decision in order to the overall decision be also positive.
+ 'Consensus' means 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.</kc-tooltip>
+ </div>
+ <input type="hidden" data-ng-model="policy.type"/>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html
new file mode 100644
index 0000000..36ae53c
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/provider/resource-server-policy-scope-detail.html
@@ -0,0 +1,113 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission">Permissions</a></li>
+ <li data-ng-show="create">Add Scope Permission</li>
+ <li data-ng-hide="create">{{policy.name}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">Add Scope Permission</h1>
+ <h1 data-ng-hide="create">{{policy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name <span class="required">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required>
+ </div>
+ <kc-tooltip>The name of this permission.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Description </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+ </div>
+ <kc-tooltip>A description for this permission.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="reqActions">Resource</label>
+
+ <div class="col-md-6">
+ <select class="form-control" id="reqActions"
+ ng-model="policy.config.resources"
+ ng-change="resolveScopes(policy)"
+ data-ng-options="resource._id as resource.name for resource in resources">
+ <option value="">Any resource...</option>
+ </select>
+ </div>
+ <kc-tooltip>Restrict the scopes to those associated with the selected resource. If not selected all scopes would be available.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="policy.config.resources">
+ <label class="col-md-2 control-label" for="reqActions">Scopes <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2 id="reqActions"
+ data-ng-model="policy.config.scopes"
+ data-placeholder="Any scope..." multiple
+ data-ng-required="policy.config.resources != ''"
+ data-ng-options="scope.id as scope.name for scope in scopes track by scope.id"/>
+ </div>
+
+ <kc-tooltip>Specifies that this permission must be applied to one or more scopes.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="!policy.config.resources">
+ <label class="col-md-2 control-label" for="reqActions">Scopes <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2="{ minimumInputLength: 1}" id="reqActions"
+ data-ng-model="policy.config.scopes"
+ data-placeholder="Any scope..." multiple
+ data-ng-required="policy.config.resources == ''"
+ data-ng-options="scope.id as scope.name for scope in scopes track by scope.id"/>
+ </select>
+ </div>
+ <kc-tooltip>Specifies that this permission must be applied to one or more scopes.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="reqActions">Apply Policy <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2 id="reqActions" data-ng-model="policy.config.applyPolicies" data-placeholder="Select a policy..." multiple required>
+ <option ng-repeat="policy in policies" value="{{policy.id}}" ng-selected="true">{{policy.name}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>Specifies all the policies that must be applied to the scopes defined by this permission.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="policy.decisionStrategy">Decision Strategy</label>
+
+ <div class="col-md-6">
+ <select class="form-control" id="policy.decisionStrategy"
+ data-ng-model="policy.decisionStrategy"
+ ng-change="selectDecisionStrategy()">
+ <option ng-repeat="strategy in decisionStrategies" value="{{strategy}}">{{strategy}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The decision strategy dictates how the policies associated with a given permission are evaluated and how a final decision is obtained.
+ 'Affirmative' means that at least one policy must evaluate to a positive decision in order to the overall decision be also positive.
+ 'Unanimous' means that all policies must evaluate to a positive decision in order to the overall decision be also positive.
+ 'Consensus' means 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.</kc-tooltip>
+ </div>
+ <input type="hidden" data-ng-model="policy.type"/>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
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
new file mode 100644
index 0000000..35d471d
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/permission/resource-server-permission-list.html
@@ -0,0 +1,53 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <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">
+ <div class="form-inline">
+ <div class="form-group">
+ Filter by:
+ <div class="input-group">
+ <input type="text" placeholder="Name" data-ng-model="search.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ </div>
+ <div class="input-group">
+ <select class="form-control search" data-ng-model="search.type"
+ ng-options="p.type as p.name group by p.group for p in policyProviders track by p.type">
+ <option value="" selected ng-click="search.type = ''">All types</option>
+ </select>
+ </div>
+ </div>
+ <div class="pull-right">
+ <select class="form-control" ng-model="policyType"
+ ng-options="p.name for p in policyProviders track by p.type"
+ data-ng-change="addPolicy(policyType);">
+ <option value="" disabled selected>Create permission...</option>
+ </select>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="policies.length == 0">
+ <th>Permission Name</th>
+ <th>Description</th>
+ <th>Type</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="policy in policies | filter: {name: search.name, type: search.type} | orderBy:'name'">
+ <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>
+ </tr>
+ <tr data-ng-show="(policies | filter:search).length == 0">
+ <td class="text-muted" colspan="3" data-ng-show="search.name">No results</td>
+ <td class="text-muted" colspan="3" data-ng-hide="search.name">No permissions available</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html
new file mode 100644
index 0000000..2cb27e3
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-aggregate-detail.html
@@ -0,0 +1,89 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">Policies</a></li>
+ <li data-ng-show="create">Add Aggregate Policy</li>
+ <li data-ng-hide="create">Aggregated</li>
+ <li data-ng-hide="create">{{policy.name}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">Add Aggregate Policy</h1>
+ <h1 data-ng-hide="create">{{policy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
+ data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name <span class="required">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required>
+ </div>
+ <kc-tooltip>The name of this policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Description </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+ </div>
+ <kc-tooltip>A description for this policy.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="reqActions">Apply Policy <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2 id="reqActions" data-ng-model="policy.config.applyPolicies" data-placeholder="Select a policy..." multiple required>
+ <option ng-repeat="policy in policies" value="{{policy.id}}" ng-selected="true">{{policy.name}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>Specifies all the policies that must be applied to the scopes defined by this policy.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="policy.decisionStrategy">Decision Strategy</label>
+
+ <div class="col-md-6">
+ <select class="form-control" id="policy.decisionStrategy"
+ data-ng-model="policy.decisionStrategy"
+ ng-change="selectDecisionStrategy()">
+ <option ng-repeat="strategy in decisionStrategies" value="{{strategy}}">{{strategy}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The decision strategy dictates how the policies associated with a given policy are evaluated and how a final decision is obtained.
+ 'Affirmative' means that at least one policy must evaluate to a positive decision in order to the overall decision be also positive.
+ 'Unanimous' means that all policies must evaluate to a positive decision in order to the overall decision be also positive.
+ 'Consensus' means 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.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="policy.logic">Logic</label>
+
+ <div class="col-sm-1">
+ <select class="form-control" id="policy.logic"
+ data-ng-model="policy.logic">
+ <option ng-repeat="logic in logics" value="{{logic}}">{{logic | toCamelCase}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The logic dictates how the policy decision should be made. If 'Positive', the resulting effect (permit or deny) obtained during the evaluation of this policy will
+ be used to perform a decision. If 'Negative', the resulting effect will be negated, in other words, a permit becomes a deny and vice-versa.
+ </div>
+ <input type="hidden" data-ng-model="policy.type"/>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-drools-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-drools-detail.html
new file mode 100644
index 0000000..debd0ca
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-drools-detail.html
@@ -0,0 +1,128 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">Policies</a></li>
+ <li data-ng-show="create">Add Drools Policy</li>
+ <li data-ng-hide="create">Drools</li>
+ <li data-ng-hide="create">{{policy.name}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">Add Drools Policy</h1>
+ <h1 data-ng-hide="create">{{policy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
+ data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name <span class="required" data-ng-show="create">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required>
+ </div>
+ <kc-tooltip>The name of this policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Description </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+ </div>
+ <kc-tooltip>A description for this policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="policy.config.mavenArtifactGroupId">Policy Maven Artifact <span class="required" data-ng-show="create">*</span></label>
+ <button data-ng-click="resolveModules()" class="btn btn-primary">Resolve</button>
+ <div class="col-sm-3">
+ <input class="form-control" type="text" id="policy.config.mavenArtifactGroupId" name="policy.config.mavenArtifactGroupId" data-ng-model="policy.config.mavenArtifactGroupId" placeholder="Group Identifier" required>
+ </div>
+ <kc-tooltip>A Maven GAV pointing to an artifact from where the rules would be loaded from. Once you have provided the GAV, you can click *Resolve* to load both *Module* and *Session* fields.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="policy.config.mavenArtifactId"></label>
+ <div class="col-sm-3">
+ <input class="form-control" type="text" id="policy.config.mavenArtifactId" name="policy.config.mavenArtifactId" data-ng-model="policy.config.mavenArtifactId" autofocus placeholder="Artifact Identifier" required>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="policy.config.mavenArtifactVersion"></label>
+ <div class="col-sm-3">
+ <input class="form-control" type="text" id="policy.config.mavenArtifactVersion" name="policy.config.mavenArtifactVersion" data-ng-model="policy.config.mavenArtifactVersion" autofocus placeholder="Version" required>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="policy.config.moduleName">Module <span class="required" data-ng-show="create">*</span></label>
+ <div class="col-sm-3">
+ <div>
+ <select class="form-control" id="policy.config.moduleName"
+ ng-model="policy.config.moduleName"
+ ng-options="moduleName as moduleName for moduleName in drools.moduleNames"
+ ng-change="resolveSessions()"
+ ng-disabled="!drools.moduleNames.length"
+ required>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>The module used by this policy. You must provide a module in order to select a specific session from where rules will be loaded from.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="policy.config.sessionName">Session <span class="required" data-ng-show="create">*</span></label>
+ <div class="col-sm-3">
+ <div>
+ <select class="form-control" id="policy.config.sessionName"
+ ng-model="policy.config.sessionName"
+ ng-options="sessionName as sessionName for sessionName in drools.moduleSessions"
+ ng-disabled="!drools.moduleSessions.length"
+ required>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>The session used by this policy. The session provides all the rules to evaluate when processing the policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="policy.config.scannerPeriod">Update Period</label>
+ <div class="col-md-6 time-selector">
+ <input class="form-control" type="number" required min="1" max="31536000" data-ng-model="policy.config.scannerPeriod" id="policy.config.scannerPeriod"
+ name="policy.config.scannerPeriod"
+ ng-disabled="!policy.config.sessionName"/>
+ <select class="form-control" name="policy.config.scannerPeriodUnit"
+ data-ng-model="policy.config.scannerPeriodUnit"
+ ng-disabled="!policy.config.sessionName">
+ <option>Seconds</option>
+ <option>Minutes</option>
+ <option>Hours</option>
+ <option>Days</option>
+ </select>
+ </div>
+ <kc-tooltip>Specifies an interval for scanning for artifact updates.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="policy.logic">Logic</label>
+
+ <div class="col-sm-1">
+ <select class="form-control" id="policy.logic"
+ data-ng-model="policy.logic">
+ <option ng-repeat="logic in logics" value="{{logic}}">{{logic | toCamelCase}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The logic dictates how the policy decision should be made. If 'Positive', the resulting effect (permit or deny) obtained during the evaluation of this policy will
+ be used to perform a decision. If 'Negative', the resulting effect will be negated, in other words, a permit becomes a deny and vice-versa.
+ </div>
+ <input type="hidden" data-ng-model="policy.type"/>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-js-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-js-detail.html
new file mode 100644
index 0000000..1153ded
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-js-detail.html
@@ -0,0 +1,71 @@
+<style>
+ .ace_editor { height: 200px; }
+</style>
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">Policies</a></li>
+ <li data-ng-show="create">Add JS Policy</li>
+ <li data-ng-hide="create">JavaScript</li>
+ <li data-ng-hide="create">{{policy.name}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">Add JS Policy</h1>
+ <h1 data-ng-hide="create">{{policy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name <span class="required">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required>
+ </div>
+ <kc-tooltip>The name of this policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Description </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+ </div>
+ <kc-tooltip>A description for this policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Code </label>
+ <div class="col-sm-6">
+ <div ui-ace="{ onLoad : initEditor }" data-ng-model="policy.config.code"></div>
+ </div>
+ <kc-tooltip>The JavaScript code providing the conditions for this policy.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="policy.logic">Logic</label>
+
+ <div class="col-sm-1">
+ <select class="form-control" id="policy.logic"
+ data-ng-model="policy.logic">
+ <option ng-repeat="logic in logics" value="{{logic}}">{{logic | toCamelCase}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The logic dictates how the policy decision should be made. If 'Positive', the resulting effect (permit or deny) obtained during the evaluation of this policy will
+ be used to perform a decision. If 'Negative', the resulting effect will be negated, in other words, a permit becomes a deny and vice-versa.
+ </div>
+ <input type="hidden" data-ng-model="policy.type"/>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-role-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-role-detail.html
new file mode 100644
index 0000000..a716730
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-role-detail.html
@@ -0,0 +1,115 @@
+<!--
+ ~ JBoss, Home of Professional Open Source.
+ ~ Copyright 2016 Red Hat, Inc., and individual contributors
+ ~ as indicated by the @author tags.
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">Policies</a></li>
+ <li data-ng-show="create">Add Role Policy</li>
+ <li data-ng-hide="create">Role</li>
+ <li data-ng-hide="create">{{policy.name}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">Add Role Policy</h1>
+ <h1 data-ng-hide="create">{{policy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
+ data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name <span class="required">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required>
+ </div>
+ <kc-tooltip>The name of this policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Description </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+ </div>
+ <kc-tooltip>A description for this policy.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="roles">Roles <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2="{ minimumInputLength: 1}" id="roles" data-ng-model="selectedRole" data-ng-change="selectRole(selectedRole);" data-placeholder="Select a role..."
+ ng-options="role as role.name for role in roles" data-ng-required="selectedUsers.length == 0 && selectedRoles.length == 0">
+ </select>
+ </div>
+
+ <kc-tooltip>Specifies which role(s) are allowed by this policy.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" style="margin-top: -15px;">
+ <label class="col-md-2 control-label"></label>
+ <div class="col-sm-3">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr data-ng-hide="!selectedRoles.length">
+ <th>Role name</th>
+ <th>Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="role in selectedRoles | orderBy:'name'">
+ <td>{{role.name}}</td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm" ng-click="removeFromList(selectedRoles, $index);">Remove</button>
+ </td>
+ </tr>
+ <tr data-ng-show="!selectedRoles.length">
+ <td class="text-muted" colspan="3">No roles assigned.</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="policy.logic">Logic</label>
+
+ <div class="col-sm-1">
+ <select class="form-control" id="policy.logic"
+ data-ng-model="policy.logic">
+ <option ng-repeat="logic in logics" value="{{logic}}">{{logic | toCamelCase}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The logic dictates how the policy decision should be made. If 'Positive', the resulting effect (permit or deny) obtained during the evaluation of this policy will
+ be used to perform a decision. If 'Negative', the resulting effect will be negated, in other words, a permit becomes a deny and vice-versa.
+ </div>
+ <input type="hidden" data-ng-model="policy.type"/>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-time-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-time-detail.html
new file mode 100644
index 0000000..0247687
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-time-detail.html
@@ -0,0 +1,81 @@
+<style>
+ .ace_editor { height: 200px; }
+</style>
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">Policies</a></li>
+ <li data-ng-show="create">Add Time Policy</li>
+ <li data-ng-hide="create">Time</li>
+ <li data-ng-hide="create">{{policy.name}}</li>
+ </ol>
+
+
+ <h1 data-ng-show="create">Add Time Policy</h1>
+ <h1 data-ng-hide="create">{{policy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name <span class="required">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required>
+ </div>
+ <kc-tooltip>The name of this policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Description </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+ </div>
+ <kc-tooltip>A description for this policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="policy.config.nbf">Not Before</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" style="width: 150px" type="text" id="policy.config.nbf" name="notBefore" data-ng-model="policy.config.nbf" placeholder="yyyy-MM-dd hh:mm:ss">
+ </div>
+ <kc-tooltip>Defines the time before which the policy MUST NOT be granted. Only granted if current date/time is after or equal to this value.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="policy.config.noa">Not On or After</label>
+
+ <div class="col-md-6 time-selector">
+ <input class="form-control" style="width: 150px" type="text" id="policy.config.noa" name="policy.config.noa" data-ng-model="policy.config.noa" placeholder="yyyy-MM-dd hh:mm:ss">
+ </div>
+ <kc-tooltip>Defines the time after which the policy MUST NOT be granted. Only granted if current date/time is before or equal to this value.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="policy.logic">Logic</label>
+
+ <div class="col-sm-1">
+ <select class="form-control" id="policy.logic"
+ data-ng-model="policy.logic">
+ <option ng-repeat="logic in logics" value="{{logic}}">{{logic | toCamelCase}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The logic dictates how the policy decision should be made. If 'Positive', the resulting effect (permit or deny) obtained during the evaluation of this policy will
+ be used to perform a decision. If 'Negative', the resulting effect will be negated, in other words, a permit becomes a deny and vice-versa.
+ </div>
+ <input type="hidden" data-ng-model="policy.type"/>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-user-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-user-detail.html
new file mode 100644
index 0000000..0678aab
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-user-detail.html
@@ -0,0 +1,96 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">Policies</a></li>
+ <li data-ng-show="create">Add User Policy</li>
+ <li data-ng-hide="create">User</li>
+ <li data-ng-hide="create">{{policy.name}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">Add User Policy</h1>
+ <h1 data-ng-hide="create">{{policy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
+ data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name <span class="required">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required>
+ </div>
+ <kc-tooltip>The name of this policy.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="description">Description </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+ </div>
+ <kc-tooltip>A description for this policy.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="users">Users <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2="{ minimumInputLength: 1}" id="users" data-ng-model="selectedUser" data-ng-change="selectUser(selectedUser);" data-placeholder="Select an user..."
+ ng-options="user as user.username for user in users" data-ng-required="selectedRoles.length == 0">
+ </select>
+ </div>
+ <kc-tooltip>Specifies which user(s) are allowed by this policy.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" style="margin-top: -15px;">
+ <label class="col-md-2 control-label"></label>
+ <div class="col-sm-3">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr data-ng-hide="!selectedUsers.length">
+ <th>Username</th>
+ <th>Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="user in selectedUsers | orderBy:'username'">
+ <td>{{user.username}}</td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm" ng-click="removeFromList(selectedUsers, $index);">Remove</button>
+ </td>
+ </tr>
+ <tr data-ng-show="!selectedUsers.length">
+ <td class="text-muted" colspan="3">No users assigned.</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="policy.logic">Logic</label>
+
+ <div class="col-sm-1">
+ <select class="form-control" id="policy.logic"
+ data-ng-model="policy.logic">
+ <option ng-repeat="logic in logics" value="{{logic}}">{{logic | toCamelCase}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The logic dictates how the policy decision should be made. If 'Positive', the resulting effect (permit or deny) obtained during the evaluation of this policy will
+ be used to perform a decision. If 'Negative', the resulting effect will be negated, in other words, a permit becomes a deny and vice-versa.
+ </div>
+ <input type="hidden" data-ng-model="policy.type"/>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html
new file mode 100644
index 0000000..ebd976b
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate.html
@@ -0,0 +1,283 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/evaluate">Policy Evaluation</a></li>
+ </ol>
+
+ <kc-tabs-resource-server></kc-tabs-resource-server>
+
+ <div data-ng-show="showResult">
+ <br>
+ <a href="" data-ng-click="showRequestTab()">New Evaluation</a>
+ </div>
+
+ <div data-ng-show="evaluationResult && !showResult">
+ <br>
+ <a href="" data-ng-click="showResultTab()">Previous Result</a>
+ </div>
+
+ <div data-ng-hide="showResult">
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset>
+ <fieldset class="border-top">
+ <legend><span class="text">Identity Information</span>
+ <kc-tooltip>The available options to configure the identity information that will be used when evaluating policies.</kc-tooltip>
+ </legend>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="client">Client</label>
+
+ <div class="col-sm-2">
+ <div>
+ <select class="form-control" id="client"
+ ng-model="authzRequest.clientId"
+ ng-options="client.id as client.clientId for client in clients track by client.id">
+ <option value="">Select a client...</option>
+ </select>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>A resource server is an already existing client application. In this case, the
+ client application will also act as a resource server in order to have its resources managed
+ and protected.
+ </kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="users">User <span class="required"
+ data-ng-show="!authzRequest.roleIds || authzRequest.roleIds.length == 0">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2="{ minimumInputLength: 1, allowClear:true}" id="users"
+ data-ng-model="authzRequest.userId" data-placeholder="Select an user..."
+ ng-options="user.id as user.username for user in users track by user.id"
+ data-ng-required="!authzRequest.roleIds || authzRequest.roleIds.length == 0">
+ <option value=""></option>
+ </select>
+ </div>
+
+ <kc-tooltip>Specifies which user(s) are allowed by this policy.</kc-tooltip>
+ </div>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2">
+ <button class="btn btn-primary" data-ng-click="entitlements()" data-ng-disabled="authzRequest.userId == null || authzRequest.clientId == null">Entitlements</button>
+ </div>
+ </div>
+
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="reqActions">Roles <span class="required"
+ data-ng-show="!authzRequest.userId || authzRequest.userId == null">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2="{ minimumInputLength: 1}"
+ data-ng-model="authzRequest.roleIds"
+ data-placeholder="Any role..." multiple
+ data-ng-required="!authzRequest.userId || authzRequest.userId == null">
+ <option ng-repeat="role in roles track by role.id" value="{{role.name}}">{{role.name}}
+ </option>
+ </select>
+ </div>
+
+ <kc-tooltip>Specifies that this policy must be applied to one or more scopes.</kc-tooltip>
+ </div>
+ </fieldset>
+ <fieldset>
+ <legend collapsed><span class="text">Contextual Information</span>
+ <kc-tooltip>The available options to configure any contextual information that will be used when evaluating policies.</kc-tooltip>
+ </legend>
+ <div class="form-group clearfix block">
+ <label class="col-md-2 control-label" for="newRedirectUri">Contextual Attributes</label>
+
+ <div class="col-sm-6">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th>Key</th>
+ <th>Value</th>
+ <th>Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="(key, value) in (authzRequest.context.attributes)">
+ <td>{{getContextAttributeName(key)}}</td>
+ <td>
+ <select class="form-control" id="attribute-{{key}}"
+ data-ng-model="authzRequest.context.attributes[key]"
+ data-ng-show="getContextAttribute(key).values"
+ ng-options="value1.key as value1.name for value1 in getContextAttribute(key).values">
+ </select>
+ <input ng-model="authzRequest.context.attributes[key]" class="form-control"
+ type="text" name="{{key}}" id="attribute-{{key}}"
+ data-ng-hide="getContextAttribute(key).values"/>
+ </td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm"
+ data-ng-click="removeContextAttribute(key)">Delete
+ </button>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <select class="form-control" id="newContextAttribute.key"
+ data-ng-model="newContextAttribute"
+ ng-change="selectDefaultContextAttribute()"
+ data-ng-hide="!isDefaultContextAttribute()"
+ ng-options="attribute as attribute.name for attribute in defaultContextAttributes track by attribute.key">
+ </select>
+ <input ng-model="newContextAttribute.key" class="form-control" type="text"
+ id="newAttributeKey" data-ng-hide="isDefaultContextAttribute()"/>
+ </td>
+ <td>
+ <select class="form-control" id="newContextAttribute.value"
+ data-ng-model="newContextAttribute.value"
+ data-ng-show="newContextAttribute.values"
+ ng-options="value.key as value.name for value in newContextAttribute.values track by value.key">
+ </select>
+ <input ng-model="newContextAttribute.value" class="form-control" type="text"
+ id="newAttributeValue" data-ng-show="!newContextAttribute.values"/>
+ </td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm"
+ data-ng-click="addContextAttribute()"
+ data-ng-disabled="!newContextAttribute.key || newContextAttribute.key == ''">
+ Add
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <kc-tooltip>Any attribute provided by a running environment or execution context.</kc-tooltip>
+ </div>
+ </fieldset>
+ <fieldset>
+ <legend><span class="text">Permission</span>
+ <kc-tooltip>The available options to configure the permissions to which policies will be applied.</kc-tooltip>
+ </legend>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="applyResourceType">Apply to Resource Type</label>
+
+ <div class="col-md-6">
+ <input ng-model="applyResourceType" id="applyResourceType" onoffswitch
+ data-ng-click="setApplyToResourceType()"/>
+ </div>
+ <kc-tooltip>Specifies if this policy must be applied to all resources with a given type. In this
+ case, this policy will be evaluated for all instances
+ of a given resource type.
+ </kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-hide="applyResourceType">
+ <label class="col-md-2 control-label" for="reqActions">Resources <span class="required">*</span></label>
+
+ <div class="col-md-6">
+ <select ui-select2="{ minimumInputLength: 1, allowClear:true }"
+ ng-model="newResource._id"
+ data-placeholder="Select a resource..."
+ data-ng-required="!applyResourceType && authzRequest.resources.length == 0 && !authzRequest.entitlements"
+ data-ng-click="resolveScopes()"
+ ng-options="resource._id as resource.name for resource in resources track by resource._id">
+ <option value=""></option>
+ </select>
+ </div>
+ <kc-tooltip>Specifies that this policy must be applied to a specific resource instance.
+ </kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="applyResourceType">
+ <label class="col-md-2 control-label" for="newResource.type">Resource Type <span
+ class="required">*</span></label>
+
+ <div class="col-md-6">
+ <input class="form-control" type="text" id="newResource.type" name="newResource.type"
+ data-ng-model="authzRequest.resources[0].type"
+ data-ng-required="applyResourceType && !authzRequest.resources[0].type && !authzRequest.entitlements">
+ </div>
+
+ <kc-tooltip>Specifies that this policy must be applied to all resources instances of a given
+ type.
+ </kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="applyResourceType || newResource._id == null">
+ <label class="col-md-2 control-label" for="newResource.scopes">Scopes</label>
+
+ <div class="col-md-6">
+ <select ui-select2="{ minimumInputLength: 1}"
+ id="newResource.scopes"
+ multiple
+ data-ng-model="newResource.scopes"
+ data-placeholder="Select a scope..."
+ data-ng-options="scope.name as scope.name for scope in scopes track by scope.name"/>
+ </div>
+
+ <kc-tooltip>Specifies that this policy must be applied to one or more scopes.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix" data-ng-show="newResource._id != null">
+ <label class="col-md-2 control-label" for="newResource.scopes">Scopes</label>
+
+ <div class="col-md-6">
+ <select ui-select2
+ id="newResource.scopes"
+ data-ng-model="newResource.scopes"
+ data-placeholder="Any scope..." multiple>
+ <option ng-repeat="scope in scopes" value="{{scope.name}}">{{scope.name}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>Specifies that this policy must be applied to one or more scopes.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix block" data-ng-show="!applyResourceType">
+ <label class="col-md-2 control-label" for="newRedirectUri"></label>
+
+ <div class="col-sm-6">
+ <button data-ng-click="addResource()" class="btn btn-primary">Add</button>
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th>Resource</th>
+ <th>Scopes</th>
+ <th>Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr data-ng-show="!authzRequest.resources || authzRequest.resources.length == 0">
+ <td colspan="3">
+ No resources.
+ </td>
+ </tr>
+ <tr ng-repeat="resource in authzRequest.resources">
+ <td>{{resource.name ? resource.name : 'Any resource with scope(s)'}}</td>
+ <td>
+ <span data-ng-show="!resource.scopes.length">Any scope.</span>
+ <span data-ng-show="resource.scopes.length > 0">
+ <span ng-repeat="scope in resource.scopes">
+ {{scope}} {{$last ? '' : ', '}}
+ </span>
+ </span>
+ </td>
+ <td class="kc-action-cell">
+ <button class="btn btn-default btn-block btn-sm"
+ data-ng-click="removeResource($index)">Delete
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2">
+ <button kc-save data-ng-click="evaluate()">Evaluate</button>
+ <button kc-reset data-ng-disabled="!changed">Reset</button>
+ </div>
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ <div data-ng-include="resultUrl" data-ng-show="showResult"/>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html
new file mode 100644
index 0000000..a9ff3f4
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-evaluate-result.html
@@ -0,0 +1,68 @@
+<fieldset>
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <span data-ng-show="evaluationResult.results.length == 0"><strong>Could not obtain any result for the given authorization request. Check if the provided resource(s) or scope(s) are associated with any policy.</strong></span>
+ <fieldset class="border-top" data-ng-repeat="result in evaluationResult.results">
+ <legend collapsed><span class="text">{{result.resource.name}}</span>
+ <kc-tooltip>Provides information about how policies were evaluated for this resource.</kc-tooltip>
+ </legend>
+ <div class="form-group">
+ <label class="col-md-2 control-label">Result</label>
+
+ <div class="col-sm-2">
+ <div>
+ <span style="color: green"
+ data-ng-show="result.status == 'PERMIT'"><strong>{{result.status}}</strong></span>
+ <span style="color: red"
+ data-ng-hide="result.status == 'PERMIT'"><strong>{{result.status}}</strong></span>
+ </div>
+ </div>
+ <kc-tooltip>The overall result for this permission request.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label">Scopes</label>
+
+ <div class="col-sm-2">
+ <span data-ng-show="result.scopes.length == 0">Any scope.</span>
+
+ <div>
+ <ul>
+ <li data-ng-repeat="scope in result.scopes">
+ {{scope.name}}
+ </li>
+ </ul>
+ </div>
+ </div>
+ <kc-tooltip>The requested scopes.</kc-tooltip>
+ </div>
+ <div class="form-group" data-ng-show="!evaluationResult.entitlements">
+ <label class="col-md-2 control-label">Policies</label>
+
+ <div class="col-sm-6">
+ <span data-ng-show="result.policies.length == 0">No policies were found for this resource.</span>
+
+ <div>
+ <ul>
+ <li data-ng-repeat="policyResult in result.policies">
+ <strong><a
+ href="#/realms/{{realm.realm}}/authz/resource-server/{{server.id}}/policy/{{policyResult.policy.type}}/{{policyResult.policy.id}}">{{policyResult.policy.name}}</a></strong>
+ decision was <span style="color: green" data-ng-show="policyResult.status == 'PERMIT'"><strong>{{policyResult.status}}</strong></span>
+ <span style="color: red" data-ng-hide="policyResult.status == 'PERMIT'"><strong>{{policyResult.status}}</strong></span>
+ by <strong>{{policyResult.policy.decisionStrategy}}</strong> decision.</a>
+ <ul>
+ <li data-ng-repeat="subPolicy in policyResult.associatedPolicies">
+ <strong><a
+ href="#/realms/{{realm.realm}}/authz/resource-server/{{server.id}}/policy/{{subPolicy.policy.type}}/{{subPolicy.policy.id}}">{{subPolicy.policy.name}}</a></strong>
+ voted to <span style="color: green"
+ data-ng-show="subPolicy.status == 'PERMIT'"><strong>{{subPolicy.status}}</strong></span>
+ <span style="color: red" data-ng-hide="subPolicy.status == 'PERMIT'"><strong>{{subPolicy.status}}</strong></span>.</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <kc-tooltip>Details about which policies were evaluated and their decisions.</kc-tooltip>
+ </div>
+ </fieldset>
+ </form>
+</fieldset>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html
new file mode 100644
index 0000000..cb4691c
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/resource-server-policy-list.html
@@ -0,0 +1,53 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <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">
+ <div class="form-inline">
+ <div class="form-group">
+ Filter by:
+ <div class="input-group">
+ <input type="text" placeholder="Name" data-ng-model="search.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ </div>
+ <div class="input-group">
+ <select class="form-control search" data-ng-model="search.type"
+ ng-options="p.type as p.name group by p.group for p in policyProviders track by p.type">
+ <option value="" selected ng-click="search.type = ''">All types</option>
+ </select>
+ </div>
+ </div>
+ <div class="pull-right">
+ <select class="form-control" ng-model="policyType"
+ ng-options="p.name group by p.group for p in policyProviders track by p.type"
+ data-ng-change="addPolicy(policyType);">
+ <option value="" disabled selected>Create policy...</option>
+ </select>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="policies.length == 0">
+ <th>Policy Name</th>
+ <th>Description</th>
+ <th>Type</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="policy in policies | filter: {name: search.name, type: search.type} | orderBy:'name'">
+ <td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy/{{policy.type}}/{{policy.id}}">{{policy.name}}</a></td>
+ <td>{{policy.description}}</td>
+ <td>{{policy.type}}</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>
+ <td class="text-muted" colspan="3" data-ng-hide="search.name">No policies available</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
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
new file mode 100644
index 0000000..918cadf
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-detail.html
@@ -0,0 +1,82 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <kc-tabs-resource-server></kc-tabs-resource-server>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset>
+ <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">
+ <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)">
+ </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>
+ </div>
+ </fieldset>
+ <fieldset class="border-top" data-ng-hide="files.length > 0">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="server.policyEnforcementMode">Policy Enforcement Mode</label>
+ <div class="col-md-2">
+ <select class="form-control" id="server.policyEnforcementMode" data-ng-model="server.policyEnforcementMode">
+ <option value="ENFORCING">Enforcing</option>
+ <option value="PERMISSIVE">Permissive</option>
+ <option value="DISABLED">Disabled</option>
+ </select>
+ </div>
+ <kc-tooltip>The policy enforcement mode dictates how policies are enforced when evaluating authorization requests. 'Enforcing' means requests are denied by default even when there is no policy associated with a given resource. 'Permissive' means requests
+ are allowed even when there is no policy associated with a given resource. 'Disabled' completely disables the evaluation of policies and allow access to any resource.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="server.allowRemoteResourceManagement">Remote Resource Management</label>
+ <div class="col-md-6">
+ <input ng-model="server.allowRemoteResourceManagement" id="server.allowRemoteResourceManagement" onoffswitch />
+ </div>
+ <kc-tooltip>Should resources be managed remotely by the resource server? If false, resources can only be managed from this admin console.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </fieldset>
+
+ <fieldset class="border-top" data-ng-show="!files || files.length == 0">
+ <legend><span class="text">Export Settings</span>
+ <kc-tooltip>Here you can export all settings for this resource server.</kc-tooltip>
+ </legend>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="server.allowRemoteResourceManagement">Export Settings</label>
+ <div class="col-md-6">
+ <button data-ng-click="export()" class="btn btn-primary" data-ng-hide="settings">Export</button>
+ <button data-ng-click="downloadSettings()" class="btn btn-primary" data-ng-show="settings">Download</button>
+ <button data-ng-click="cancelExport()" class="btn btn-primary" data-ng-show="settings">Cancel</button>
+ </div>
+ <kc-tooltip>Export and download all settings for this resource server.</kc-tooltip>
+ </div>
+ <fieldset class="margin-top">
+ <div class="form-group" ng-show="settings">
+ <div class="col-sm-12">
+ <a class="btn btn-primary btn-lg" data-ng-click="download()" type="submit" ng-show="installation">Download</a>
+ <textarea class="form-control" rows="20" kc-select-action="click">{{settings}}</textarea>
+ </div>
+ </div>
+ </fieldset>
+ </fieldset>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-list.html
new file mode 100644
index 0000000..3b4130e
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-list.html
@@ -0,0 +1,49 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+ <h1>
+ <span>Resource Servers</span>
+ <kc-tooltip>Resource Servers are applications serving resources to their users. These resources can be a RESTFul API, web pages or any other kind of resource that must be managed and protected by a set of authorization policies.</kc-tooltip>
+ </h1>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+ <div class="form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="Search..." data-ng-model="search.clientId" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ </div>
+
+ <div class="pull-right">
+ <a id="createServer" class="btn btn-default" href="#/realms/{{realm.realm}}/authz/resource-server/create">Create</a>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="servers.length == 0">
+ <th>Name</th>
+ <th>Policy Enforcement Mode</th>
+ <th>Allows Remote Resource Management ?</th>
+ <th>Allows Entitlement ?</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="server in servers | filter:search | orderBy:'clientId'">
+ <td><a href="#/realms/{{realm.realm}}/authz/resource-server/{{server.id}}">{{server.name}}</a></td>
+ <td>{{server.policyEnforcementMode | toCamelCase}}</td>
+ <td>{{server.allowRemoteResourceManagement}}</td>
+ <td>{{server.allowEntitlements}}</td>
+ </tr>
+ <tr data-ng-show="(servers | filter:search).length == 0">
+ <td class="text-muted" colspan="3" data-ng-show="search.clientId">No results</td>
+ <td class="text-muted" colspan="3" data-ng-hide="search.clientId">No servers available</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
new file mode 100644
index 0000000..0b68b2a
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
@@ -0,0 +1,79 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/resource">Resource</a></li>
+ <li data-ng-show="create">Add Resource</li>
+ <li data-ng-hide="create">{{resource.name}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">Add Resource</h1>
+ <h1 data-ng-hide="create">{{resource.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
+ data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name <span class="required" data-ng-show="create">*</span></label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="resource.name" autofocus required>
+ </div>
+ <kc-tooltip>An unique name for this resource. The name can be used to uniquely identify a resource, useful when querying for a specific resource.</kc-tooltip>
+ </div>
+ <div class="form-group" data-ng-hide="create">
+ <label class="col-md-2 control-label" for="resource.owner.name">Owner </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="resource.owner.name" name="name" data-ng-model="resource.owner.name" autofocus disabled>
+ </div>
+ <kc-tooltip>The owner of this resource.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="type">Type </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="type" name="name" data-ng-model="resource.type" autofocus>
+ </div>
+ <kc-tooltip>The type of this resource. It can be used to group different resource instances with the same type.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="uri">URI </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="uri" name="name" data-ng-model="resource.uri" autofocus>
+ </div>
+ <kc-tooltip>An URI that can also be used to uniquely identify this resource.</kc-tooltip>
+ </div>
+ <div class="form-group clearfix">
+ <label class="col-md-2 control-label" for="reqActions">Scopes</label>
+
+ <div class="col-md-6">
+ <select ui-select2 id="reqActions" ng-model="resource.scopes" data-placeholder="Select an scope..." multiple>
+ <option ng-repeat="scope in scopes" value="{{scope.name}}" ng-selected="true">{{scope.name}}</option>
+ </select>
+ </div>
+
+ <kc-tooltip>The scopes associated with this resource.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="iconUri">Icon URI </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="iconUri" name="name" data-ng-model="resource.icon_uri" autofocus>
+ </div>
+ <kc-tooltip>An URI pointing to an icon for this resource.</kc-tooltip>
+ </div>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html
new file mode 100644
index 0000000..04cccce
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html
@@ -0,0 +1,85 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <kc-tabs-resource-server></kc-tabs-resource-server>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="7">
+ <div class="form-inline">
+ Filter by:
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="Name" data-ng-model="search.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ <div class="input-group">
+ <input type="text" placeholder="Owner" data-ng-model="search.owner.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ <div class="input-group">
+ <select class="form-control search" data-ng-model="search.type"
+ ng-options="r.type as r.type for r in resources | unique : 'type'">
+ <option value="" selected ng-click="search.type = ''">All types</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="pull-right">
+ <a id="createResource" class="btn btn-default" href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/resource/create">Create</a>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="resources.length == 0">
+ <th>Name</th>
+ <th>Type</th>
+ <th>Uri</th>
+ <th>Owner</th>
+ <th>Scopes</th>
+ <th>Permissions</th>
+ <th>Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="resource in resources | filter:search | orderBy:'name'">
+ <td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/resource/{{resource._id}}">{{resource.name}}</a></td>
+ <td>
+ <span data-ng-show="resource.type">{{resource.type}}</span>
+ <span data-ng-show="!resource.type">No type defined.</span>
+ </td>
+ <td>{{resource.uri}}</td>
+ <td>{{resource.owner.name}}</td>
+ <td>
+ <span data-ng-show="!resource.scopes.length">No scopes assigned.</span>
+ <span data-ng-show="resource.scopes.length > 0">
+ <span ng-repeat="scope in resource.scopes">
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope/{{scope.id}}">{{scope.name}}</a>{{$last ? '' : ', '}}
+ </span>
+ </span>
+ </td>
+ <td>
+ <span data-ng-show="!resource.policies.length">No permission assigned.</span>
+ <span data-ng-show="resource.policies.length > 0">
+ <span ng-repeat="policy in resource.policies">
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}//authz/resource-server/permission/{{policy.type}}/{{policy.id}}">{{policy.name}}</a>{{$last ? '' : ', '}}
+ </span>
+ </span>
+ </td>
+ <td class="kc-action-cell" style="vertical-align: middle">
+ <button class="btn btn-default btn-block btn-sm" ng-click="createPolicy(resource);">Create Permission</button>
+ </td>
+ </tr>
+ <tr data-ng-show="(resources | filter:search).length == 0">
+ <td class="text-muted" colspan="6" data-ng-show="search.name">No results</td>
+ <td class="text-muted" colspan="6" data-ng-hide="search.name">No resources available</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-detail.html
new file mode 100644
index 0000000..f215cac
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-detail.html
@@ -0,0 +1,47 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
+ <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope">Scope</a></li>
+ <li data-ng-show="create">Add Scope</li>
+ <li data-ng-hide="create">{{scope.name}}</li>
+ </ol>
+
+ <h1 data-ng-show="create">Add Scope</h1>
+ <h1 data-ng-hide="create">{{scope.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
+ data-ng-hide="changed" data-ng-click="remove()"></i></h1>
+
+ <form class="form-horizontal" name="clientForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Name </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="scope.name" autofocus>
+ </div>
+ <kc-tooltip>An unique name for this scope. The name can be used to uniquely identify a scope, useful when querying for a specific scope.</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="name">Icon URI </label>
+ <div class="col-sm-6">
+ <input class="form-control" type="text" id="name" name="name" data-ng-model="scope.iconUri" autofocus>
+ </div>
+ <kc-tooltip>An URI pointing to an icon for this scope.</kc-tooltip>
+ </div>
+ </fieldset>
+
+ <div class="form-group">
+ <div class="col-md-10 col-md-offset-2" data-ng-show="create">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-cancel data-ng-click="cancel()">Cancel</button>
+ </div>
+ <div class="col-md-10 col-md-offset-2" data-ng-show="!create && access.manageClients">
+ <button kc-save data-ng-disabled="!changed">Save</button>
+ <button kc-reset data-ng-disabled="!changed">Cancel</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html
new file mode 100644
index 0000000..42d6026
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-scope-list.html
@@ -0,0 +1,41 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <kc-tabs-resource-server></kc-tabs-resource-server>
+
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="5">
+ <div class="form-inline">
+ <div class="form-group">
+ <div class="input-group">
+ <input type="text" placeholder="Search..." data-ng-model="search.name" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
+ <div class="input-group-addon">
+ <i class="fa fa-search" type="submit"></i>
+ </div>
+ </div>
+ </div>
+
+ <div class="pull-right">
+ <a id="createScope" class="btn btn-default" href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope/create">Create</a>
+ </div>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-hide="scopes.length == 0">
+ <th>Scope Name</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="scope in scopes | filter:search | orderBy:'name'">
+ <td><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope/{{scope.id}}">{{scope.name}}</a></td>
+ </tr>
+ <tr data-ng-show="(scopes | filter:search).length == 0">
+ <td class="text-muted" colspan="3" data-ng-show="search.name">No results</td>
+ <td class="text-muted" colspan="3" data-ng-hide="search.name">No scopes available</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
index ef1f7b4..8e1f254 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html
@@ -110,6 +110,13 @@
<input ng-model="client.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
+ <div class="form-group" data-ng-show="protocol == 'openid-connect'">
+ <label class="col-md-2 control-label" for="authorizationServicesEnabled">Authorization Enabled</label>
+ <kc-tooltip>Enable/Disable fine-grained authorization support for a client</kc-tooltip>
+ <div class="col-md-6">
+ <input ng-model="client.authorizationServicesEnabled" name="authorizationServicesEnabled" id="authorizationServicesEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
+ </div>
+ </div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlServerSignature">{{:: 'include-authnstatement' | translate}}</label>
<div class="col-sm-6">
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/authz/kc-authz-modal.html b/themes/src/main/resources/theme/base/admin/resources/templates/authz/kc-authz-modal.html
new file mode 100644
index 0000000..18acf86
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/authz/kc-authz-modal.html
@@ -0,0 +1,11 @@
+<div class="modal-header">
+ <button type="button" class="close" ng-click="cancel()">
+ <span class="pficon pficon-close"></span>
+ </button>
+ <h4 class="modal-title">{{title}}</h4>
+</div>
+<div class="modal-body" ng-bind-html="message"></div>
+<div class="modal-footer">
+ <button type="button" data-ng-class="btns.cancel.cssClass" ng-click="cancel()">{{btns.cancel.label}}</button>
+ <button type="button" data-ng-class="btns.ok.cssClass" ng-click="ok()">{{btns.ok.label}}</button>
+</div>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/authz/kc-tabs-resource-server.html b/themes/src/main/resources/theme/base/admin/resources/templates/authz/kc-tabs-resource-server.html
new file mode 100755
index 0000000..4919fe6
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/authz/kc-tabs-resource-server.html
@@ -0,0 +1,13 @@
+<div data-ng-controller="ClientTabCtrl">
+
+ <kc-tabs-client></kc-tabs-client>
+
+ <ul class="nav nav-tabs nav-tabs-pf" data-ng-hide="create && !path[4]" style="margin-left: 15px">
+ <li ng-class="{active: !path[6]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/">Settings</a></li>
+ <li ng-class="{active: path[6] == 'resource'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/resource">Resources</a></li>
+ <li ng-class="{active: path[6] == 'scope'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope">Scopes</a></li>
+ <li ng-class="{active: path[6] == 'policy'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">Policies</a></li>
+ <li ng-class="{active: path[6] == 'permission'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission">Permissions</a></li>
+ <li ng-class="{active: path[6] == 'evaluate'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/evaluate">Evaluate</a></li>
+ </ul>
+</div>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index 58388ff..63132af 100755
--- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -6,7 +6,7 @@
<i id="removeClient" class="pficon pficon-delete clickable" data-ng-show="access.manageClients" data-ng-click="removeClient()"></i>
</h1>
- <ul class="nav nav-tabs nav-tabs-pf" data-ng-hide="create && !path[4]">
+ <ul class="nav nav-tabs" data-ng-hide="create && !path[4]">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{:: 'settings' | translate}}</a></li>
<li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a></li>
<li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">{{:: 'saml-keys' | translate}}</a></li>
@@ -19,6 +19,7 @@
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/scope-mappings">{{:: 'scope' | translate}}</a>
<kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
</li>
+ <li ng-class="{active: path[4] == 'authz'}" data-ng-show="client.authorizationServicesEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">Authorization</a></li>
<li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a></li>
<!-- <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
<li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!client.bearerOnly">
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
index 837407a..96d4780 100755
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
@@ -36,6 +36,9 @@
<local-cache name="realmVersions">
<transaction mode="BATCH" locking="PESSIMISTIC"/>
</local-cache>
+ <local-cache name="authorization">
+ <eviction max-entries="100" strategy="LRU"/>
+ </local-cache>
</cache-container>
<cache-container name="server" default-cache="default" module="org.wildfly.clustering.server">
<local-cache name="default">