You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2019/05/30 10:05:34 UTC

[syncope] 02/02: [SYNCOPE-1455] SRA now working with routes defined in Core

This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git

commit 23cc100d28e4dfe2b54304681a053fdcd1bffe26
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Thu May 30 12:04:01 2019 +0200

    [SYNCOPE-1455] SRA now working with routes defined in Core
---
 archetype/pom.xml                                  |  14 +-
 .../META-INF/maven/archetype-metadata.xml          |  20 +
 .../resources/archetype-resources/enduser/pom.xml  | 177 --------
 .../main/resources/archetype-resources/fit/pom.xml | 275 ++++++++++++
 .../main/resources/archetype-resources/sra/pom.xml |  85 ++++
 .../sra/src/test}/resources/keymaster.properties   |   0
 archetype/src/main/resources/meta-pom.xml          |   8 +
 .../client/lib/SyncopeClientFactoryBean.java       |   6 +-
 .../syncope/common/lib/to/GatewayRouteTO.java      |  26 +-
 .../syncope/common/lib/types/AMEntitlement.java    |   4 -
 .../syncope/common/lib/types/FilterFactory.java    |   7 +-
 ...{GatewayFilter.java => GatewayRouteFilter.java} |  20 +-
 ...ayPredicate.java => GatewayRoutePredicate.java} |  37 +-
 .../syncope/common/lib/types/PredicateCond.java    |   3 +-
 .../syncope/common/lib/types/PredicateFactory.java |   1 -
 .../rest/api/service/GatewayRouteService.java      |  12 -
 .../zookeper/ZookeeperKeymasterClientContext.java  |  38 +-
 .../syncope/core/logic/GatewayRouteLogic.java      |  41 +-
 .../rest/cxf/service/GatewayRouteServiceImpl.java  |   7 +-
 .../logic/init/IdMImplementationTypeLoader.java    |   3 +-
 .../init/ClassPathScanImplementationLookup.java    |   3 +-
 .../logic/init/IdRepoImplementationTypeLoader.java |   3 +-
 .../core/rest/cxf/RestServiceExceptionMapper.java  |   7 +-
 .../core/persistence/api/entity/GatewayRoute.java  |  16 +-
 .../core/persistence/jpa/RuntimeDomainLoader.java  |   4 +-
 .../core/persistence/jpa/StartupDomainLoader.java  |   2 +-
 .../persistence/jpa/entity/JPAGatewayRoute.java    |  29 +-
 .../persistence/jpa/inner/GatewayRouteTest.java    |   8 +-
 .../java/data/GatewayRouteDataBinderImpl.java      |  19 +-
 .../syncope/core/starter/SyncopeCoreStartup.java   |   2 +-
 docker/sra/pom.xml                                 |  36 +-
 docker/sra/src/main/resources/Dockerfile           |   4 +-
 .../sra}/src/main/resources/application.properties |   6 +-
 .../sra/src/main/resources/keymaster.properties    |   9 +-
 docker/sra/src/main/resources/log4j2.xml           |  16 +-
 .../sra}/src/main/resources/sra.properties         |   2 -
 docker/sra/src/main/resources/startup.sh           |   3 +-
 ...ompose-zookeeper.yml => docker-compose-all.yml} |  19 +-
 fit/console-reference/pom.xml                      |   3 +-
 fit/core-reference/pom.xml                         |   2 +-
 .../syncope/fit/core/GatewayRouteITCase.java       |  52 ++-
 fit/enduser-reference/pom.xml                      |   3 +-
 pom.xml                                            |  41 +-
 sra/pom.xml                                        | 188 ++++++--
 ...tartup.java => CustomGatewayFilterFactory.java} |  32 +-
 .../syncope/sra/CustomRoutePredicateFactory.java   |  56 +++
 .../apache/syncope/sra/ManagementController.java   |  93 ++++
 .../java/org/apache/syncope/sra/RouteProvider.java | 472 +++++++++++++++++++++
 .../org/apache/syncope/sra/RouteRefresher.java     |  21 +-
 .../apache/syncope/sra/SyncopeSRAApplication.java  |  49 ++-
 .../org/apache/syncope/sra/SyncopeSRAShutdown.java |   2 +
 .../org/apache/syncope/sra/SyncopeSRAStartup.java  |   2 +
 sra/src/main/resources/application.properties      |   4 -
 sra/src/main/resources/log4j2.xml                  |   9 +-
 sra/src/main/resources/sra.properties              |   2 -
 .../BodyPropertyAddingGatewayFilterFactory.java    | 201 +++++++++
 .../BodyPropertyMatchingRoutePredicateFactory.java |  81 ++++
 .../org/apache/syncope/sra/SyncopeSRATest.java     | 207 +++++++++
 .../syncope/sra/SyncopeSRATestController.java}     |  15 +-
 .../syncope/sra/SyncopeSRATestCoreStartup.java     | 116 +++++
 .../sra/SyncopeSRATestKeymasterStartup.java        |  92 ++++
 .../{main => test}/resources/keymaster.properties  |   2 +-
 62 files changed, 2330 insertions(+), 387 deletions(-)

diff --git a/archetype/pom.xml b/archetype/pom.xml
index 2fcee7d..c51466b 100644
--- a/archetype/pom.xml
+++ b/archetype/pom.xml
@@ -42,7 +42,7 @@ under the License.
       <extension>
         <groupId>org.apache.maven.archetype</groupId>
         <artifactId>archetype-packaging</artifactId>
-        <version>3.0.1</version>
+        <version>3.1.0</version>
       </extension>
     </extensions>
 
@@ -334,6 +334,18 @@ under the License.
         <directory>../fit/enduser-reference/src/main/webapp/WEB-INF</directory>
         <targetPath>${project.build.outputDirectory}/archetype-resources/enduser/src/main/webapp/WEB-INF</targetPath>
       </resource>
+      
+      <resource>
+        <directory>../sra/src/main/resources</directory>
+        <targetPath>${project.build.outputDirectory}/archetype-resources/sra/src/main/resources</targetPath>
+      </resource>
+      <resource>
+        <directory>../sra/src/test/resources</directory>
+        <targetPath>${project.build.outputDirectory}/archetype-resources/sra/src/main/resources</targetPath>
+        <includes>
+          <include>keymaster.properties</include>          
+        </includes>
+      </resource>
     </resources>
   </build>
   
diff --git a/archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index 391cd25..f22b5fb 100644
--- a/archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -79,5 +79,25 @@ under the License.
         </fileSet>
       </fileSets>
     </module>
+    <module id="syncope-sra" dir="sra" name="syncope-sra">
+      <fileSets>
+        <fileSet filtered="false" encoding="UTF-8">
+          <directory>src/main/resources</directory>
+        </fileSet>
+        <fileSet filtered="false" encoding="UTF-8">
+          <directory>src/test/resources</directory>
+        </fileSet>
+      </fileSets>
+    </module>
+    <module id="syncope-fit" dir="fit" name="syncope-fit">
+      <fileSets>
+        <fileSet filtered="false" encoding="UTF-8">
+          <directory>src/main/resources</directory>
+        </fileSet>
+        <fileSet filtered="false" encoding="UTF-8">
+          <directory>src/test/resources</directory>
+        </fileSet>
+      </fileSets>
+    </module>
   </modules>
 </archetype-descriptor>
diff --git a/archetype/src/main/resources/archetype-resources/enduser/pom.xml b/archetype/src/main/resources/archetype-resources/enduser/pom.xml
index 1004ea7..2902c73 100644
--- a/archetype/src/main/resources/archetype-resources/enduser/pom.xml
+++ b/archetype/src/main/resources/archetype-resources/enduser/pom.xml
@@ -94,183 +94,6 @@ under the License.
 
   <profiles>
     <profile>
-      <id>embedded</id>
-      
-      <properties>
-        <conf.directory>${project.build.directory}/test-classes</conf.directory>
-      </properties>
-      
-      <dependencies>
-        <dependency>
-          <groupId>org.apache.syncope.fit</groupId>
-          <artifactId>syncope-fit-build-tools</artifactId>
-          <version>${syncope.version}</version>
-          <type>war</type>
-          <scope>test</scope>
-        </dependency>
-        <dependency>
-          <groupId>com.h2database</groupId>
-          <artifactId>h2</artifactId>
-          <scope>test</scope>
-        </dependency>
-      </dependencies>
-      
-      <build>
-        <defaultGoal>clean verify cargo:run</defaultGoal>  
-        
-        <plugins>
-          <plugin>
-            <groupId>org.apache.maven.plugins</groupId>
-            <artifactId>maven-antrun-plugin</artifactId>
-            <inherited>true</inherited>
-            <executions>
-              <execution>
-                <id>addFlowableTestContent</id>
-                <phase>prepare-package</phase>
-                <configuration>
-                  <target>
-                    <taskdef resource="net/sf/antcontrib/antcontrib.properties"
-                             classpathref="maven.plugin.classpath"/>
-
-                    <if>
-                      <not>
-                        <available file="../core/target/test-classes/backup/MasterContent.xml"/>
-                      </not>
-                      <then>
-                        <mkdir dir="../core/target/test-classes/backup"/>
-                        <copy file="../core/target/test-classes/domains/MasterContent.xml" 
-                              todir="../core/target/test-classes/backup"/>
-                      </then>
-                    </if>
-                    <if>
-                      <available file="../core/target/syncope/WEB-INF/lib/flowable-engine-${flowable.version}.jar"/>
-                      <then>
-                        <xslt basedir="../core/target/test-classes/backup" 
-                              includes="MasterContent.xml"
-                              destdir="../core/target/test-classes/domains"
-                              extension=".xml"
-                              force="true"
-                              style="../core/src/test/resources/addFlowableToContent.xsl"/>
-                      </then>
-                    </if>
-                  </target>
-                </configuration>
-                <goals>
-                  <goal>run</goal>
-                </goals>
-              </execution>
-              <execution>
-                <id>setupEmbeddedConf</id>
-                <phase>package</phase>
-                <configuration>
-                  <target>
-                    <delete dir="../core/target/syncope/WEB-INF/classes/domains"/>
-                    <copy todir="../core/target/syncope/WEB-INF/classes/domains">
-                      <fileset dir="../core/target/test-classes/domains"/>
-                    </copy>
-                    <copy file="../core/target/test-classes/connid.properties" 
-                          todir="../core/target/syncope/WEB-INF/classes" 
-                          overwrite="true"/>
-                    <copy file="../core/target/test-classes/keymaster.properties" 
-                          todir="../core/target/syncope/WEB-INF/classes" 
-                          overwrite="true"/>
-
-                    <copy file="../console/target/test-classes/keymaster.properties" 
-                          todir="../console/target/syncope-console/WEB-INF/classes" 
-                          overwrite="true"/>
-
-                    <copy file="${project.build.directory}/test-classes/enduser.properties" 
-                          todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
-                          overwrite="true"/>                    
-                    <copy file="${project.build.directory}/test-classes/keymaster.properties" 
-                          todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
-                          overwrite="true"/>                    
-                    <copy file="${project.build.directory}/test-classes/customFormAttributes.json" 
-                          todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
-                          overwrite="true"/>
-                    <copy file="${project.build.directory}/test-classes/customTemplate.json" 
-                          todir="${project.build.directory}/${project.build.finalName}/WEB-INF/classes" 
-                          overwrite="true"/>
-                  </target>
-                </configuration>
-                <goals>
-                  <goal>run</goal>
-                </goals>
-              </execution>
-            </executions>
-            <dependencies>
-              <dependency>
-                <groupId>ant-contrib</groupId>
-                <artifactId>ant-contrib</artifactId>
-                <version>20020829</version>
-              </dependency>
-            </dependencies>
-          </plugin>
-      
-          <plugin>
-            <groupId>org.codehaus.cargo</groupId>
-            <artifactId>cargo-maven2-plugin</artifactId>
-            <inherited>true</inherited>
-            <configuration>
-              <container>
-                <containerId>tomcat9x</containerId>
-                <zipUrlInstaller>
-                  <url>http://central.maven.org/maven2/org/apache/tomcat/tomcat/${tomcat.version}/tomcat-${tomcat.version}.zip</url>
-                  <downloadDir>${settings.localRepository}/org/codehaus/cargo/cargo-container-archives</downloadDir>
-                  <extractDir>${project.build.directory}/cargo/extract</extractDir>
-                </zipUrlInstaller>
-                <dependencies>
-                  <dependency>
-                    <groupId>com.h2database</groupId>
-                    <artifactId>h2</artifactId>
-                  </dependency>
-                </dependencies>
-                <timeout>300000</timeout>
-                <log>${cargo.log}</log>
-                <output>${cargo.output}</output> 
-              </container>
-              <configuration>
-                <properties>
-                  <cargo.jvmargs>
-                    -Dspring.profiles.active=embedded
-                    -XX:+CMSClassUnloadingEnabled -Xmx1024m -Xms512m</cargo.jvmargs>
-                </properties>
-              </configuration>
-              <deployables>
-                <deployable>
-                  <groupId>org.apache.syncope.fit</groupId>
-                  <artifactId>syncope-fit-build-tools</artifactId>
-                  <type>war</type>
-                  <properties>
-                    <context>syncope-fit-build-tools</context>
-                  </properties>
-                </deployable>
-                <deployable>
-                  <location>../core/target/syncope</location>
-                  <properties>
-                    <context>syncope</context>
-                  </properties>
-                </deployable>
-                <deployable>
-                  <location>../console/target/syncope-console</location>
-                  <properties>
-                    <context>syncope-console</context>
-                  </properties>
-                </deployable>
-                <deployable>
-                  <location>${project.build.directory}/${project.build.finalName}</location>
-                  <properties>
-                    <context>syncope-enduser</context>
-                  </properties>
-                </deployable>
-              </deployables>
-            </configuration>
-          </plugin>
-        </plugins>  
-      </build>
-    </profile>
-
-    <profile>
       <id>all</id>
 
       <dependencies>
diff --git a/archetype/src/main/resources/archetype-resources/fit/pom.xml b/archetype/src/main/resources/archetype-resources/fit/pom.xml
new file mode 100644
index 0000000..7c939d2
--- /dev/null
+++ b/archetype/src/main/resources/archetype-resources/fit/pom.xml
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT 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>${groupId}</groupId>
+    <artifactId>${rootArtifactId}</artifactId>
+    <version>${version}</version>
+  </parent>
+
+  <name>Apache Syncope sample project - FIT</name>
+  <groupId>${groupId}</groupId>
+  <artifactId>${artifactId}</artifactId>
+  <packaging>war</packaging>
+
+  <properties>
+    <exec.skip>true</exec.skip>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.syncope.fit</groupId>
+      <artifactId>syncope-fit-build-tools</artifactId>
+      <version>${syncope.version}</version>
+      <type>war</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-install-plugin</artifactId>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-deploy-plugin</artifactId>
+        <configuration>
+          <skip>true</skip>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+        <inherited>false</inherited>
+        <configuration>
+          <failOnMissingWebXml>false</failOnMissingWebXml>
+        </configuration>
+        <executions>
+          <execution>
+            <id>default-war</id>
+            <phase>none</phase>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <inherited>true</inherited>
+        <executions>
+          <execution>
+            <id>addFlowableTestContent</id>
+            <phase>prepare-package</phase>
+            <configuration>
+              <target>
+                <taskdef resource="net/sf/antcontrib/antcontrib.properties"
+                         classpathref="maven.plugin.classpath"/>
+
+                <if>
+                  <not>
+                    <available file="../core/target/test-classes/backup/MasterContent.xml"/>
+                  </not>
+                  <then>
+                    <mkdir dir="../core/target/test-classes/backup"/>
+                    <copy file="../core/target/test-classes/domains/MasterContent.xml" 
+                          todir="../core/target/test-classes/backup"/>
+                  </then>
+                </if>
+                <if>
+                  <available file="../core/target/syncope/WEB-INF/lib/flowable-engine-${flowable.version}.jar"/>
+                  <then>
+                    <xslt basedir="../core/target/test-classes/backup" 
+                          includes="MasterContent.xml"
+                          destdir="../core/target/test-classes/domains"
+                          extension=".xml"
+                          force="true"
+                          style="../core/src/test/resources/addFlowableToContent.xsl"/>
+                  </then>
+                </if>
+              </target>
+            </configuration>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
+          <execution>
+            <id>setupEmbeddedConf</id>
+            <phase>package</phase>
+            <configuration>
+              <target>
+                <delete dir="../core/target/syncope/WEB-INF/classes/domains"/>
+                <copy todir="../core/target/syncope/WEB-INF/classes/domains">
+                  <fileset dir="../core/target/test-classes/domains"/>
+                </copy>
+                <copy file="../core/target/test-classes/connid.properties" 
+                      todir="../core/target/syncope/WEB-INF/classes" 
+                      overwrite="true"/>
+                <copy file="../core/target/test-classes/keymaster.properties" 
+                      todir="../core/target/syncope/WEB-INF/classes" 
+                      overwrite="true"/>
+
+                <copy file="../console/target/test-classes/keymaster.properties" 
+                      todir="../console/target/syncope-console/WEB-INF/classes" 
+                      overwrite="true"/>
+
+                <copy file="../enduser/target/test-classes/keymaster.properties" 
+                      todir="../enduser/target/syncope-enduser/WEB-INF/classes" 
+                      overwrite="true"/>                
+                <copy file="../enduser/target/test-classes/customFormAttributes.json" 
+                      todir="../enduser/target/syncope-enduser/WEB-INF/classes" 
+                      overwrite="true"/>
+                <copy file="../enduser/target/test-classes/customTemplate.json" 
+                      todir="../enduser/target/syncope-enduser/WEB-INF/classes" 
+                      overwrite="true"/>
+              </target>
+            </configuration>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
+        </executions>
+        <dependencies>
+          <dependency>
+            <groupId>ant-contrib</groupId>
+            <artifactId>ant-contrib</artifactId>
+            <version>20020829</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+      
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <inherited>true</inherited>
+        <executions>
+          <execution>
+            <id>default-cli</id>
+            <phase>pre-integration-test</phase>
+            <goals>
+              <goal>exec</goal>
+            </goals>
+            <configuration>
+              <executable>java</executable>
+              <arguments>
+                <argument>-jar</argument>
+                <argument>${basedir}/../sra/target/syncope-sra.jar</argument>
+                <argument>-Dreactor.netty.http.server.accessLogEnabled=true</argument>
+              </arguments>
+              <environmentVariables>
+                <LOADER_PATH>${basedir}/../sra/target/test-classes</LOADER_PATH>
+              </environmentVariables>
+              <async>true</async>
+              <asyncDestroyOnShutdown>true</asyncDestroyOnShutdown>
+            </configuration>                        
+          </execution>
+        </executions>
+      </plugin>
+      
+      <plugin>
+        <groupId>org.codehaus.cargo</groupId>
+        <artifactId>cargo-maven2-plugin</artifactId>
+        <inherited>true</inherited>
+        <configuration>
+          <container>
+            <containerId>tomcat9x</containerId>
+            <zipUrlInstaller>
+              <url>http://central.maven.org/maven2/org/apache/tomcat/tomcat/${tomcat.version}/tomcat-${tomcat.version}.zip</url>
+              <downloadDir>${settings.localRepository}/org/codehaus/cargo/cargo-container-archives</downloadDir>
+              <extractDir>${project.build.directory}/cargo/extract</extractDir>
+            </zipUrlInstaller>
+            <dependencies>
+              <dependency>
+                <groupId>com.h2database</groupId>
+                <artifactId>h2</artifactId>
+              </dependency>
+            </dependencies>
+            <timeout>300000</timeout>
+            <log>${cargo.log}</log>
+            <output>${cargo.output}</output> 
+          </container>
+          <configuration>
+            <properties>
+              <cargo.jvmargs>
+                -Dspring.profiles.active=embedded
+                -XX:+CMSClassUnloadingEnabled -Xmx1024m -Xms512m</cargo.jvmargs>
+            </properties>
+          </configuration>
+          <deployables>
+            <deployable>
+              <groupId>org.apache.syncope.fit</groupId>
+              <artifactId>syncope-fit-build-tools</artifactId>
+              <type>war</type>
+              <properties>
+                <context>syncope-fit-build-tools</context>
+              </properties>
+            </deployable>
+            <deployable>
+              <location>../core/target/syncope</location>
+              <properties>
+                <context>syncope</context>
+              </properties>
+            </deployable>
+            <deployable>
+              <location>../console/target/syncope-console</location>
+              <properties>
+                <context>syncope-console</context>
+              </properties>
+            </deployable>
+            <deployable>
+              <location>../enduser/target/syncope-enduser</location>
+              <properties>
+                <context>syncope-enduser</context>
+              </properties>
+            </deployable>
+          </deployables>
+        </configuration>
+      </plugin>
+    </plugins> 
+  </build>
+  
+  <profiles>
+    <profile>
+      <id>embedded</id>
+
+      <properties>
+        <exec.skip>false</exec.skip>
+      </properties>
+
+      <build>
+        <defaultGoal>clean verify cargo:run</defaultGoal>  
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/archetype/src/main/resources/archetype-resources/sra/pom.xml b/archetype/src/main/resources/archetype-resources/sra/pom.xml
new file mode 100644
index 0000000..48376d9
--- /dev/null
+++ b/archetype/src/main/resources/archetype-resources/sra/pom.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT 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>${groupId}</groupId>
+    <artifactId>${rootArtifactId}</artifactId>
+    <version>${version}</version>
+  </parent>
+
+  <name>Apache Syncope sample project - SRA</name>
+  <groupId>${groupId}</groupId>
+  <artifactId>${artifactId}</artifactId>
+  <packaging>jar</packaging>
+
+  <dependencies>
+    <dependency>
+      <groupId>${groupId}</groupId>
+      <artifactId>syncope-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.apache.syncope</groupId>
+      <artifactId>syncope-sra</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.syncope.ext.self-keymaster</groupId>
+      <artifactId>syncope-ext-self-keymaster-client</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.common.keymaster</groupId>
+      <artifactId>syncope-common-keymaster-client-zookeeper</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <finalName>syncope-sra</finalName>
+
+    <plugins>
+      <plugin>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-maven-plugin</artifactId>
+        <configuration>
+          <mainClass>org.apache.syncope.sra.SyncopeSRAApplication</mainClass>
+          <layout>ZIP</layout>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>repackage</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+
+    <resources>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>true</filtering>
+      </resource>
+    </resources>
+  </build>
+</project>
diff --git a/sra/src/main/resources/keymaster.properties b/archetype/src/main/resources/archetype-resources/sra/src/test/resources/keymaster.properties
similarity index 100%
copy from sra/src/main/resources/keymaster.properties
copy to archetype/src/main/resources/archetype-resources/sra/src/test/resources/keymaster.properties
diff --git a/archetype/src/main/resources/meta-pom.xml b/archetype/src/main/resources/meta-pom.xml
index d16d99a..8eb4a43 100644
--- a/archetype/src/main/resources/meta-pom.xml
+++ b/archetype/src/main/resources/meta-pom.xml
@@ -83,6 +83,12 @@ under the License.
       </dependency>
 
       <dependency>
+        <groupId>org.apache.syncope</groupId>
+        <artifactId>syncope-sra</artifactId>
+        <version>${syncope.version}</version>
+      </dependency>
+
+      <dependency>
         <groupId>org.apache.syncope.ext.self-keymaster</groupId>
         <artifactId>syncope-ext-self-keymaster-rest-cxf</artifactId>
         <version>${syncope.version}</version>
@@ -128,6 +134,8 @@ under the License.
     <module>core</module>
     <module>console</module>
     <module>enduser</module>
+    <module>sra</module>
+    <module>fit</module>
   </modules>
 
 </project>
diff --git a/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClientFactoryBean.java b/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClientFactoryBean.java
index f055f83..c250a19 100644
--- a/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClientFactoryBean.java
+++ b/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClientFactoryBean.java
@@ -277,13 +277,11 @@ public class SyncopeClientFactoryBean {
 
     /**
      * Builds client instance which will be passing the provided value in the
-     * {@link javax.ws.rs.core.HttpHeaders#AUTHORIZATION}
-     * request header.
+     * {@link javax.ws.rs.core.HttpHeaders#AUTHORIZATION} request header.
      *
      * @param jwt value received after login, in the {@link RESTHeaders#TOKEN} response header
      * @return client instance which will be passing the provided value in the
-     * {@link javax.ws.rs.core.HttpHeaders#AUTHORIZATION}
-     * request header
+     * {@link javax.ws.rs.core.HttpHeaders#AUTHORIZATION} request header
      */
     public SyncopeClient create(final String jwt) {
         return create(new JWTAuthenticationHandler(jwt));
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/GatewayRouteTO.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/GatewayRouteTO.java
index f973c70..17ae03a 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/GatewayRouteTO.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/to/GatewayRouteTO.java
@@ -25,12 +25,16 @@ import java.util.List;
 import javax.ws.rs.PathParam;
 import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
-import org.apache.syncope.common.lib.types.GatewayFilter;
-import org.apache.syncope.common.lib.types.GatewayPredicate;
+import org.apache.syncope.common.lib.types.GatewayRouteFilter;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
 import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 
+@XmlRootElement(name = "gatewayRoute")
+@XmlType
 public class GatewayRouteTO implements EntityTO {
 
     private static final long serialVersionUID = 4044528284951757870L;
@@ -39,11 +43,13 @@ public class GatewayRouteTO implements EntityTO {
 
     private String name;
 
+    private int order = 0;
+
     private URI target;
 
-    private final List<GatewayFilter> filters = new ArrayList<>();
+    private final List<GatewayRouteFilter> filters = new ArrayList<>();
 
-    private final List<GatewayPredicate> predicates = new ArrayList<>();
+    private final List<GatewayRoutePredicate> predicates = new ArrayList<>();
 
     private GatewayRouteStatus status;
 
@@ -66,6 +72,14 @@ public class GatewayRouteTO implements EntityTO {
         this.name = name;
     }
 
+    public int getOrder() {
+        return order;
+    }
+
+    public void setOrder(final int order) {
+        this.order = order;
+    }
+
     public URI getTarget() {
         return target;
     }
@@ -77,14 +91,14 @@ public class GatewayRouteTO implements EntityTO {
     @XmlElementWrapper(name = "filters")
     @XmlElement(name = "filter")
     @JsonProperty("filters")
-    public List<GatewayFilter> getFilters() {
+    public List<GatewayRouteFilter> getFilters() {
         return filters;
     }
 
     @XmlElementWrapper(name = "predicates")
     @XmlElement(name = "predicate")
     @JsonProperty("predicates")
-    public List<GatewayPredicate> getPredicates() {
+    public List<GatewayRoutePredicate> getPredicates() {
         return predicates;
     }
 
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
index 6d7d762..06d511c 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/AMEntitlement.java
@@ -26,12 +26,8 @@ import java.util.TreeSet;
 
 public final class AMEntitlement {
 
-    public static final String GATEWAY_ROUTE_LIST = "GATEWAY_ROUTE_LIST";
-
     public static final String GATEWAY_ROUTE_CREATE = "GATEWAY_ROUTE_CREATE";
 
-    public static final String GATEWAY_ROUTE_READ = "GATEWAY_ROUTE_READ";
-
     public static final String GATEWAY_ROUTE_UPDATE = "GATEWAY_ROUTE_UPDATE";
 
     public static final String GATEWAY_ROUTE_DELETE = "GATEWAY_ROUTE_DELETE";
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/FilterFactory.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/FilterFactory.java
index 1120af9..e7fd117 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/FilterFactory.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/FilterFactory.java
@@ -26,8 +26,7 @@ public enum FilterFactory {
     ADD_REQUEST_PARAMETER,
     ADD_RESPONSE_HEADER,
     HYSTRIX,
-    MODIFY_REQUEST_BODY,
-    MODIFY_RESPONSE_BODY,
+    FALLBACK_HEADERS,
     PREFIX_PATH,
     PRESERVE_HOST_HEADER,
     REDIRECT,
@@ -45,9 +44,7 @@ public enum FilterFactory {
     SAVE_SESSION,
     STRIP_PREFIX,
     REQUEST_HEADER_TO_REQUEST_URI,
-    CHANGE_REQUEST_URI,
-    SET_REQIEST_SIZE,
-    FALLBACK_HEADERS,
+    SET_REQUEST_SIZE,
     CUSTOM
 
 }
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayFilter.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRouteFilter.java
similarity index 80%
rename from common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayFilter.java
rename to common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRouteFilter.java
index cbf946e..1b103a5 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayFilter.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRouteFilter.java
@@ -19,16 +19,20 @@
 package org.apache.syncope.common.lib.types;
 
 import java.io.Serializable;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 
-public class GatewayFilter implements Serializable {
+@XmlRootElement(name = "gatewayRouteFilter")
+@XmlType
+public class GatewayRouteFilter implements Serializable {
 
     private static final long serialVersionUID = -635785645207375128L;
 
     public static class Builder {
 
-        private final GatewayFilter instance = new GatewayFilter();
+        private final GatewayRouteFilter instance = new GatewayRouteFilter();
 
         public Builder factory(final FilterFactory factory) {
             instance.setFactory(factory);
@@ -40,7 +44,7 @@ public class GatewayFilter implements Serializable {
             return this;
         }
 
-        public GatewayFilter build() {
+        public GatewayRouteFilter build() {
             return instance;
         }
     }
@@ -84,10 +88,18 @@ public class GatewayFilter implements Serializable {
         if (getClass() != obj.getClass()) {
             return false;
         }
-        final GatewayFilter other = (GatewayFilter) obj;
+        final GatewayRouteFilter other = (GatewayRouteFilter) obj;
         return new EqualsBuilder().
                 append(factory, other.factory).
                 append(args, other.args).
                 build();
     }
+
+    @Override
+    public String toString() {
+        return "GatewayFilter{"
+                + "factory=" + factory
+                + ", args=" + args
+                + '}';
+    }
 }
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayPredicate.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicate.java
similarity index 74%
rename from common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayPredicate.java
rename to common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicate.java
index 27c6635..9f26bbe 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayPredicate.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/GatewayRoutePredicate.java
@@ -19,16 +19,25 @@
 package org.apache.syncope.common.lib.types;
 
 import java.io.Serializable;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 
-public class GatewayPredicate implements Serializable {
+@XmlRootElement(name = "gatewayRoutePredicate")
+@XmlType
+public class GatewayRoutePredicate implements Serializable {
 
     private static final long serialVersionUID = -635785645207375128L;
 
     public static class Builder {
 
-        private final GatewayPredicate instance = new GatewayPredicate();
+        private final GatewayRoutePredicate instance = new GatewayRoutePredicate();
+
+        public Builder negate() {
+            instance.setNegate(true);
+            return this;
+        }
 
         public Builder cond(final PredicateCond cond) {
             instance.setCond(cond);
@@ -45,17 +54,27 @@ public class GatewayPredicate implements Serializable {
             return this;
         }
 
-        public GatewayPredicate build() {
+        public GatewayRoutePredicate build() {
             return instance;
         }
     }
 
+    private boolean negate;
+
     private PredicateCond cond;
 
     private PredicateFactory factory;
 
     private String args;
 
+    public boolean isNegate() {
+        return negate;
+    }
+
+    public void setNegate(final boolean negate) {
+        this.negate = negate;
+    }
+
     public PredicateCond getCond() {
         return cond;
     }
@@ -100,11 +119,21 @@ public class GatewayPredicate implements Serializable {
         if (getClass() != obj.getClass()) {
             return false;
         }
-        final GatewayPredicate other = (GatewayPredicate) obj;
+        final GatewayRoutePredicate other = (GatewayRoutePredicate) obj;
         return new EqualsBuilder().
                 append(cond, other.cond).
                 append(factory, other.factory).
                 append(args, other.args).
                 build();
     }
+
+    @Override
+    public String toString() {
+        return "GatewayPredicate{"
+                + "negate=" + negate
+                + ", cond=" + cond
+                + ", factory=" + factory
+                + ", args=" + args
+                + '}';
+    }
 }
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateCond.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateCond.java
index 93b0bad..12d2704 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateCond.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateCond.java
@@ -23,7 +23,6 @@ import javax.xml.bind.annotation.XmlEnum;
 @XmlEnum
 public enum PredicateCond {
     AND,
-    OR,
-    NOT
+    OR
 
 }
diff --git a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateFactory.java b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateFactory.java
index 5cf4e77..c1e431f 100644
--- a/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateFactory.java
+++ b/common/am/lib/src/main/java/org/apache/syncope/common/lib/types/PredicateFactory.java
@@ -32,7 +32,6 @@ public enum PredicateFactory {
     PATH,
     QUERY,
     REMOTE_ADDR,
-    READ_BODY,
     CUSTOM
 
 }
diff --git a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/GatewayRouteService.java b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/GatewayRouteService.java
index 6b957b8..c44af7e 100644
--- a/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/GatewayRouteService.java
+++ b/common/am/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/GatewayRouteService.java
@@ -129,16 +129,4 @@ public interface GatewayRouteService extends JAXRSService {
     @Path("push")
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
     void pushToSRA();
-
-    /**
-     * Push route with matching key to SRA.
-     *
-     * @param key route key
-     */
-    @ApiResponses(
-            @ApiResponse(responseCode = "204", description = "Operation was successful"))
-    @POST
-    @Path("push/{key}")
-    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
-    void pushToSRA(@NotNull @PathParam("key") String key);
 }
diff --git a/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperKeymasterClientContext.java b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperKeymasterClientContext.java
index 234093f..033f5b7 100644
--- a/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperKeymasterClientContext.java
+++ b/common/keymaster/client-zookeeper/src/main/java/org/apache/syncope/common/keymaster/client/zookeper/ZookeeperKeymasterClientContext.java
@@ -59,7 +59,8 @@ public class ZookeeperKeymasterClientContext {
     @Value("${keymaster.maxRetries:3}")
     private Integer maxRetries;
 
-    @ConditionalOnExpression("#{'${keymaster.address}' matches '^(?!http).+'}")
+    @ConditionalOnExpression("#{'${keymaster.address}' "
+            + "matches '^((\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})|[a-z\\.]+):[0-9]+$'}")
     @Bean
     public CuratorFramework curatorFramework() throws InterruptedException {
         if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
@@ -86,20 +87,18 @@ public class ZookeeperKeymasterClientContext {
                 connectString(address).
                 retryPolicy(new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries));
         if (StringUtils.isNotBlank(username)) {
-            clientBuilder.
-                    authorization("digest", (username).getBytes()).
-                    aclProvider(new ACLProvider() {
-
-                        @Override
-                        public List<ACL> getDefaultAcl() {
-                            return ZooDefs.Ids.CREATOR_ALL_ACL;
-                        }
-
-                        @Override
-                        public List<ACL> getAclForPath(final String path) {
-                            return ZooDefs.Ids.CREATOR_ALL_ACL;
-                        }
-                    });
+            clientBuilder.authorization("digest", username.getBytes()).aclProvider(new ACLProvider() {
+
+                @Override
+                public List<ACL> getDefaultAcl() {
+                    return ZooDefs.Ids.CREATOR_ALL_ACL;
+                }
+
+                @Override
+                public List<ACL> getAclForPath(final String path) {
+                    return ZooDefs.Ids.CREATOR_ALL_ACL;
+                }
+            });
         }
         CuratorFramework client = clientBuilder.build();
         client.start();
@@ -108,20 +107,23 @@ public class ZookeeperKeymasterClientContext {
         return client;
     }
 
-    @ConditionalOnExpression("#{'${keymaster.address}' matches '^(?!http).+'}")
+    @ConditionalOnExpression("#{'${keymaster.address}' "
+            + "matches '^((\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})|[a-z\\.]+):[0-9]+$'}")
     @Bean
     public ConfParamOps selfConfParamOps() {
         return new ZookeeperConfParamOps();
     }
 
-    @ConditionalOnExpression("#{'${keymaster.address}' matches '^(?!http).+'}")
+    @ConditionalOnExpression("#{'${keymaster.address}' "
+            + "matches '^((\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})|[a-z\\.]+):[0-9]+$'}")
     @Bean
     public ServiceOps serviceOps() {
         return new ZookeeperServiceDiscoveryOps();
         //return new ZookeeperServiceOps();
     }
 
-    @ConditionalOnExpression("#{'${keymaster.address}' matches '^(?!http).+'}")
+    @ConditionalOnExpression("#{'${keymaster.address}' "
+            + "matches '^((\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})|[a-z\\.]+):[0-9]+$'}")
     @Bean
     public DomainOps domainOps() {
         return new ZookeeperDomainOps();
diff --git a/core/am/logic/src/main/java/org/apache/syncope/core/logic/GatewayRouteLogic.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/GatewayRouteLogic.java
index 3d57c17..71a343e 100644
--- a/core/am/logic/src/main/java/org/apache/syncope/core/logic/GatewayRouteLogic.java
+++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/GatewayRouteLogic.java
@@ -19,9 +19,20 @@
 package org.apache.syncope.core.logic;
 
 import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
 import java.util.List;
 import java.util.stream.Collectors;
+import javax.annotation.Resource;
+import javax.ws.rs.core.HttpHeaders;
 import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.cxf.transport.http.auth.DefaultBasicAuthSupplier;
+import org.apache.syncope.common.keymaster.client.api.KeymasterException;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
+import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
 import org.apache.syncope.common.lib.to.GatewayRouteTO;
 import org.apache.syncope.common.lib.types.AMEntitlement;
 import org.apache.syncope.core.persistence.api.dao.GatewayRouteDAO;
@@ -45,6 +56,16 @@ public class GatewayRouteLogic extends AbstractTransactionalLogic<GatewayRouteTO
     @Autowired
     private EntityFactory entityFactory;
 
+    @Autowired
+    private ServiceOps serviceOps;
+
+    @Resource(name = "anonymousUser")
+    private String anonymousUser;
+
+    @Resource(name = "anonymousKey")
+    private String anonymousKey;
+
+    @PreAuthorize("isAuthenticated()")
     public List<GatewayRouteTO> list() {
         return routeDAO.findAll().stream().map(binder::getGatewayRouteTO).collect(Collectors.toList());
     }
@@ -57,7 +78,7 @@ public class GatewayRouteLogic extends AbstractTransactionalLogic<GatewayRouteTO
         return binder.getGatewayRouteTO(routeDAO.save(route));
     }
 
-    @PreAuthorize("hasRole('" + AMEntitlement.GATEWAY_ROUTE_READ + "')")
+    @PreAuthorize("isAuthenticated()")
     public GatewayRouteTO read(final String key) {
         GatewayRoute route = routeDAO.find(key);
         if (route == null) {
@@ -92,12 +113,18 @@ public class GatewayRouteLogic extends AbstractTransactionalLogic<GatewayRouteTO
 
     @PreAuthorize("hasRole('" + AMEntitlement.GATEWAY_ROUTE_PUSH + "')")
     public void pushToSRA() {
-        // TODO
-    }
-
-    @PreAuthorize("hasRole('" + AMEntitlement.GATEWAY_ROUTE_PUSH + "')")
-    public void pushToSRA(final String key) {
-        // TODO
+        try {
+            NetworkService sra = serviceOps.get(NetworkService.Type.SRA);
+            HttpClient.newBuilder().build().sendAsync(
+                    HttpRequest.newBuilder(URI.create(
+                            StringUtils.appendIfMissing(sra.getAddress(), "/") + "management/routes/refresh")).
+                            header(HttpHeaders.AUTHORIZATION,
+                                    DefaultBasicAuthSupplier.getBasicAuthHeader(anonymousUser, anonymousKey)).
+                            POST(HttpRequest.BodyPublishers.noBody()).build(),
+                    HttpResponse.BodyHandlers.discarding());
+        } catch (KeymasterException e) {
+            throw new NotFoundException("Could not find any SRA instance", e);
+        }
     }
 
     @Override
diff --git a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/GatewayRouteServiceImpl.java b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/GatewayRouteServiceImpl.java
index f02f0c1..545b630 100644
--- a/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/GatewayRouteServiceImpl.java
+++ b/core/am/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/GatewayRouteServiceImpl.java
@@ -33,7 +33,7 @@ public class GatewayRouteServiceImpl extends AbstractServiceImpl implements Gate
 
     @Autowired
     private GatewayRouteLogic logic;
-    
+
     @Override
     public List<GatewayRouteTO> list() {
         return logic.list();
@@ -67,9 +67,4 @@ public class GatewayRouteServiceImpl extends AbstractServiceImpl implements Gate
     public void pushToSRA() {
         logic.pushToSRA();
     }
-
-    @Override
-    public void pushToSRA(final String key) {
-        logic.pushToSRA(key);
-    }
 }
diff --git a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/init/IdMImplementationTypeLoader.java b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/init/IdMImplementationTypeLoader.java
index b3a039c..1a64ee1 100644
--- a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/init/IdMImplementationTypeLoader.java
+++ b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/init/IdMImplementationTypeLoader.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.logic.init;
 import org.apache.syncope.common.lib.types.IdMImplementationType;
 import org.apache.syncope.common.lib.types.ImplementationTypesHolder;
 import org.apache.syncope.core.persistence.api.SyncopeCoreLoader;
+import org.springframework.core.Ordered;
 import org.springframework.stereotype.Component;
 
 @Component
@@ -28,7 +29,7 @@ public class IdMImplementationTypeLoader implements SyncopeCoreLoader {
 
     @Override
     public int getOrder() {
-        return Integer.MIN_VALUE;
+        return Ordered.HIGHEST_PRECEDENCE;
     }
 
     @Override
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/ClassPathScanImplementationLookup.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/ClassPathScanImplementationLookup.java
index f1c21a8..c594ecb 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/ClassPathScanImplementationLookup.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/ClassPathScanImplementationLookup.java
@@ -61,6 +61,7 @@ import org.apache.syncope.core.spring.security.JWTSSOProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
+import org.springframework.core.Ordered;
 import org.springframework.core.type.filter.AssignableTypeFilter;
 import org.springframework.util.ClassUtils;
 
@@ -91,7 +92,7 @@ public class ClassPathScanImplementationLookup implements ImplementationLookup {
 
     @Override
     public int getOrder() {
-        return 250;
+        return Ordered.HIGHEST_PRECEDENCE;
     }
 
     /**
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/IdRepoImplementationTypeLoader.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/IdRepoImplementationTypeLoader.java
index 3735d1c..0eded04 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/IdRepoImplementationTypeLoader.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/init/IdRepoImplementationTypeLoader.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.logic.init;
 import org.apache.syncope.common.lib.types.IdRepoImplementationType;
 import org.apache.syncope.common.lib.types.ImplementationTypesHolder;
 import org.apache.syncope.core.persistence.api.SyncopeCoreLoader;
+import org.springframework.core.Ordered;
 import org.springframework.stereotype.Component;
 
 @Component
@@ -28,7 +29,7 @@ public class IdRepoImplementationTypeLoader implements SyncopeCoreLoader {
 
     @Override
     public int getOrder() {
-        return Integer.MIN_VALUE;
+        return Ordered.HIGHEST_PRECEDENCE;
     }
 
     @Override
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/RestServiceExceptionMapper.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/RestServiceExceptionMapper.java
index d3b1b77..f5e6be1 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/RestServiceExceptionMapper.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/RestServiceExceptionMapper.java
@@ -103,10 +103,13 @@ public class RestServiceExceptionMapper implements ExceptionMapper<Exception> {
 
             builder = builder(ClientExceptionType.DelegatedAdministration, ExceptionUtils.getRootCauseMessage(ex));
         } else if (ex instanceof EntityExistsException || ex instanceof DuplicateException
-                || ex instanceof PersistenceException && ex.getCause() instanceof EntityExistsException) {
+                || ((ex instanceof PersistenceException || ex instanceof DataIntegrityViolationException)
+                && ex.getCause() instanceof EntityExistsException)) {
 
             builder = builder(ClientExceptionType.EntityExists,
-                    getPersistenceErrorMessage(ex instanceof PersistenceException ? ex.getCause() : ex));
+                    getPersistenceErrorMessage(
+                            ex instanceof PersistenceException || ex instanceof DataIntegrityViolationException
+                                    ? ex.getCause() : ex));
         } else if (ex instanceof DataIntegrityViolationException || ex instanceof UncategorizedDataAccessException) {
             builder = builder(ClientExceptionType.DataIntegrityViolation, getPersistenceErrorMessage(ex));
         } else if (ex instanceof ConnectorException) {
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/GatewayRoute.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/GatewayRoute.java
index 70f1e6e..4aef3f7 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/GatewayRoute.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/GatewayRoute.java
@@ -20,8 +20,8 @@ package org.apache.syncope.core.persistence.api.entity;
 
 import java.net.URI;
 import java.util.List;
-import org.apache.syncope.common.lib.types.GatewayFilter;
-import org.apache.syncope.common.lib.types.GatewayPredicate;
+import org.apache.syncope.common.lib.types.GatewayRouteFilter;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
 import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 
 public interface GatewayRoute extends Entity {
@@ -30,17 +30,21 @@ public interface GatewayRoute extends Entity {
 
     void setName(String name);
 
+    int getOrder();
+
+    void setOrder(int order);
+
     URI getTarget();
 
     void setTarget(URI target);
 
-    List<GatewayFilter> getFilters();
+    List<GatewayRouteFilter> getFilters();
 
-    void setFilters(List<GatewayFilter> filters);
+    void setFilters(List<GatewayRouteFilter> filters);
 
-    List<GatewayPredicate> getPredicates();
+    List<GatewayRoutePredicate> getPredicates();
 
-    void setPredicates(List<GatewayPredicate> predicates);
+    void setPredicates(List<GatewayRoutePredicate> predicates);
 
     GatewayRouteStatus getStatus();
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/RuntimeDomainLoader.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/RuntimeDomainLoader.java
index ffc258d..36ba211 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/RuntimeDomainLoader.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/RuntimeDomainLoader.java
@@ -53,7 +53,7 @@ public class RuntimeDomainLoader implements DomainWatcher {
 
             ApplicationContextProvider.getApplicationContext().getBeansOfType(SyncopeCoreLoader.class).values().
                     stream().sorted(Comparator.comparing(SyncopeCoreLoader::getOrder)).
-                    forEach(loader -> {
+                    forEachOrdered(loader -> {
                         String loaderName = AopUtils.getTargetClass(loader).getName();
 
                         LOG.debug("[{}] Starting on domain '{}'", loaderName, domain);
@@ -72,7 +72,7 @@ public class RuntimeDomainLoader implements DomainWatcher {
 
             ApplicationContextProvider.getApplicationContext().getBeansOfType(SyncopeCoreLoader.class).values().
                     stream().sorted(Comparator.comparing(SyncopeCoreLoader::getOrder).reversed()).
-                    forEach(loader -> {
+                    forEachOrdered(loader -> {
                         String loaderName = AopUtils.getTargetClass(loader).getName();
 
                         LOG.debug("[{}] Starting on domain '{}'", loaderName, domain);
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/StartupDomainLoader.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/StartupDomainLoader.java
index 6364dd2..545c14c 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/StartupDomainLoader.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/StartupDomainLoader.java
@@ -69,7 +69,7 @@ public class StartupDomainLoader implements SyncopeCoreLoader {
 
     @Override
     public int getOrder() {
-        return Ordered.HIGHEST_PRECEDENCE;
+        return Ordered.LOWEST_PRECEDENCE;
     }
 
     @Override
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAGatewayRoute.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAGatewayRoute.java
index 8aee362..a6c4e1e 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAGatewayRoute.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPAGatewayRoute.java
@@ -29,8 +29,8 @@ import javax.persistence.Enumerated;
 import javax.persistence.Lob;
 import javax.persistence.Table;
 import javax.validation.constraints.NotNull;
-import org.apache.syncope.common.lib.types.GatewayFilter;
-import org.apache.syncope.common.lib.types.GatewayPredicate;
+import org.apache.syncope.common.lib.types.GatewayRouteFilter;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
 import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.syncope.core.persistence.api.entity.GatewayRoute;
 import org.apache.syncope.core.persistence.jpa.validation.entity.GatewayRouteCheck;
@@ -48,6 +48,9 @@ public class JPAGatewayRoute extends AbstractGeneratedKeyEntity implements Gatew
     @Column(unique = true, nullable = false)
     private String name;
 
+    private Integer routeOrder;
+
+    @NotNull
     private String target;
 
     @Lob
@@ -71,6 +74,16 @@ public class JPAGatewayRoute extends AbstractGeneratedKeyEntity implements Gatew
     }
 
     @Override
+    public int getOrder() {
+        return routeOrder == null ? 0 : routeOrder;
+    }
+
+    @Override
+    public void setOrder(final int order) {
+        this.routeOrder = order;
+    }
+
+    @Override
     public URI getTarget() {
         return URI.create(target);
     }
@@ -81,26 +94,26 @@ public class JPAGatewayRoute extends AbstractGeneratedKeyEntity implements Gatew
     }
 
     @Override
-    public List<GatewayFilter> getFilters() {
+    public List<GatewayRouteFilter> getFilters() {
         return filters == null
                 ? Collections.emptyList()
-                : Arrays.asList(POJOHelper.deserialize(filters, GatewayFilter[].class));
+                : Arrays.asList(POJOHelper.deserialize(filters, GatewayRouteFilter[].class));
     }
 
     @Override
-    public void setFilters(final List<GatewayFilter> filters) {
+    public void setFilters(final List<GatewayRouteFilter> filters) {
         this.filters = POJOHelper.serialize(filters);
     }
 
     @Override
-    public List<GatewayPredicate> getPredicates() {
+    public List<GatewayRoutePredicate> getPredicates() {
         return predicates == null
                 ? Collections.emptyList()
-                : Arrays.asList(POJOHelper.deserialize(predicates, GatewayPredicate[].class));
+                : Arrays.asList(POJOHelper.deserialize(predicates, GatewayRoutePredicate[].class));
     }
 
     @Override
-    public void setPredicates(final List<GatewayPredicate> predicates) {
+    public void setPredicates(final List<GatewayRoutePredicate> predicates) {
         this.predicates = POJOHelper.serialize(predicates);
     }
 
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/GatewayRouteTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/GatewayRouteTest.java
index 2bcc959..7fdab60 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/GatewayRouteTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/GatewayRouteTest.java
@@ -28,8 +28,8 @@ import java.util.List;
 import java.util.UUID;
 import javax.ws.rs.HttpMethod;
 import org.apache.syncope.common.lib.types.FilterFactory;
-import org.apache.syncope.common.lib.types.GatewayFilter;
-import org.apache.syncope.common.lib.types.GatewayPredicate;
+import org.apache.syncope.common.lib.types.GatewayRouteFilter;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
 import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.syncope.common.lib.types.PredicateFactory;
 import org.apache.syncope.core.persistence.api.dao.GatewayRouteDAO;
@@ -68,9 +68,9 @@ public class GatewayRouteTest extends AbstractTest {
         GatewayRoute route = entityFactory.newEntity(GatewayRoute.class);
         route.setName("just for test");
         route.setTarget(URI.create("http://httpbin.org:80"));
-        route.setPredicates(Arrays.asList(new GatewayPredicate.Builder().
+        route.setPredicates(Arrays.asList(new GatewayRoutePredicate.Builder().
                 factory(PredicateFactory.METHOD).args(HttpMethod.GET).build()));
-        route.setFilters(Arrays.asList(new GatewayFilter.Builder().
+        route.setFilters(Arrays.asList(new GatewayRouteFilter.Builder().
                 factory(FilterFactory.ADD_REQUEST_HEADER).args("X-Request-Foo, Bar").build()));
         route.setStatus(GatewayRouteStatus.DRAFT);
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GatewayRouteDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GatewayRouteDataBinderImpl.java
index ac7e563..0259510 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GatewayRouteDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GatewayRouteDataBinderImpl.java
@@ -18,7 +18,11 @@
  */
 package org.apache.syncope.core.provisioning.java.data;
 
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.GatewayRouteTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.syncope.core.persistence.api.entity.GatewayRoute;
 import org.apache.syncope.core.provisioning.api.data.GatewayRouteDataBinder;
 import org.springframework.stereotype.Component;
@@ -28,11 +32,23 @@ public class GatewayRouteDataBinderImpl implements GatewayRouteDataBinder {
 
     @Override
     public void getGatewayRoute(final GatewayRoute route, final GatewayRouteTO routeTO) {
+        SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.RequiredValuesMissing);
+        if (StringUtils.isBlank(routeTO.getName())) {
+            sce.getElements().add("name");
+        }
+        if (routeTO.getTarget() == null) {
+            sce.getElements().add("target");
+        }
+        if (!sce.isEmpty()) {
+            throw sce;
+        }
+
         route.setName(routeTO.getName());
+        route.setOrder(routeTO.getOrder());
         route.setTarget(routeTO.getTarget());
         route.setFilters(routeTO.getFilters());
         route.setPredicates(routeTO.getPredicates());
-        route.setStatus(routeTO.getStatus());
+        route.setStatus(routeTO.getStatus() == null ? GatewayRouteStatus.DRAFT : routeTO.getStatus());
     }
 
     @Override
@@ -40,6 +56,7 @@ public class GatewayRouteDataBinderImpl implements GatewayRouteDataBinder {
         GatewayRouteTO routeTO = new GatewayRouteTO();
         routeTO.setKey(route.getKey());
         routeTO.setName(route.getName());
+        routeTO.setOrder(route.getOrder());
         routeTO.setTarget(route.getTarget());
         routeTO.getFilters().addAll(route.getFilters());
         routeTO.getPredicates().addAll(route.getPredicates());
diff --git a/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreStartup.java b/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreStartup.java
index a1d130a..f93de92 100644
--- a/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreStartup.java
+++ b/core/starter/src/main/java/org/apache/syncope/core/starter/SyncopeCoreStartup.java
@@ -51,7 +51,7 @@ public class SyncopeCoreStartup extends SyncopeCoreStartStop
     public void onApplicationEvent(final ContextRefreshedEvent event) {
         event.getApplicationContext().getBeansOfType(SyncopeCoreLoader.class).values().stream().
                 sorted(Comparator.comparing(SyncopeCoreLoader::getOrder)).
-                forEach(loader -> {
+                forEachOrdered(loader -> {
                     String loaderName = AopUtils.getTargetClass(loader).getName();
 
                     LOG.debug("[{}] Starting initialization", loaderName);
diff --git a/docker/sra/pom.xml b/docker/sra/pom.xml
index 2866259..6a80959 100644
--- a/docker/sra/pom.xml
+++ b/docker/sra/pom.xml
@@ -42,26 +42,36 @@ under the License.
       <groupId>org.apache.syncope</groupId>
       <artifactId>syncope-sra</artifactId>
       <version>${project.version}</version>
-      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.syncope.ext.self-keymaster</groupId>
+      <artifactId>syncope-ext-self-keymaster-client</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.common.keymaster</groupId>
+      <artifactId>syncope-common-keymaster-client-zookeeper</artifactId>
+      <version>${project.version}</version>
     </dependency>
   </dependencies>
 
   <build>
     <plugins>
       <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-antrun-plugin</artifactId>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-maven-plugin</artifactId>
+        <configuration>
+          <mainClass>org.apache.syncope.sra.SyncopeSRAApplication</mainClass>
+          <layout>ZIP</layout>
+        </configuration>
         <executions>
           <execution>
             <goals>
-              <goal>run</goal>
+              <goal>repackage</goal>
             </goals>
-            <phase>package</phase>
             <configuration>
-              <target>
-                <copy file="${settings.localRepository}/org/apache/syncope/syncope-sra/${syncope.version}/syncope-sra-${syncope.version}.jar"
-                      todir="${project.build.outputDirectory}"/>
-              </target>
+              <outputDirectory>${project.build.outputDirectory}</outputDirectory>
             </configuration>
           </execution>
         </executions>
@@ -105,14 +115,6 @@ under the License.
         <directory>src/main/resources</directory>
         <filtering>true</filtering>
       </resource>
-
-      <resource>
-        <directory>${project.basedir}/../../sra/src/main/resources</directory>
-        <includes>
-          <include>application.properties</include>
-        </includes>
-        <filtering>true</filtering>
-      </resource>
     </resources>
   </build>
 
diff --git a/docker/sra/src/main/resources/Dockerfile b/docker/sra/src/main/resources/Dockerfile
index 5651fc0..c7398f3 100644
--- a/docker/sra/src/main/resources/Dockerfile
+++ b/docker/sra/src/main/resources/Dockerfile
@@ -26,10 +26,10 @@ RUN mkdir /opt/syncope/conf
 RUN mkdir /opt/syncope/lib
 RUN mkdir /opt/syncope/log
 
-COPY application.properties /opt/syncope/conf/
+COPY *.properties /opt/syncope/conf/
 COPY log4j2.xml /opt/syncope/conf/
 
-COPY syncope-sra-*jar /opt/syncope/lib/syncope-sra.jar
+COPY syncope-docker-sra-*jar /opt/syncope/lib/syncope-sra.jar
 
 COPY startup.sh /opt/syncope/bin
 RUN chmod 755 /opt/syncope/bin/startup.sh
diff --git a/sra/src/main/resources/application.properties b/docker/sra/src/main/resources/application.properties
similarity index 85%
copy from sra/src/main/resources/application.properties
copy to docker/sra/src/main/resources/application.properties
index 98bc964..b6b5147 100644
--- a/sra/src/main/resources/application.properties
+++ b/docker/sra/src/main/resources/application.properties
@@ -20,13 +20,9 @@ spring.main.banner-mode=log
 
 server.port=8080
 
-management.endpoint.gateway.enabled=true
-management.endpoints.web.exposure.include=gateway
-
 spring.cloud.gateway.metrics.enabled=true
 management.endpoint.metrics.enabled=true
-management.endpoints.web.exposure.include=*
 management.endpoint.prometheus.enabled=true
 management.metrics.export.prometheus.enabled=true
 
-service.discovery.address=http://localhost:8080/
+service.discovery.address=${SERVICE_DISCOVERY_ADDRESS}
diff --git a/sra/src/main/resources/sra.properties b/docker/sra/src/main/resources/keymaster.properties
similarity index 86%
copy from sra/src/main/resources/sra.properties
copy to docker/sra/src/main/resources/keymaster.properties
index 6789cfd..14e8ca6 100644
--- a/sra/src/main/resources/sra.properties
+++ b/docker/sra/src/main/resources/keymaster.properties
@@ -14,9 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-sra.directory=${conf.directory}
-
-anonymousUser=${anonymousUser}
-anonymousKey=${anonymousKey}
-
-useGZIPCompression=true
+keymaster.address=${KEYMASTER_ADDRESS}
+keymaster.username=${KEYMASTER_USERNAME}
+keymaster.password=${KEYMASTER_PASSWORD}
diff --git a/docker/sra/src/main/resources/log4j2.xml b/docker/sra/src/main/resources/log4j2.xml
index 033f1ed..6fdccd1 100644
--- a/docker/sra/src/main/resources/log4j2.xml
+++ b/docker/sra/src/main/resources/log4j2.xml
@@ -32,11 +32,23 @@ under the License.
     <asyncLogger name="org.apache.syncope.client.lib" additivity="false" level="OFF">
       <appender-ref ref="console"/>
     </asyncLogger>
-
     <asyncLogger name="org.apache.syncope.sra" additivity="false" level="INFO">
       <appender-ref ref="console"/>
     </asyncLogger>
-    
+
+    <asyncLogger name="org.apache.cxf" additivity="false" level="ERROR">
+      <appender-ref ref="console"/>
+    </asyncLogger>
+
+    <asyncLogger name="org.springframework.cloud.gateway" additivity="false" level="INFO">
+      <appender-ref ref="console"/>
+    </asyncLogger>
+
+    <!-- Requires -Dreactor.netty.http.server.accessLogEnabled=true to work-->
+    <asyncLogger name="reactor.netty.http.server.AccessLog" additivity="false" level="INFO">
+      <appender-ref ref="console"/>
+    </asyncLogger>
+
     <root level="INFO">
       <appender-ref ref="console"/>
     </root>
diff --git a/sra/src/main/resources/sra.properties b/docker/sra/src/main/resources/sra.properties
similarity index 96%
copy from sra/src/main/resources/sra.properties
copy to docker/sra/src/main/resources/sra.properties
index 6789cfd..77cb93e 100644
--- a/sra/src/main/resources/sra.properties
+++ b/docker/sra/src/main/resources/sra.properties
@@ -14,8 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-sra.directory=${conf.directory}
-
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
diff --git a/docker/sra/src/main/resources/startup.sh b/docker/sra/src/main/resources/startup.sh
index 69093c3..df42979 100755
--- a/docker/sra/src/main/resources/startup.sh
+++ b/docker/sra/src/main/resources/startup.sh
@@ -19,4 +19,5 @@
 
 export LOADER_PATH="/opt/syncope/conf,/opt/syncope/lib"
 java -Dfile.encoding=UTF-8 -server -Xms1536m -Xmx1536m -XX:NewSize=256m -XX:MaxNewSize=256m \
- -XX:+DisableExplicitGC -Djava.security.egd=file:/dev/./urandom -jar /opt/syncope/lib/syncope-sra.jar
+ -XX:+DisableExplicitGC -Djava.security.egd=file:/dev/./urandom \
+ -Dreactor.netty.http.server.accessLogEnabled=true -jar /opt/syncope/lib/syncope-sra.jar
diff --git a/docker/src/main/resources/docker-compose/docker-compose-zookeeper.yml b/docker/src/main/resources/docker-compose/docker-compose-all.yml
similarity index 82%
rename from docker/src/main/resources/docker-compose/docker-compose-zookeeper.yml
rename to docker/src/main/resources/docker-compose/docker-compose-all.yml
index 2d216f6..1ed6be1 100644
--- a/docker/src/main/resources/docker-compose/docker-compose-zookeeper.yml
+++ b/docker/src/main/resources/docker-compose/docker-compose-all.yml
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# Full deployment (Core, Console, Enduser) on PostgreSQL, with Keymaster on Zookeeper
+# Full deployment (Core, Console, Enduser, SRA) on PostgreSQL, with Keymaster on Zookeeper
 
 # Zookeeper is configured without JAAS, hence empty KEYMASTER_USERNAME / KEYMASTER_PASSWORD
 # are passed to other containers
@@ -38,6 +38,7 @@ services:
    syncope:
      depends_on:
        - db
+       - keymaster
      image: apache/syncope:${SYNCOPE_VERSION}
      ports:
        - "18080:8080"
@@ -58,6 +59,7 @@ services:
    syncope-console:
      depends_on:
        - syncope
+       - keymaster
      image: apache/syncope-console:${SYNCOPE_VERSION}
      ports:
        - "28080:8080"
@@ -71,6 +73,7 @@ services:
    syncope-enduser:
      depends_on:
        - syncope
+       - keymaster
      image: apache/syncope-enduser:${SYNCOPE_VERSION}
      ports:
        - "38080:8080"
@@ -80,3 +83,17 @@ services:
        KEYMASTER_USERNAME: ${KEYMASTER_USERNAME:-}
        KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD:-}
        SERVICE_DISCOVERY_ADDRESS: http://syncope-enduser:8080/syncope-enduser/
+
+   syncope-sra:
+     depends_on:
+       - syncope
+       - keymaster
+     image: apache/syncope-sra:${SYNCOPE_VERSION}
+     ports:
+       - "48080:8080"
+     restart: always
+     environment:
+       KEYMASTER_ADDRESS: keymaster:2181
+       KEYMASTER_USERNAME: ${KEYMASTER_USERNAME:-}
+       KEYMASTER_PASSWORD: ${KEYMASTER_PASSWORD:-}
+       SERVICE_DISCOVERY_ADDRESS: http://syncope-sra:8080/
diff --git a/fit/console-reference/pom.xml b/fit/console-reference/pom.xml
index d5ee498..1971cc9 100644
--- a/fit/console-reference/pom.xml
+++ b/fit/console-reference/pom.xml
@@ -291,8 +291,7 @@ under the License.
                     -Dspring.profiles.active=embedded
                     -javaagent:${java.home}/lib/hotswap/hotswap-agent.jar=autoHotswap=true,disablePlugin=Spring,disablePlugin=Hibernate,disablePlugin=CxfJAXRS
                     -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
-                    -XX:+CMSClassUnloadingEnabled
-                    -Xmx1024m -Xms512m</cargo.jvmargs>
+                    -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -Xmx1024m -Xms512m</cargo.jvmargs>
                 </properties>
               </configuration>
             </configuration>
diff --git a/fit/core-reference/pom.xml b/fit/core-reference/pom.xml
index 039d1f74..0f6bf3b 100644
--- a/fit/core-reference/pom.xml
+++ b/fit/core-reference/pom.xml
@@ -1626,7 +1626,7 @@ under the License.
                     -Dspring.profiles.active=embedded
                     -javaagent:${java.home}/lib/hotswap/hotswap-agent.jar=autoHotswap=true,disablePlugin=Spring,disablePlugin=Hibernate
                     -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
-                    -XX:+CMSClassUnloadingEnabled -XX:+UseG1GC -Xmx1024m -Xms512m</cargo.jvmargs>
+                    -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -Xmx1024m -Xms512m</cargo.jvmargs>
                 </properties>
               </configuration>
             </configuration>
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GatewayRouteITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GatewayRouteITCase.java
index 5d4f8c4..4994066 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GatewayRouteITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/GatewayRouteITCase.java
@@ -32,8 +32,8 @@ import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.to.GatewayRouteTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.FilterFactory;
-import org.apache.syncope.common.lib.types.GatewayFilter;
-import org.apache.syncope.common.lib.types.GatewayPredicate;
+import org.apache.syncope.common.lib.types.GatewayRouteFilter;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
 import org.apache.syncope.common.lib.types.GatewayRouteStatus;
 import org.apache.syncope.common.lib.types.PredicateFactory;
 import org.apache.syncope.common.rest.api.service.GatewayRouteService;
@@ -69,9 +69,9 @@ public class GatewayRouteITCase extends AbstractITCase {
         GatewayRouteTO route = new GatewayRouteTO();
         route.setName("just for test");
         route.setTarget(URI.create("http://httpbin.org:80"));
-        route.getPredicates().add(new GatewayPredicate.Builder().
+        route.getPredicates().add(new GatewayRoutePredicate.Builder().
                 factory(PredicateFactory.METHOD).args(HttpMethod.GET).build());
-        route.getFilters().add(new GatewayFilter.Builder().
+        route.getFilters().add(new GatewayRouteFilter.Builder().
                 factory(FilterFactory.ADD_REQUEST_HEADER).args("X-Request-Foo, Bar").build());
         route.setStatus(GatewayRouteStatus.DRAFT);
 
@@ -103,4 +103,48 @@ public class GatewayRouteITCase extends AbstractITCase {
         int endCount = gatewayRouteService.list().size();
         assertEquals(endCount, beforeCount);
     }
+
+    @Test
+    public void exceptions() {
+        GatewayRouteTO route = new GatewayRouteTO();
+        try {
+            gatewayRouteService.create(route);
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.RequiredValuesMissing, e.getType());
+        }
+
+        route.setName("createException");
+        try {
+            gatewayRouteService.create(route);
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.RequiredValuesMissing, e.getType());
+        }
+
+        route.setTarget(URI.create("http://httpbin.org:80"));
+        Response response = gatewayRouteService.create(route);
+        assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatusInfo().getStatusCode());
+
+        try {
+            gatewayRouteService.create(route);
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.EntityExists, e.getType());
+        }
+
+        route.setKey(UUID.randomUUID().toString());
+        try {
+            gatewayRouteService.update(route);
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.NotFound, e.getType());
+        }
+        try {
+            gatewayRouteService.delete(route.getKey());
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.NotFound, e.getType());
+        }
+    }
 }
diff --git a/fit/enduser-reference/pom.xml b/fit/enduser-reference/pom.xml
index 8cd5fe2..9bf0e73 100644
--- a/fit/enduser-reference/pom.xml
+++ b/fit/enduser-reference/pom.xml
@@ -282,8 +282,7 @@ under the License.
                     -Dspring.profiles.active=embedded
                     -javaagent:${java.home}/lib/hotswap/hotswap-agent.jar=autoHotswap=true,disablePlugin=Spring,disablePlugin=Hibernate,disablePlugin=CxfJAXRS
                     -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
-                    -XX:+CMSClassUnloadingEnabled
-                    -Xmx1024m -Xms512m</cargo.jvmargs>
+                    -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -Xmx1024m -Xms512m</cargo.jvmargs>
                 </properties>
               </configuration>
             </configuration>
diff --git a/pom.xml b/pom.xml
index 1de07d5..639d09a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -396,6 +396,7 @@ under the License.
     <spring.version>5.1.7.RELEASE</spring.version>
     <spring-security.version>5.1.5.RELEASE</spring-security.version>
     <spring-boot.version>2.1.5.RELEASE</spring-boot.version>
+    <spring-cloud-gateway.version>2.1.1.RELEASE</spring-cloud-gateway.version>
 
     <openjpa.version>3.1.0</openjpa.version>
     <hikaricp.version>3.3.1</hikaricp.version>
@@ -1035,10 +1036,13 @@ under the License.
 
       <dependency>
         <groupId>org.springframework.cloud</groupId>
-        <artifactId>spring-cloud-dependencies</artifactId>
-        <version>Greenwich.SR1</version>
-        <type>pom</type>
-        <scope>import</scope>
+        <artifactId>spring-cloud-starter-gateway</artifactId>
+        <version>${spring-cloud-gateway.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
+        <version>${spring-cloud-gateway.version}</version>
       </dependency>
 
       <dependency>
@@ -1831,6 +1835,17 @@ under the License.
         </exclusions>
       </dependency>
       <dependency>
+        <groupId>org.apache.curator</groupId>
+        <artifactId>curator-x-discovery</artifactId>
+        <version>${curator.version}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>org.apache.zookeeper</groupId>
+            <artifactId>zookeeper</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
         <groupId>org.apache.zookeeper</groupId>
         <artifactId>zookeeper</artifactId>
         <version>${zookeeper.version}</version>
@@ -1848,6 +1863,24 @@ under the License.
 
       <!-- TEST -->
       <dependency>
+        <groupId>org.apache.curator</groupId>
+        <artifactId>curator-test</artifactId>
+        <version>2.13.0</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.cxf</groupId>
+        <artifactId>cxf-rt-transports-http-netty-server</artifactId>
+        <version>${cxf.version}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework.cloud</groupId>
+        <artifactId>spring-cloud-contract-wiremock</artifactId>
+        <version>${spring-cloud-gateway.version}</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
         <groupId>org.bouncycastle</groupId>
         <artifactId>bcpkix-jdk15on</artifactId>
         <version>1.61</version>
diff --git a/sra/pom.xml b/sra/pom.xml
index b9df9a8..5b0d595 100644
--- a/sra/pom.xml
+++ b/sra/pom.xml
@@ -43,18 +43,18 @@ under the License.
       <artifactId>spring-cloud-starter-gateway</artifactId>
     </dependency>
     <dependency>
-      <groupId>org.springframework.cloud</groupId>
-      <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.springframework.cloud</groupId>
-      <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-security</artifactId>
       <exclusions>
         <exclusion>
-          <artifactId>spring-boot-starter-web</artifactId>
           <groupId>org.springframework.boot</groupId>
+          <artifactId>spring-boot-starter-logging</artifactId>
         </exclusion>
-      </exclusions>
+      </exclusions>    
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.cloud</groupId>
+      <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
     </dependency>
     <dependency>
       <groupId>org.springframework.boot</groupId>
@@ -77,13 +77,8 @@ under the License.
     </dependency>
 
     <dependency>
-      <groupId>org.apache.syncope.ext.self-keymaster</groupId>
-      <artifactId>syncope-ext-self-keymaster-client</artifactId>
-      <version>${project.version}</version>
-    </dependency>
-    <dependency>
       <groupId>org.apache.syncope.common.keymaster</groupId>
-      <artifactId>syncope-common-keymaster-client-zookeeper</artifactId>
+      <artifactId>syncope-common-keymaster-client-api</artifactId>
       <version>${project.version}</version>
     </dependency>
 
@@ -124,6 +119,33 @@ under the License.
     </dependency>
 
     <dependency>
+      <groupId>org.apache.syncope.common.keymaster</groupId>
+      <artifactId>syncope-common-keymaster-client-zookeeper</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.curator</groupId>
+      <artifactId>curator-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.cxf</groupId>
+      <artifactId>cxf-rt-transports-http-netty-server</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>org.apache.geronimo.specs</groupId>
+          <artifactId>geronimo-servlet_2.5_spec</artifactId>
+        </exclusion>
+      </exclusions>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.cloud</groupId>
+      <artifactId>spring-cloud-contract-wiremock</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-test</artifactId>
       <scope>test</scope>
@@ -138,18 +160,14 @@ under the License.
   <build>
     <plugins>
       <plugin>
-        <groupId>org.springframework.boot</groupId>
-        <artifactId>spring-boot-maven-plugin</artifactId>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <inherited>true</inherited>
         <configuration>
-          <mainClass>org.apache.syncope.sra.SyncopeSRAApplication</mainClass>
+          <systemPropertyVariables>
+            <reactor.netty.http.server.accessLogEnabled>true</reactor.netty.http.server.accessLogEnabled>
+          </systemPropertyVariables>
         </configuration>
-        <executions>
-          <execution>
-            <goals>
-              <goal>repackage</goal>
-            </goals>
-          </execution>
-        </executions>
       </plugin>
     </plugins>
     
@@ -158,7 +176,6 @@ under the License.
         <directory>src/main/resources</directory>
         <filtering>true</filtering>
       </resource>
-
       <resource>
         <directory>${basedir}/../src/main/resources</directory>
         <filtering>true</filtering>
@@ -167,10 +184,131 @@ under the License.
         </includes>
       </resource>
     </resources>
+    
+    <testResources>
+      <testResource>
+        <directory>${basedir}/src/test/resources</directory>
+        <filtering>true</filtering>
+      </testResource>
+    </testResources>
   </build>
 
   <profiles>
     <profile>
+      <id>debug</id>
+
+      <properties>
+        <skipTests>true</skipTests>
+      </properties>
+
+      <dependencies>
+        <dependency>
+          <groupId>org.apache.syncope.common.keymaster</groupId>
+          <artifactId>syncope-common-keymaster-client-zookeeper</artifactId>
+          <version>${project.version}</version>
+          <scope>compile</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.curator</groupId>
+          <artifactId>curator-test</artifactId>
+          <scope>compile</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.apache.cxf</groupId>
+          <artifactId>cxf-rt-transports-http-netty-server</artifactId>
+          <scope>compile</scope>
+        </dependency>
+      </dependencies>
+
+      <build>
+        <defaultGoal>clean package io.fabric8:docker-maven-plugin:start spring-boot:run</defaultGoal>
+
+        <plugins>
+          <plugin>
+            <groupId>io.fabric8</groupId>
+            <artifactId>docker-maven-plugin</artifactId>
+            <configuration>
+              <images>
+                <image>
+                  <name>zookeeper:${zookeeper.version}</name>
+                  <run>
+                    <ports>
+                      <port>2181:2181</port>
+                    </ports>
+                    <volumes>
+                      <bind>
+                        <volume>${project.build.testOutputDirectory}/zoo.cfg:/conf/zoo.cfg</volume>
+                        <volume>${project.build.testOutputDirectory}/java.env:/conf/java.env</volume>
+                        <volume>${project.build.testOutputDirectory}/server-jaas.conf:/conf/server-jaas.conf</volume>
+                        <volume>${project.build.testOutputDirectory}/client-jaas.conf:/conf/client-jaas.conf</volume>
+                      </bind>
+                    </volumes>
+                  </run>
+                </image>
+              </images>
+            </configuration>
+          </plugin>
+
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>build-helper-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>add-source</goal>
+                </goals>
+                <configuration>
+                  <sources>
+                    <source>${basedir}/src/test/java</source>
+                  </sources>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-compiler-plugin</artifactId>
+            <configuration>
+              <excludes>
+                <exclude>**/org/apache/syncope/sra/**Test.java</exclude>
+                <exclude>**/org/apache/syncope/sra/**Keymaster*.java</exclude>
+              </excludes>
+            </configuration>
+          </plugin>
+
+          <plugin>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-maven-plugin</artifactId>
+            <configuration>
+              <mainClass>org.apache.syncope.sra.SyncopeSRAApplication</mainClass>
+              <systemPropertyVariables>
+                <reactor.netty.http.server.accessLogEnabled>true</reactor.netty.http.server.accessLogEnabled>
+              </systemPropertyVariables>
+              <jvmArguments>
+                -Xdebug -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n
+              </jvmArguments>
+            </configuration>
+          </plugin>
+        </plugins>
+        
+        <resources>
+          <resource>
+            <directory>${basedir}/src/test/resources</directory>
+            <filtering>true</filtering>
+          </resource>
+        </resources>
+
+        <testResources>
+          <testResource>
+            <directory>${basedir}/../common/keymaster/client-zookeeper/src/main/resources</directory>
+            <filtering>true</filtering>
+          </testResource>
+        </testResources>
+      </build>
+    </profile>
+
+    <profile>
       <id>site</id>
 
       <build>
diff --git a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAStartup.java b/sra/src/main/java/org/apache/syncope/sra/CustomGatewayFilterFactory.java
similarity index 54%
copy from sra/src/main/java/org/apache/syncope/sra/SyncopeSRAStartup.java
copy to sra/src/main/java/org/apache/syncope/sra/CustomGatewayFilterFactory.java
index 08ea447..7ac2cd8 100644
--- a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAStartup.java
+++ b/sra/src/main/java/org/apache/syncope/sra/CustomGatewayFilterFactory.java
@@ -18,14 +18,32 @@
  */
 package org.apache.syncope.sra;
 
-import org.springframework.context.ApplicationListener;
-import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
 
-public class SyncopeSRAStartup extends SyncopeSRAStartStop
-        implements ApplicationListener<ContextRefreshedEvent> {
+/**
+ * Base class for custom gateway filter factories.
+ */
+public abstract class CustomGatewayFilterFactory
+        extends AbstractGatewayFilterFactory<CustomGatewayFilterFactory.Config> {
 
-    @Override
-    public void onApplicationEvent(final ContextRefreshedEvent event) {
-        serviceOps.register(getNetworkService());
+    public static class Config {
+
+        private String data;
+
+        public String getData() {
+            return data;
+        }
+
+        public void setData(final String data) {
+            this.data = data;
+        }
     }
+
+    public CustomGatewayFilterFactory() {
+        super(CustomGatewayFilterFactory.Config.class);
+    }
+
+    @Override
+    public abstract GatewayFilter apply(Config config);
 }
diff --git a/sra/src/main/java/org/apache/syncope/sra/CustomRoutePredicateFactory.java b/sra/src/main/java/org/apache/syncope/sra/CustomRoutePredicateFactory.java
new file mode 100644
index 0000000..22d9299
--- /dev/null
+++ b/sra/src/main/java/org/apache/syncope/sra/CustomRoutePredicateFactory.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra;
+
+import java.util.function.Predicate;
+import org.springframework.cloud.gateway.handler.AsyncPredicate;
+import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Base class for custom predicate factories.
+ */
+public abstract class CustomRoutePredicateFactory
+        extends AbstractRoutePredicateFactory<CustomRoutePredicateFactory.Config> {
+
+    public static class Config {
+
+        private String data;
+
+        public String getData() {
+            return data;
+        }
+
+        public void setData(final String data) {
+            this.data = data;
+        }
+    }
+
+    public CustomRoutePredicateFactory() {
+        super(CustomRoutePredicateFactory.Config.class);
+    }
+
+    @Override
+    public abstract AsyncPredicate<ServerWebExchange> applyAsync(Config config);
+
+    @Override
+    public Predicate<ServerWebExchange> apply(final Config config) {
+        throw new UnsupportedOperationException(getClass().getName() + " is only async.");
+    }
+}
diff --git a/sra/src/main/java/org/apache/syncope/sra/ManagementController.java b/sra/src/main/java/org/apache/syncope/sra/ManagementController.java
new file mode 100644
index 0000000..7d15556
--- /dev/null
+++ b/sra/src/main/java/org/apache/syncope/sra/ManagementController.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.gateway.route.Route;
+import org.springframework.cloud.gateway.route.RouteDefinition;
+import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
+import org.springframework.cloud.gateway.route.RouteLocator;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping(path = "/management")
+public class ManagementController {
+
+    @Autowired
+    private RouteRefresher routeRefresher;
+
+    @Autowired
+    private RouteDefinitionLocator routeDefinitionLocator;
+
+    @Autowired
+    private RouteLocator routeLocator;
+
+    @PostMapping("/routes/refresh")
+    public Mono<Void> refresh() {
+        routeRefresher.refresh();
+        return Mono.empty();
+    }
+
+    @GetMapping("/routes")
+    public Mono<List<Map<String, Object>>> routes() {
+        Mono<Map<String, RouteDefinition>> routeDefs =
+                routeDefinitionLocator.getRouteDefinitions().collectMap(RouteDefinition::getId);
+        Mono<List<Route>> routes = routeLocator.getRoutes().collectList();
+        return Mono.zip(routeDefs, routes).map(tuple -> {
+            Map<String, RouteDefinition> defs = tuple.getT1();
+            List<Route> routeList = tuple.getT2();
+            List<Map<String, Object>> allRoutes = new ArrayList<>();
+
+            routeList.forEach(route -> {
+                Map<String, Object> r = new HashMap<>();
+                r.put("route_id", route.getId());
+                r.put("order", route.getOrder());
+
+                if (defs.containsKey(route.getId())) {
+                    r.put("route_definition", defs.get(route.getId()));
+                } else {
+                    Map<String, Object> obj = new HashMap<>();
+
+                    obj.put("predicate", route.getPredicate().toString());
+
+                    if (!route.getFilters().isEmpty()) {
+                        obj.put("filters",
+                                route.getFilters().stream().map(Object::toString).collect(Collectors.toList()));
+                    }
+
+                    if (!obj.isEmpty()) {
+                        r.put("route_object", obj);
+                    }
+                }
+                allRoutes.add(r);
+            });
+
+            return allRoutes;
+        });
+    }
+}
diff --git a/sra/src/main/java/org/apache/syncope/sra/RouteProvider.java b/sra/src/main/java/org/apache/syncope/sra/RouteProvider.java
new file mode 100644
index 0000000..709705a
--- /dev/null
+++ b/sra/src/main/java/org/apache/syncope/sra/RouteProvider.java
@@ -0,0 +1,472 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra;
+
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.client.lib.AnonymousAuthenticationHandler;
+import org.apache.syncope.client.lib.SyncopeClient;
+import org.apache.syncope.client.lib.SyncopeClientFactoryBean;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
+import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
+import org.apache.syncope.common.lib.to.GatewayRouteTO;
+import org.apache.syncope.common.lib.types.GatewayRouteFilter;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
+import org.apache.syncope.common.lib.types.GatewayRouteStatus;
+import org.apache.syncope.common.rest.api.service.GatewayRouteService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
+import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.RemoveRequestHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory;
+import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
+import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
+import org.springframework.cloud.gateway.handler.AsyncPredicate;
+import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory;
+import org.springframework.cloud.gateway.handler.predicate.BeforeRoutePredicateFactory;
+import org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory;
+import org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactory;
+import org.springframework.cloud.gateway.handler.predicate.HeaderRoutePredicateFactory;
+import org.springframework.cloud.gateway.handler.predicate.HostRoutePredicateFactory;
+import org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateFactory;
+import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
+import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory;
+import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
+import org.springframework.cloud.gateway.route.Route;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+
+@Component
+public class RouteProvider {
+
+    private static final Logger LOG = LoggerFactory.getLogger(RouteProvider.class);
+
+    @Autowired
+    private ServiceOps serviceOps;
+
+    @Autowired
+    private ConfigurableApplicationContext ctx;
+
+    @Value("${anonymousUser}")
+    private String anonymousUser;
+
+    @Value("${anonymousKey}")
+    private String anonymousKey;
+
+    @Value("${useGZIPCompression}")
+    private boolean useGZIPCompression;
+
+    private SyncopeClient client;
+
+    @SuppressWarnings("unchecked")
+    private GatewayFilter toFilter(final String routeId, final GatewayRouteFilter gwfilter)
+            throws ClassNotFoundException {
+
+        GatewayFilter filter;
+
+        switch (gwfilter.getFactory()) {
+            case ADD_REQUEST_HEADER:
+                String[] addRequestHeaderArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(AddRequestHeaderGatewayFilterFactory.class).
+                        apply(c -> c.setName(addRequestHeaderArgs[0].trim()).
+                        setValue(addRequestHeaderArgs[1].trim()));
+                break;
+
+            case ADD_REQUEST_PARAMETER:
+                String[] addRequestParameterArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(AddRequestParameterGatewayFilterFactory.class).
+                        apply(c -> c.setName(addRequestParameterArgs[0].trim()).
+                        setValue(addRequestParameterArgs[1].trim()));
+                break;
+
+            case ADD_RESPONSE_HEADER:
+                String[] addResponseHeaderArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(AddResponseHeaderGatewayFilterFactory.class).
+                        apply(c -> c.setName(addResponseHeaderArgs[0].trim()).
+                        setValue(addResponseHeaderArgs[1].trim()));
+                break;
+
+            case HYSTRIX:
+                String[] hystrixArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(HystrixGatewayFilterFactory.class).
+                        apply(routeId, c -> {
+                            if (StringUtils.isNotBlank(hystrixArgs[0])) {
+                                c.setName(hystrixArgs[0].trim());
+                            }
+                            if (StringUtils.isNotBlank(hystrixArgs[1])) {
+                                c.setFallbackUri(hystrixArgs[1].trim());
+                            }
+                        });
+                break;
+
+            case FALLBACK_HEADERS:
+                String[] fallbackHeadersArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(FallbackHeadersGatewayFilterFactory.class).
+                        apply(c -> {
+                            if (StringUtils.isNotBlank(fallbackHeadersArgs[0])) {
+                                c.setCauseExceptionMessageHeaderName(fallbackHeadersArgs[0].trim());
+                            }
+                            if (StringUtils.isNotBlank(fallbackHeadersArgs[1])) {
+                                c.setExecutionExceptionMessageHeaderName(fallbackHeadersArgs[1].trim());
+                            }
+                            if (StringUtils.isNotBlank(fallbackHeadersArgs[2])) {
+                                c.setExecutionExceptionTypeHeaderName(fallbackHeadersArgs[2].trim());
+                            }
+                            if (StringUtils.isNotBlank(fallbackHeadersArgs[3])) {
+                                c.setRootCauseExceptionTypeHeaderName(fallbackHeadersArgs[3].trim());
+                            }
+                        });
+                break;
+
+            case PREFIX_PATH:
+                filter = ctx.getBean(PrefixPathGatewayFilterFactory.class).
+                        apply(c -> c.setPrefix(gwfilter.getArgs().trim()));
+                break;
+
+            case PRESERVE_HOST_HEADER:
+                filter = ctx.getBean(PreserveHostHeaderGatewayFilterFactory.class).apply();
+                break;
+
+            case REDIRECT:
+                String[] redirectArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(RedirectToGatewayFilterFactory.class).
+                        apply(redirectArgs[0].trim(), redirectArgs[1].trim());
+                break;
+
+            case REMOVE_REQUEST_HEADER:
+                filter = ctx.getBean(RemoveRequestHeaderGatewayFilterFactory.class).
+                        apply(c -> c.setName(gwfilter.getArgs().trim()));
+                break;
+
+            case REMOVE_RESPONSE_HEADER:
+                filter = ctx.getBean(RemoveResponseHeaderGatewayFilterFactory.class).
+                        apply(c -> c.setName(gwfilter.getArgs().trim()));
+                break;
+
+            case REQUEST_RATE_LIMITER:
+                String[] requestRateLimiterArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(RequestRateLimiterGatewayFilterFactory.class).
+                        apply(c -> {
+                            if (StringUtils.isNotBlank(requestRateLimiterArgs[0])) {
+                                c.setDenyEmptyKey(BooleanUtils.toBoolean(requestRateLimiterArgs[0].trim()));
+                            }
+                            if (StringUtils.isNotBlank(requestRateLimiterArgs[1])) {
+                                c.setEmptyKeyStatus(requestRateLimiterArgs[1].trim());
+                            }
+                            if (StringUtils.isNotBlank(requestRateLimiterArgs[2])) {
+                                c.setKeyResolver(ctx.getBean(requestRateLimiterArgs[2].trim(), KeyResolver.class));
+                            }
+                            if (StringUtils.isNotBlank(requestRateLimiterArgs[3])) {
+                                c.setRateLimiter(ctx.getBean(requestRateLimiterArgs[3].trim(), RateLimiter.class));
+                            }
+                            if (StringUtils.isNotBlank(requestRateLimiterArgs[4])) {
+                                c.setStatusCode(HttpStatus.valueOf(requestRateLimiterArgs[4].trim()));
+                            }
+                        });
+                break;
+
+            case REWRITE_PATH:
+                String[] rewritePathArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(RewritePathGatewayFilterFactory.class).
+                        apply(c -> c.setRegexp(rewritePathArgs[0].trim()).
+                        setReplacement(rewritePathArgs[1].trim()));
+                break;
+
+            case RETRY:
+                filter = ctx.getBean(RetryGatewayFilterFactory.class).
+                        apply(c -> c.setRetries(Integer.valueOf(gwfilter.getArgs().trim())));
+                break;
+
+            case SECURE_HEADERS:
+                filter = ctx.getBean(SecureHeadersGatewayFilterFactory.class).apply(c -> {
+                });
+                break;
+
+            case SET_PATH:
+                filter = ctx.getBean(SetPathGatewayFilterFactory.class).
+                        apply(c -> c.setTemplate(gwfilter.getArgs().trim()));
+                break;
+
+            case SET_REQUEST_HEADER:
+                String[] setRequestHeaderArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(SetRequestHeaderGatewayFilterFactory.class).
+                        apply(c -> c.setName(setRequestHeaderArgs[0].trim()).
+                        setValue(setRequestHeaderArgs[1].trim()));
+                break;
+
+            case SET_RESPONSE_HEADER:
+                String[] setResponseHeaderArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(SetResponseHeaderGatewayFilterFactory.class).
+                        apply(c -> c.setName(setResponseHeaderArgs[0].trim()).
+                        setValue(setResponseHeaderArgs[1].trim()));
+                break;
+
+            case REWRITE_RESPONSE_HEADER:
+                String[] rewriteResponseHeaderArgs = gwfilter.getArgs().split(",");
+                filter = ctx.getBean(RewriteResponseHeaderGatewayFilterFactory.class).
+                        apply(c -> c.setReplacement(rewriteResponseHeaderArgs[2].trim()).
+                        setRegexp(rewriteResponseHeaderArgs[1].trim()).
+                        setName(rewriteResponseHeaderArgs[0].trim()));
+                break;
+
+            case SET_STATUS:
+                filter = ctx.getBean(SetStatusGatewayFilterFactory.class).
+                        apply(c -> c.setStatus(gwfilter.getArgs().trim()));
+                break;
+
+            case SAVE_SESSION:
+                filter = ctx.getBean(SaveSessionGatewayFilterFactory.class).apply(c -> {
+                });
+                break;
+
+            case STRIP_PREFIX:
+                filter = ctx.getBean(StripPrefixGatewayFilterFactory.class).
+                        apply(c -> c.setParts(Integer.valueOf(gwfilter.getArgs().trim())));
+                break;
+
+            case REQUEST_HEADER_TO_REQUEST_URI:
+                filter = ctx.getBean(RequestHeaderToRequestUriGatewayFilterFactory.class).
+                        apply(c -> c.setName(gwfilter.getArgs().trim()));
+                break;
+
+            case SET_REQUEST_SIZE:
+                filter = ctx.getBean(RequestSizeGatewayFilterFactory.class).
+                        apply(c -> c.setMaxSize(Long.valueOf(gwfilter.getArgs().trim())));
+                break;
+
+            case CUSTOM:
+                String[] customArgs = gwfilter.getArgs().split(";");
+                CustomGatewayFilterFactory factory;
+                if (ctx.getBeanFactory().containsSingleton(customArgs[0])) {
+                    factory = (CustomGatewayFilterFactory) ctx.getBeanFactory().getSingleton(customArgs[0]);
+                } else {
+                    factory = (CustomGatewayFilterFactory) ctx.getBeanFactory().
+                            createBean(Class.forName(customArgs[0]), AbstractBeanDefinition.AUTOWIRE_BY_TYPE, false);
+                    ctx.getBeanFactory().registerSingleton(customArgs[0], factory);
+                }
+                filter = factory.apply(c -> c.setData(customArgs[1]));
+                break;
+
+            default:
+                filter = null;
+        }
+
+        if (filter == null) {
+            throw new IllegalArgumentException("Could not translate " + gwfilter);
+        }
+
+        return filter instanceof Ordered ? filter : new OrderedGatewayFilter(filter, 0);
+    }
+
+    private AsyncPredicate<ServerWebExchange> toPredicate(final GatewayRoutePredicate gwpredicate, final boolean negate)
+            throws ClassNotFoundException {
+
+        AsyncPredicate<ServerWebExchange> predicate;
+        switch (gwpredicate.getFactory()) {
+            case AFTER:
+                predicate = ctx.getBean(AfterRoutePredicateFactory.class).
+                        applyAsync(c -> c.setDatetime(ZonedDateTime.parse(gwpredicate.getArgs().trim())));
+                break;
+
+            case BEFORE:
+                predicate = ctx.getBean(BeforeRoutePredicateFactory.class).
+                        applyAsync(c -> c.setDatetime(ZonedDateTime.parse(gwpredicate.getArgs().trim())));
+                break;
+
+            case BETWEEN:
+                String[] betweenArgs = gwpredicate.getArgs().split(",");
+                predicate = ctx.getBean(BetweenRoutePredicateFactory.class).
+                        applyAsync(c -> c.setDatetime1(ZonedDateTime.parse(betweenArgs[0].trim())).
+                        setDatetime2(ZonedDateTime.parse(betweenArgs[1].trim())));
+                break;
+
+            case COOKIE:
+                String[] cookieArgs = gwpredicate.getArgs().split(",");
+                predicate = ctx.getBean(CookieRoutePredicateFactory.class).
+                        applyAsync(c -> c.setName(cookieArgs[0].trim()).
+                        setRegexp(cookieArgs[1].trim()));
+                break;
+
+            case HEADER:
+                predicate = ctx.getBean(HeaderRoutePredicateFactory.class).
+                        applyAsync(c -> c.setHeader(gwpredicate.getArgs().trim()));
+                break;
+
+            case HOST:
+                String[] hostArgs = gwpredicate.getArgs().split(",");
+                predicate = ctx.getBean(HostRoutePredicateFactory.class).
+                        applyAsync(c -> c.setPatterns(Arrays.asList(hostArgs)));
+                break;
+
+            case METHOD:
+                predicate = ctx.getBean(MethodRoutePredicateFactory.class).
+                        applyAsync(c -> c.setMethod(HttpMethod.resolve(gwpredicate.getArgs().trim())));
+                break;
+
+            case PATH:
+                String[] pathArgs = gwpredicate.getArgs().split(",");
+                predicate = ctx.getBean(PathRoutePredicateFactory.class).
+                        applyAsync(c -> c.setPatterns(Arrays.asList(pathArgs)));
+                break;
+
+            case QUERY:
+                String[] queryArgs = gwpredicate.getArgs().split(",");
+                predicate = ctx.getBean(QueryRoutePredicateFactory.class).
+                        applyAsync(c -> c.setParam(queryArgs[0].trim()).
+                        setRegexp(queryArgs[1].trim()));
+                break;
+
+            case REMOTE_ADDR:
+                String[] remoteAddrArgs = gwpredicate.getArgs().split(",");
+                predicate = ctx.getBean(RemoteAddrRoutePredicateFactory.class).
+                        applyAsync(c -> c.setSources(Arrays.asList(remoteAddrArgs)));
+                break;
+
+            case CUSTOM:
+                String[] customArgs = gwpredicate.getArgs().split(";");
+                CustomRoutePredicateFactory factory;
+                if (ctx.getBeanFactory().containsSingleton(customArgs[0])) {
+                    factory = (CustomRoutePredicateFactory) ctx.getBeanFactory().getSingleton(customArgs[0]);
+                } else {
+                    factory = (CustomRoutePredicateFactory) ctx.getBeanFactory().
+                            createBean(Class.forName(customArgs[0]), AbstractBeanDefinition.AUTOWIRE_BY_TYPE, false);
+                    ctx.getBeanFactory().registerSingleton(customArgs[0], factory);
+                }
+                predicate = factory.applyAsync(c -> c.setData(customArgs[1]));
+                break;
+
+            default:
+                predicate = null;
+        }
+
+        if (predicate == null) {
+            throw new IllegalArgumentException("Could not translate " + gwpredicate);
+        }
+
+        if (negate) {
+            predicate.negate();
+        }
+        return predicate;
+    }
+
+    private Route.AsyncBuilder toRoute(final GatewayRouteTO gwroute) {
+        Route.AsyncBuilder builder = new Route.AsyncBuilder().
+                id(gwroute.getKey()).order(gwroute.getOrder()).uri(gwroute.getTarget());
+
+        if (gwroute.getPredicates().isEmpty()) {
+            builder.predicate(exchange -> true);
+        } else {
+            gwroute.getPredicates().forEach(gwpredicate -> {
+                if (builder.getPredicate() == null) {
+                    try {
+                        builder.asyncPredicate(toPredicate(gwpredicate, gwpredicate.isNegate()));
+                    } catch (Exception e) {
+                        LOG.error("Could not translate {}, skipping", gwpredicate, e);
+                    }
+                } else {
+                    try {
+                        switch (gwpredicate.getCond()) {
+                            case OR:
+                                builder.or(toPredicate(gwpredicate, gwpredicate.isNegate()));
+                                break;
+
+                            case AND:
+                            default:
+                                builder.and(toPredicate(gwpredicate, gwpredicate.isNegate()));
+                        }
+                    } catch (Exception e) {
+                        LOG.error("Could not translate {}, skipping", gwpredicate, e);
+                    }
+                }
+            });
+        }
+
+        if (!gwroute.getFilters().isEmpty()) {
+            builder.filters(gwroute.getFilters().stream().
+                    map(gwfilter -> {
+                        try {
+                            return toFilter(gwroute.getKey(), gwfilter);
+                        } catch (Exception e) {
+                            LOG.error("Could not translate {}, skipping", gwfilter, e);
+                            return null;
+                        }
+                    }).
+                    filter(Objects::nonNull).
+                    collect(Collectors.toList()));
+        }
+
+        return builder;
+    }
+
+    public List<Route.AsyncBuilder> fetch() {
+        synchronized (this) {
+            if (client == null) {
+                try {
+                    client = new SyncopeClientFactoryBean().
+                            setAddress(serviceOps.get(NetworkService.Type.CORE).getAddress()).
+                            setUseCompression(useGZIPCompression).
+                            create(new AnonymousAuthenticationHandler(anonymousUser, anonymousKey));
+                } catch (Exception e) {
+                    LOG.error("Could not init SyncopeClient", e);
+                    return Collections.emptyList();
+                }
+            }
+        }
+
+        return client.getService(GatewayRouteService.class).list().stream().
+                filter(gwroute -> gwroute.getStatus() == GatewayRouteStatus.PUBLISHED).
+                map(gwroute -> toRoute(gwroute)).
+                collect(Collectors.toList());
+    }
+}
diff --git a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/init/IdMImplementationTypeLoader.java b/sra/src/main/java/org/apache/syncope/sra/RouteRefresher.java
similarity index 61%
copy from core/idm/logic/src/main/java/org/apache/syncope/core/logic/init/IdMImplementationTypeLoader.java
copy to sra/src/main/java/org/apache/syncope/sra/RouteRefresher.java
index b3a039c..8d19965 100644
--- a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/init/IdMImplementationTypeLoader.java
+++ b/sra/src/main/java/org/apache/syncope/sra/RouteRefresher.java
@@ -16,23 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.logic.init;
+package org.apache.syncope.sra;
 
-import org.apache.syncope.common.lib.types.IdMImplementationType;
-import org.apache.syncope.common.lib.types.ImplementationTypesHolder;
-import org.apache.syncope.core.persistence.api.SyncopeCoreLoader;
+import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.ApplicationEventPublisherAware;
 import org.springframework.stereotype.Component;
 
 @Component
-public class IdMImplementationTypeLoader implements SyncopeCoreLoader {
+public class RouteRefresher implements ApplicationEventPublisherAware {
+
+    private ApplicationEventPublisher publisher;
 
     @Override
-    public int getOrder() {
-        return Integer.MIN_VALUE;
+    public void setApplicationEventPublisher(final ApplicationEventPublisher publisher) {
+        this.publisher = publisher;
     }
 
-    @Override
-    public void load() {
-        ImplementationTypesHolder.getInstance().putAll(IdMImplementationType.values());
+    public void refresh() {
+        publisher.publishEvent(new RefreshRoutesEvent(this));
     }
 }
diff --git a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java b/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java
index 09c620d..c0bda58 100644
--- a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java
+++ b/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAApplication.java
@@ -18,21 +18,66 @@
  */
 package org.apache.syncope.sra;
 
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.gateway.route.RouteLocator;
 import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
+import org.springframework.context.EnvironmentAware;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.core.env.Environment;
+import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
+import org.springframework.web.util.pattern.PathPatternParser;
+import reactor.core.publisher.Flux;
 
+@PropertySource("classpath:sra.properties")
+@PropertySource(value = "file:${conf.directory}/sra.properties", ignoreResourceNotFound = true)
+@EnableWebFluxSecurity
 @SpringBootApplication
-public class SyncopeSRAApplication {
+public class SyncopeSRAApplication implements EnvironmentAware {
 
     public static void main(final String[] args) {
         SpringApplication.run(SyncopeSRAApplication.class, args);
     }
 
+    @Autowired
+    private RouteProvider provider;
+
+    private Environment env;
+
+    @Override
+    public void setEnvironment(final Environment env) {
+        this.env = env;
+    }
+
     @Bean
     public RouteLocator routes(final RouteLocatorBuilder builder) {
-        return builder.routes().build();
+        return () -> Flux.fromIterable(provider.fetch()).map(routeBuilder -> routeBuilder.build());
+    }
+
+    @Bean
+    public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
+        http.csrf().disable().securityMatcher(
+                new PathPatternParserServerWebExchangeMatcher(new PathPatternParser().parse("/management/**"))).
+                authorizeExchange().anyExchange().hasRole(IdRepoEntitlement.ANONYMOUS).and().httpBasic();
+        return http.build();
+    }
+
+    @Bean
+    public MapReactiveUserDetailsService userDetailsService() {
+        UserDetails user = User.builder().
+                username(env.getProperty("anonymousUser")).
+                password("{noop}" + env.getProperty("anonymousKey")).
+                roles(IdRepoEntitlement.ANONYMOUS).
+                build();
+        return new MapReactiveUserDetailsService(user);
     }
 }
diff --git a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAShutdown.java b/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAShutdown.java
index d335836..4ed772c 100644
--- a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAShutdown.java
+++ b/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAShutdown.java
@@ -20,7 +20,9 @@ package org.apache.syncope.sra;
 
 import org.springframework.context.ApplicationListener;
 import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.stereotype.Component;
 
+@Component
 public class SyncopeSRAShutdown extends SyncopeSRAStartStop
         implements ApplicationListener<ContextClosedEvent> {
 
diff --git a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAStartup.java b/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAStartup.java
index 08ea447..cbc7b59 100644
--- a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAStartup.java
+++ b/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAStartup.java
@@ -20,7 +20,9 @@ package org.apache.syncope.sra;
 
 import org.springframework.context.ApplicationListener;
 import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.stereotype.Component;
 
+@Component
 public class SyncopeSRAStartup extends SyncopeSRAStartStop
         implements ApplicationListener<ContextRefreshedEvent> {
 
diff --git a/sra/src/main/resources/application.properties b/sra/src/main/resources/application.properties
index 98bc964..b50f4f9 100644
--- a/sra/src/main/resources/application.properties
+++ b/sra/src/main/resources/application.properties
@@ -20,12 +20,8 @@ spring.main.banner-mode=log
 
 server.port=8080
 
-management.endpoint.gateway.enabled=true
-management.endpoints.web.exposure.include=gateway
-
 spring.cloud.gateway.metrics.enabled=true
 management.endpoint.metrics.enabled=true
-management.endpoints.web.exposure.include=*
 management.endpoint.prometheus.enabled=true
 management.metrics.export.prometheus.enabled=true
 
diff --git a/sra/src/main/resources/log4j2.xml b/sra/src/main/resources/log4j2.xml
index d7211f5..3e0fa0c 100644
--- a/sra/src/main/resources/log4j2.xml
+++ b/sra/src/main/resources/log4j2.xml
@@ -52,11 +52,18 @@ under the License.
     <asyncLogger name="org.apache.syncope.client.lib" additivity="false" level="OFF">
       <appender-ref ref="main"/>
     </asyncLogger>
-
     <asyncLogger name="org.apache.syncope.sra" additivity="false" level="INFO">
       <appender-ref ref="main"/>
     </asyncLogger>
 
+    <asyncLogger name="org.apache.cxf" additivity="false" level="ERROR">
+      <appender-ref ref="main"/>
+    </asyncLogger>
+
+    <asyncLogger name="org.springframework.cloud.gateway" additivity="false" level="INFO">
+      <appender-ref ref="main"/>
+    </asyncLogger>
+
     <!-- Requires -Dreactor.netty.http.server.accessLogEnabled=true to work-->
     <asyncLogger name="reactor.netty.http.server.AccessLog" additivity="false" level="INFO">
       <appender-ref ref="access"/>
diff --git a/sra/src/main/resources/sra.properties b/sra/src/main/resources/sra.properties
index 6789cfd..77cb93e 100644
--- a/sra/src/main/resources/sra.properties
+++ b/sra/src/main/resources/sra.properties
@@ -14,8 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-sra.directory=${conf.directory}
-
 anonymousUser=${anonymousUser}
 anonymousKey=${anonymousKey}
 
diff --git a/sra/src/test/java/org/apache/syncope/sra/BodyPropertyAddingGatewayFilterFactory.java b/sra/src/test/java/org/apache/syncope/sra/BodyPropertyAddingGatewayFilterFactory.java
new file mode 100644
index 0000000..aec3dcc
--- /dev/null
+++ b/sra/src/test/java/org/apache/syncope/sra/BodyPropertyAddingGatewayFilterFactory.java
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.zookeeper.common.IOUtils;
+import org.reactivestreams.Publisher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.cloud.gateway.filter.GatewayFilter;
+import org.springframework.cloud.gateway.filter.GatewayFilterChain;
+import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
+import org.springframework.core.Ordered;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseCookie;
+import org.springframework.http.client.reactive.ClientHttpResponse;
+import org.springframework.http.server.reactive.ServerHttpResponse;
+import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * Inspired by {@link org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory}.
+ */
+public class BodyPropertyAddingGatewayFilterFactory extends CustomGatewayFilterFactory {
+
+    private static final Logger LOG = LoggerFactory.getLogger(BodyPropertyAddingGatewayFilterFactory.class);
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private static boolean isCompressed(final byte[] bytes) {
+        if ((bytes == null) || (bytes.length < 2)) {
+            return false;
+        } else {
+            return ((bytes[0] == (byte) (GZIPInputStream.GZIP_MAGIC))
+                    && (bytes[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8)));
+        }
+    }
+
+    @Override
+    public GatewayFilter apply(final Config config) {
+        return new ModifyResponseGatewayFilter(config);
+    }
+
+    public class ModifyResponseGatewayFilter implements GatewayFilter, Ordered {
+
+        private final Config config;
+
+        public ModifyResponseGatewayFilter(final Config config) {
+            this.config = config;
+        }
+
+        @Override
+        public Mono<Void> filter(final ServerWebExchange exchange, final GatewayFilterChain chain) {
+            ServerHttpResponse originalResponse = exchange.getResponse();
+
+            DataBufferFactory bufferFactory = originalResponse.bufferFactory();
+            ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(originalResponse) {
+
+                @Override
+                public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
+                    if (body instanceof Flux) {
+                        Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
+
+                        return super.writeWith(flux.buffer().map(dataBuffers -> {
+                            ByteArrayOutputStream payload = new ByteArrayOutputStream();
+                            dataBuffers.forEach(buffer -> {
+                                byte[] array = new byte[buffer.readableByteCount()];
+                                buffer.read(array);
+                                try {
+                                    payload.write(array);
+                                } catch (IOException e) {
+                                    LOG.error("While reading original body content", e);
+                                }
+                            });
+
+                            byte[] input = payload.toByteArray();
+
+                            InputStream is = null;
+                            boolean compressed = false;
+                            byte[] output;
+                            try {
+                                if (isCompressed(input)) {
+                                    compressed = true;
+                                    is = new GZIPInputStream(new ByteArrayInputStream(input));
+                                } else {
+                                    is = new ByteArrayInputStream(input);
+                                }
+
+                                ObjectNode content = (ObjectNode) MAPPER.readTree(is);
+                                String[] kv = config.getData().split("=");
+                                content.put(kv[0], kv[1]);
+
+                                output = MAPPER.writeValueAsBytes(content);
+                            } catch (IOException e) {
+                                LOG.error("While (de)serializing as JSON", e);
+                                output = ArrayUtils.clone(input);
+                            } finally {
+                                IOUtils.closeStream(is);
+                            }
+
+                            if (compressed) {
+                                try (ByteArrayOutputStream baos = new ByteArrayOutputStream(output.length);
+                                        GZIPOutputStream gzipos = new GZIPOutputStream(baos)) {
+
+                                    gzipos.write(output);
+                                    gzipos.close();
+                                    output = baos.toByteArray();
+                                } catch (IOException e) {
+                                    LOG.error("While GZIP-encoding output", e);
+                                }
+                            }
+
+                            return bufferFactory.wrap(output);
+                        }));
+                    }
+
+                    return super.writeWith(body);
+                }
+            };
+
+            return chain.filter(exchange.mutate().response(responseDecorator).build());
+        }
+
+        @Override
+        public int getOrder() {
+            return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
+        }
+    }
+
+    public class ResponseAdapter implements ClientHttpResponse {
+
+        private final Flux<DataBuffer> flux;
+
+        private final HttpHeaders headers;
+
+        @SuppressWarnings("unchecked")
+        public ResponseAdapter(final Publisher<? extends DataBuffer> body, final HttpHeaders headers) {
+            this.headers = headers;
+            if (body instanceof Flux) {
+                flux = (Flux) body;
+            } else {
+                flux = ((Mono) body).flux();
+            }
+        }
+
+        @Override
+        public Flux<DataBuffer> getBody() {
+            return flux;
+        }
+
+        @Override
+        public HttpHeaders getHeaders() {
+            return headers;
+        }
+
+        @Override
+        public HttpStatus getStatusCode() {
+            return null;
+        }
+
+        @Override
+        public int getRawStatusCode() {
+            return 0;
+        }
+
+        @Override
+        public MultiValueMap<String, ResponseCookie> getCookies() {
+            return null;
+        }
+    }
+}
diff --git a/sra/src/test/java/org/apache/syncope/sra/BodyPropertyMatchingRoutePredicateFactory.java b/sra/src/test/java/org/apache/syncope/sra/BodyPropertyMatchingRoutePredicateFactory.java
new file mode 100644
index 0000000..bff3903
--- /dev/null
+++ b/sra/src/test/java/org/apache/syncope/sra/BodyPropertyMatchingRoutePredicateFactory.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.util.List;
+import org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter;
+import org.springframework.cloud.gateway.handler.AsyncPredicate;
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.http.codec.HttpMessageReader;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
+import org.springframework.web.reactive.function.server.HandlerStrategies;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * Inspired by {@link org.springframework.cloud.gateway.handler.predicate.ReadBodyPredicateFactory}.
+ */
+public class BodyPropertyMatchingRoutePredicateFactory extends CustomRoutePredicateFactory {
+
+    private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";
+
+    private static final List<HttpMessageReader<?>> MESSAGE_READERS =
+            HandlerStrategies.withDefaults().messageReaders();
+
+    @Override
+    public AsyncPredicate<ServerWebExchange> applyAsync(final Config config) {
+        return exchange -> {
+            JsonNode cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
+            if (cachedBody == null) {
+                // Join all the DataBuffers so we have a single DataBuffer for the body
+                return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
+                    // Update the retain counts so we can read the body twice, once to parse into an object
+                    // that we can test the predicate against and a second time when the HTTP client sends
+                    // the request downstream 
+                    // Note: if we end up reading the body twice we will run into a problem, but as of right
+                    // now there is no good use case for doing this
+                    DataBufferUtils.retain(dataBuffer);
+                    // Make a slice for each read so each read has its own read/write indexes
+                    Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(
+                            dataBuffer.slice(0, dataBuffer.readableByteCount())));
+
+                    ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
+
+                        @Override
+                        public Flux<DataBuffer> getBody() {
+                            return cachedFlux;
+                        }
+                    };
+                    return ServerRequest.create(exchange.mutate().request(mutatedRequest).build(), MESSAGE_READERS).
+                            bodyToMono(JsonNode.class).doOnNext(value -> {
+                        exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, value);
+                        exchange.getAttributes().put(AdaptCachedBodyGlobalFilter.CACHED_REQUEST_BODY_KEY, cachedFlux);
+                    }).map(objectValue -> objectValue.has(config.getData()));
+                });
+            } else {
+                return Mono.just(cachedBody.has(config.getData()));
+            }
+        };
+    }
+}
diff --git a/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATest.java b/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATest.java
new file mode 100644
index 0000000..3b16c64
--- /dev/null
+++ b/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATest.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.net.URI;
+import org.apache.syncope.common.lib.to.GatewayRouteTO;
+import org.apache.syncope.common.lib.types.FilterFactory;
+import org.apache.syncope.common.lib.types.GatewayRouteFilter;
+import org.apache.syncope.common.lib.types.GatewayRoutePredicate;
+import org.apache.syncope.common.lib.types.GatewayRouteStatus;
+import org.apache.syncope.common.lib.types.PredicateCond;
+import org.apache.syncope.common.lib.types.PredicateFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.reactive.function.BodyInserters;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@AutoConfigureWireMock(port = 0)
+public class SyncopeSRATest {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @Autowired
+    private WebTestClient webClient;
+
+    @Autowired
+    private RouteRefresher routeRefresher;
+
+    @Value("${wiremock.server.port}")
+    private int wiremockPort;
+
+    @BeforeEach
+    public void clearRoutes() {
+        SyncopeSRATestCoreStartup.ROUTES.clear();
+    }
+
+    @Test
+    public void root() {
+        webClient.get().exchange().expectStatus().isNotFound();
+    }
+
+    @Test
+    public void getAddResponseHeader() {
+        // 1. no mapping for URL
+        webClient.get().uri("/getAddResponseHeader").exchange().expectStatus().isNotFound();
+
+        // 2. stub for proxied URL
+        stubFor(get(urlEqualTo("/getAddResponseHeader")).willReturn(aResponse()));
+
+        // 3. create route configuration
+        GatewayRouteTO routeTO = new GatewayRouteTO();
+        routeTO.setKey("getAddResponseHeader");
+        routeTO.setStatus(GatewayRouteStatus.PUBLISHED);
+        routeTO.setTarget(URI.create("http://localhost:" + wiremockPort));
+        routeTO.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.METHOD).args("GET").build());
+        routeTO.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.PATH).args("/getAddResponseHeader").cond(PredicateCond.AND).build());
+        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.ADD_RESPONSE_HEADER).args("Hello,World").build());
+
+        SyncopeSRATestCoreStartup.ROUTES.put(routeTO.getKey(), routeTO);
+
+        routeRefresher.refresh();
+
+        // 4. now mapping works for URL
+        webClient.get().uri("/getAddResponseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("Hello", "World");
+
+        // 5. update route configuration
+        routeTO.getFilters().clear();
+        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.ADD_RESPONSE_HEADER).args("Hello,WorldZ").build());
+
+        routeRefresher.refresh();
+
+        // 6. mapping for URL is updated too
+        webClient.get().uri("/getAddResponseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("Hello", "WorldZ");
+
+        // 7. update route configuration again
+        routeTO.getFilters().clear();
+
+        routeRefresher.refresh();
+
+        // 8. mapping for URL is updated again
+        webClient.get().uri("/getAddResponseHeader").exchange().
+                expectStatus().isOk().
+                expectHeader().doesNotExist("Hello");
+    }
+
+    @Test
+    public void hystrix() {
+        webClient.get().uri("/fallback").exchange().
+                expectStatus().isOk().
+                expectBody().
+                consumeWith(response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
+
+        stubFor(get(urlEqualTo("/delay/3")).
+                willReturn(aResponse().
+                        withBody("no fallback").
+                        withFixedDelay(3000)));
+
+        GatewayRouteTO routeTO = new GatewayRouteTO();
+        routeTO.setKey("hystrix");
+        routeTO.setStatus(GatewayRouteStatus.PUBLISHED);
+        routeTO.setTarget(URI.create("http://localhost:" + wiremockPort));
+        routeTO.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.HOST).args("*.hystrix.com").build());
+        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.HYSTRIX).args("fallbackcmd,forward:/fallback").build());
+
+        SyncopeSRATestCoreStartup.ROUTES.put(routeTO.getKey(), routeTO);
+
+        routeRefresher.refresh();
+
+        webClient.get().uri("/delay/3").
+                header(HttpHeaders.HOST, "www.hystrix.com").
+                exchange().
+                expectStatus().isOk().
+                expectBody().
+                consumeWith(response -> assertThat(response.getResponseBody()).isEqualTo("fallback".getBytes()));
+    }
+
+    @Test
+    public void custom() {
+        stubFor(post(urlEqualTo("/custom")).
+                willReturn(aResponse().
+                        withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).
+                        withBody("{\"data\": \"data\"}")));
+
+        GatewayRouteTO routeTO = new GatewayRouteTO();
+        routeTO.setKey("custom");
+        routeTO.setStatus(GatewayRouteStatus.PUBLISHED);
+        routeTO.setTarget(URI.create("http://localhost:" + wiremockPort));
+        routeTO.getPredicates().add(new GatewayRoutePredicate.Builder().
+                factory(PredicateFactory.CUSTOM).
+                args(BodyPropertyMatchingRoutePredicateFactory.class.getName() + ";cool").build());
+        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.ADD_RESPONSE_HEADER).args("Custom,matched").build());
+        routeTO.getFilters().add(new GatewayRouteFilter.Builder().
+                factory(FilterFactory.CUSTOM).
+                args(BodyPropertyAddingGatewayFilterFactory.class.getName() + ";customized=true").build());
+
+        SyncopeSRATestCoreStartup.ROUTES.put(routeTO.getKey(), routeTO);
+
+        routeRefresher.refresh();
+
+        webClient.post().uri("/custom").
+                body(BodyInserters.fromObject(MAPPER.createObjectNode().put("other", true))).
+                exchange().
+                expectStatus().isNotFound();
+
+        webClient.post().uri("/custom").
+                body(BodyInserters.fromObject(MAPPER.createObjectNode().put("cool", true))).
+                exchange().
+                expectStatus().isOk().
+                expectHeader().valueEquals("Custom", "matched").
+                expectBody().
+                consumeWith(response -> {
+                    try {
+                        JsonNode body = MAPPER.readTree(response.getResponseBody());
+                        assertTrue(body.has("customized"));
+                        assertTrue(body.get("customized").asBoolean());
+                    } catch (IOException e) {
+                        fail(e.getMessage(), e);
+                    }
+                });
+    }
+}
diff --git a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAShutdown.java b/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATestController.java
similarity index 69%
copy from sra/src/main/java/org/apache/syncope/sra/SyncopeSRAShutdown.java
copy to sra/src/test/java/org/apache/syncope/sra/SyncopeSRATestController.java
index d335836..c56490f 100644
--- a/sra/src/main/java/org/apache/syncope/sra/SyncopeSRAShutdown.java
+++ b/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATestController.java
@@ -18,14 +18,15 @@
  */
 package org.apache.syncope.sra;
 
-import org.springframework.context.ApplicationListener;
-import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Mono;
 
-public class SyncopeSRAShutdown extends SyncopeSRAStartStop
-        implements ApplicationListener<ContextClosedEvent> {
+@RestController
+public class SyncopeSRATestController {
 
-    @Override
-    public void onApplicationEvent(final ContextClosedEvent event) {
-        serviceOps.unregister(getNetworkService());
+    @RequestMapping("/fallback")
+    public Mono<String> fallback() {
+        return Mono.just("fallback");
     }
 }
diff --git a/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATestCoreStartup.java b/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATestCoreStartup.java
new file mode 100644
index 0000000..9a12b66
--- /dev/null
+++ b/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATestCoreStartup.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra;
+
+import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+import org.apache.cxf.jaxrs.lifecycle.SingletonResourceProvider;
+import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
+import org.apache.syncope.common.lib.to.GatewayRouteTO;
+import org.apache.syncope.common.rest.api.service.GatewayRouteService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.Ordered;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SyncopeSRATestCoreStartup extends SyncopeSRAStartStop
+        implements ApplicationListener<ContextRefreshedEvent>, Ordered {
+
+    public static final String ADDRESS = "http://localhost:9080/syncope/rest";
+
+    public static final Map<String, GatewayRouteTO> ROUTES = new ConcurrentHashMap<>();
+
+    @Autowired
+    private RouteRefresher routeRefresher;
+
+    @Override
+    public int getOrder() {
+        return Ordered.LOWEST_PRECEDENCE;
+    }
+
+    @Override
+    public void onApplicationEvent(final ContextRefreshedEvent event) {
+        // 1. start (mocked) Core as embedded CXF
+        JAXRSServerFactoryBean sf = new JAXRSServerFactoryBean();
+        sf.setAddress(ADDRESS);
+        sf.setResourceClasses(GatewayRouteService.class);
+        sf.setResourceProvider(
+                GatewayRouteService.class,
+                new SingletonResourceProvider(new StubGatewayRouteService(), true));
+        sf.setProviders(Collections.singletonList(new JacksonJsonProvider()));
+        sf.create();
+
+        // 2. register Core in Keymaster
+        NetworkService core = new NetworkService();
+        core.setType(NetworkService.Type.CORE);
+        core.setAddress(SyncopeSRATestCoreStartup.ADDRESS);
+        serviceOps.register(core);
+    }
+
+    public class StubGatewayRouteService implements GatewayRouteService {
+
+        @Override
+        public List<GatewayRouteTO> list() {
+            return ROUTES.values().stream().
+                    sorted(Comparator.comparing(GatewayRouteTO::getKey)).
+                    collect(Collectors.toList());
+        }
+
+        @Override
+        public Response create(final GatewayRouteTO routeTO) {
+            ROUTES.putIfAbsent(routeTO.getKey(), routeTO);
+            return Response.noContent().build();
+        }
+
+        @Override
+        public GatewayRouteTO read(final String key) {
+            GatewayRouteTO route = ROUTES.get(key);
+            if (route == null) {
+                throw new NotFoundException();
+            }
+            return route;
+        }
+
+        @Override
+        public void update(final GatewayRouteTO routeTO) {
+            read(routeTO.getKey());
+            ROUTES.put(routeTO.getKey(), routeTO);
+        }
+
+        @Override
+        public void delete(final String key) {
+            ROUTES.remove(key);
+        }
+
+        @Override
+        public void pushToSRA() {
+            routeRefresher.refresh();
+        }
+    }
+}
diff --git a/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATestKeymasterStartup.java b/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATestKeymasterStartup.java
new file mode 100644
index 0000000..c5649f6
--- /dev/null
+++ b/sra/src/test/java/org/apache/syncope/sra/SyncopeSRATestKeymasterStartup.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.sra;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.security.auth.login.AppConfigurationEntry;
+import javax.security.auth.login.Configuration;
+import org.apache.curator.test.InstanceSpec;
+import org.apache.curator.test.TestingServer;
+import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.Ordered;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SyncopeSRATestKeymasterStartup extends SyncopeSRAStartStop
+        implements ApplicationListener<ContextRefreshedEvent>, Ordered {
+
+    @Override
+    public int getOrder() {
+        return Ordered.HIGHEST_PRECEDENCE;
+    }
+
+    @Override
+    public void onApplicationEvent(final ContextRefreshedEvent event) {
+        // 1. start Zookeeper for Keymaster
+        AtomicReference<String> username = new AtomicReference<>();
+        AtomicReference<String> password = new AtomicReference<>();
+        try (InputStream propStream = getClass().getResourceAsStream("/keymaster.properties")) {
+            Properties props = new Properties();
+            props.load(propStream);
+
+            username.set(props.getProperty("keymaster.username"));
+            password.set(props.getProperty("keymaster.password"));
+        } catch (Exception e) {
+            throw new IllegalStateException("Could not load /keymaster.properties", e);
+        }
+
+        Configuration.setConfiguration(new Configuration() {
+
+            private final AppConfigurationEntry[] entries = {
+                new AppConfigurationEntry(
+                "org.apache.zookeeper.server.auth.DigestLoginModule",
+                AppConfigurationEntry.LoginModuleControlFlag.REQUIRED,
+                Map.of(
+                "user_" + username.get(), password.get()
+                ))
+            };
+
+            @Override
+            public AppConfigurationEntry[] getAppConfigurationEntry(final String name) {
+                return entries;
+            }
+        });
+
+        Map<String, Object> customProperties = new HashMap<>();
+        customProperties.put("authProvider.1", "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");
+        InstanceSpec spec = new InstanceSpec(null, 2181, -1, -1, true, 1, -1, -1, customProperties);
+        try {
+            new TestingServer(spec, true).start();
+        } catch (Exception e) {
+            throw new IllegalStateException("Could not start Zookeeper", e);
+        }
+
+        // 2. register Core in Keymaster
+        NetworkService core = new NetworkService();
+        core.setType(NetworkService.Type.CORE);
+        core.setAddress(SyncopeSRATestCoreStartup.ADDRESS);
+        serviceOps.register(core);
+    }
+}
diff --git a/sra/src/main/resources/keymaster.properties b/sra/src/test/resources/keymaster.properties
similarity index 93%
rename from sra/src/main/resources/keymaster.properties
rename to sra/src/test/resources/keymaster.properties
index 033fe3b..f374d8c 100644
--- a/sra/src/main/resources/keymaster.properties
+++ b/sra/src/test/resources/keymaster.properties
@@ -14,6 +14,6 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
-keymaster.address=http://localhost:9080/syncope/rest/keymaster
+keymaster.address=127.0.0.1:2181
 keymaster.username=${anonymousUser}
 keymaster.password=${anonymousKey}