You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by GitBox <gi...@apache.org> on 2018/04/23 18:26:23 UTC

[GitHub] Izakey closed pull request #5: Document the Identity API

Izakey closed pull request #5: Document the Identity API
URL: https://github.com/apache/fineract-cn-identity/pull/5
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/service/build.gradle b/service/build.gradle
index 554f5db..f46bbaa 100644
--- a/service/build.gradle
+++ b/service/build.gradle
@@ -26,6 +26,7 @@ buildscript {
 
     dependencies {
         classpath ("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
+        classpath("org.asciidoctor:asciidoctor-gradle-plugin:1.5.3")
     }
 }
 
@@ -36,6 +37,7 @@ plugins {
 
 apply from: '../shared.gradle'
 
+apply plugin: 'org.asciidoctor.convert'
 apply plugin: 'spring-boot'
 
 springBoot {
@@ -59,10 +61,26 @@ dependencies {
             [group: 'org.apache.fineract.cn.anubis', name: 'api', version: versions.frameworkanubis],
             [group: 'org.apache.fineract.cn.anubis', name: 'library', version: versions.frameworkanubis],
     )
+    testCompile(
+            [group: 'org.apache.fineract.cn.identity', name: 'api', version: project.version],
+            [group: 'org.apache.fineract.cn', name: 'api', version: versions.frameworkapi],
+            [group: 'org.apache.fineract.cn', name: 'test', version: versions.frameworktest],
+            [group: 'org.apache.fineract.cn.anubis', name: 'test', version: versions.frameworkanubis],
+            [group: 'org.springframework.restdocs', name: 'spring-restdocs-mockmvc'],
+            [group: 'org.springframework.boot', name: 'spring-boot-starter-test'],
+            [group: 'junit', name: 'junit', version: '4.12']
+    )
 }
 
-publishToMavenLocal.dependsOn bootRepackage
+asciidoctor {
+    sourceDir 'src/doc/asciidoc/'
+    outputDir 'src/doc/html5'
+    options backend: "html", doctype: "book"
+    attributes "source-highlighter": "highlightjs", \
+                'snippets': file('src/doc/generated-snippets/')
+}
 
+publishToMavenLocal.dependsOn bootRepackage
 
 publishing {
     publications {
diff --git a/service/src/doc/asciidoc/api-docs.adoc b/service/src/doc/asciidoc/api-docs.adoc
new file mode 100644
index 0000000..a518e03
--- /dev/null
+++ b/service/src/doc/asciidoc/api-docs.adoc
@@ -0,0 +1,75 @@
+== Apache Fineract CN Identity Management API Documentation ==
+
+== Users ==
+
+==== Add User ====
+
+.curl-request
+include::{snippets}/test-users/test-add-login/curl-request.adoc[]
+
+.http-request
+include::{snippets}/test-users/test-add-login/http-request.adoc[]
+
+.http-response
+include::{snippets}/test-users/test-add-login/http-response.adoc[]
+
+.httpie-request
+include::{snippets}/test-users/test-add-login/httpie-request.adoc[]
+
+==== Change User's Role ====
+
+.curl-request
+include::{snippets}/test-users/test-change-user-role/curl-request.adoc[]
+
+.http-request
+include::{snippets}/test-users/test-change-user-role/http-request.adoc[]
+
+.http-response
+include::{snippets}/test-users/test-change-user-role/http-response.adoc[]
+
+.httpie-request
+include::{snippets}/test-users/test-change-user-role/httpie-request.adoc[]
+
+== Roles ==
+
+==== Create A Role ====
+
+.curl-request
+include::{snippets}/test-roles/test-create-role/curl-request.adoc[]
+
+.http-request
+include::{snippets}/test-roles/test-create-role/http-request.adoc[]
+
+.http-response
+include::{snippets}/test-roles/test-create-role/http-response.adoc[]
+
+.httpie-request
+include::{snippets}/test-roles/test-create-role/httpie-request.adoc[]
+
+==== Update A Role ====
+
+.curl-request
+include::{snippets}/test-roles/change-role/curl-request.adoc[]
+
+.http-request
+include::{snippets}/test-roles/change-role/http-request.adoc[]
+
+.http-response
+include::{snippets}/test-roles/change-role/http-response.adoc[]
+
+.httpie-request
+include::{snippets}/test-roles/change-role/httpie-request.adoc[]
+
+==== Delete A Role ====
+
+.curl-request
+include::{snippets}/test-roles/delete-role/curl-request.adoc[]
+
+.http-request
+include::{snippets}/test-roles/delete-role/http-request.adoc[]
+
+.http-response
+include::{snippets}/test-roles/delete-role/http-response.adoc[]
+
+.httpie-request
+include::{snippets}/test-roles/delete-role/httpie-request.adoc[]
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/change-role/curl-request.adoc b/service/src/doc/generated-snippets/test-roles/change-role/curl-request.adoc
new file mode 100644
index 0000000..50b3791
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/change-role/curl-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ curl 'http://localhost:8080/identity/v1/roles/scribe1' -i -X PUT -H 'Content-Type: application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/change-role/http-request.adoc b/service/src/doc/generated-snippets/test-roles/change-role/http-request.adoc
new file mode 100644
index 0000000..4eaa90c
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/change-role/http-request.adoc
@@ -0,0 +1,7 @@
+[source,http,options="nowrap"]
+----
+PUT /identity/v1/roles/scribe1 HTTP/1.1
+Content-Type: application/json
+Host: localhost:8080
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/change-role/http-response.adoc b/service/src/doc/generated-snippets/test-roles/change-role/http-response.adoc
new file mode 100644
index 0000000..f3b256d
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/change-role/http-response.adoc
@@ -0,0 +1,5 @@
+[source,http,options="nowrap"]
+----
+HTTP/1.1 404 Not Found
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/change-role/httpie-request.adoc b/service/src/doc/generated-snippets/test-roles/change-role/httpie-request.adoc
new file mode 100644
index 0000000..74e038b
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/change-role/httpie-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ http PUT 'http://localhost:8080/identity/v1/roles/scribe1' 'Content-Type:application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/delete-role/curl-request.adoc b/service/src/doc/generated-snippets/test-roles/delete-role/curl-request.adoc
new file mode 100644
index 0000000..ddc356e
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/delete-role/curl-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ curl 'http://localhost:8080/identity/v1/roles/scribe10' -i -X DELETE -H 'Accept: */*' -H 'Content-Type: application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/delete-role/http-request.adoc b/service/src/doc/generated-snippets/test-roles/delete-role/http-request.adoc
new file mode 100644
index 0000000..58d0a00
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/delete-role/http-request.adoc
@@ -0,0 +1,8 @@
+[source,http,options="nowrap"]
+----
+DELETE /identity/v1/roles/scribe10 HTTP/1.1
+Accept: */*
+Content-Type: application/json
+Host: localhost:8080
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/delete-role/http-response.adoc b/service/src/doc/generated-snippets/test-roles/delete-role/http-response.adoc
new file mode 100644
index 0000000..f3b256d
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/delete-role/http-response.adoc
@@ -0,0 +1,5 @@
+[source,http,options="nowrap"]
+----
+HTTP/1.1 404 Not Found
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/delete-role/httpie-request.adoc b/service/src/doc/generated-snippets/test-roles/delete-role/httpie-request.adoc
new file mode 100644
index 0000000..1b92f64
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/delete-role/httpie-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ http DELETE 'http://localhost:8080/identity/v1/roles/scribe10' 'Accept:*/*' 'Content-Type:application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/test-create-role/curl-request.adoc b/service/src/doc/generated-snippets/test-roles/test-create-role/curl-request.adoc
new file mode 100644
index 0000000..b51a09f
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/test-create-role/curl-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ curl 'http://localhost:8080/identity/v1/roles' -i -X POST -H 'Accept: application/json' -H 'Content-Type: application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/test-create-role/http-request.adoc b/service/src/doc/generated-snippets/test-roles/test-create-role/http-request.adoc
new file mode 100644
index 0000000..fb0bf86
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/test-create-role/http-request.adoc
@@ -0,0 +1,8 @@
+[source,http,options="nowrap"]
+----
+POST /identity/v1/roles HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Host: localhost:8080
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/test-create-role/http-response.adoc b/service/src/doc/generated-snippets/test-roles/test-create-role/http-response.adoc
new file mode 100644
index 0000000..f3b256d
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/test-create-role/http-response.adoc
@@ -0,0 +1,5 @@
+[source,http,options="nowrap"]
+----
+HTTP/1.1 404 Not Found
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-roles/test-create-role/httpie-request.adoc b/service/src/doc/generated-snippets/test-roles/test-create-role/httpie-request.adoc
new file mode 100644
index 0000000..8166cd3
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-roles/test-create-role/httpie-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ http POST 'http://localhost:8080/identity/v1/roles' 'Accept:application/json' 'Content-Type:application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-users/test-add-login/curl-request.adoc b/service/src/doc/generated-snippets/test-users/test-add-login/curl-request.adoc
new file mode 100644
index 0000000..f5ec058
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-users/test-add-login/curl-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ curl 'http://localhost:8080/identity/v1/users/' -i -X POST -H 'Accept: application/json' -H 'Content-Type: application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-users/test-add-login/http-request.adoc b/service/src/doc/generated-snippets/test-users/test-add-login/http-request.adoc
new file mode 100644
index 0000000..5ce1a64
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-users/test-add-login/http-request.adoc
@@ -0,0 +1,8 @@
+[source,http,options="nowrap"]
+----
+POST /identity/v1/users/ HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Host: localhost:8080
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-users/test-add-login/http-response.adoc b/service/src/doc/generated-snippets/test-users/test-add-login/http-response.adoc
new file mode 100644
index 0000000..f3b256d
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-users/test-add-login/http-response.adoc
@@ -0,0 +1,5 @@
+[source,http,options="nowrap"]
+----
+HTTP/1.1 404 Not Found
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-users/test-add-login/httpie-request.adoc b/service/src/doc/generated-snippets/test-users/test-add-login/httpie-request.adoc
new file mode 100644
index 0000000..8f8365e
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-users/test-add-login/httpie-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ http POST 'http://localhost:8080/identity/v1/users/' 'Accept:application/json' 'Content-Type:application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-users/test-change-user-role/curl-request.adoc b/service/src/doc/generated-snippets/test-users/test-change-user-role/curl-request.adoc
new file mode 100644
index 0000000..be9f158
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-users/test-change-user-role/curl-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ curl 'http://localhost:8080/identity/v1/users/Ahmes1' -i -X PUT -H 'Content-Type: application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-users/test-change-user-role/http-request.adoc b/service/src/doc/generated-snippets/test-users/test-change-user-role/http-request.adoc
new file mode 100644
index 0000000..6b9e6fe
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-users/test-change-user-role/http-request.adoc
@@ -0,0 +1,7 @@
+[source,http,options="nowrap"]
+----
+PUT /identity/v1/users/Ahmes1 HTTP/1.1
+Content-Type: application/json
+Host: localhost:8080
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-users/test-change-user-role/http-response.adoc b/service/src/doc/generated-snippets/test-users/test-change-user-role/http-response.adoc
new file mode 100644
index 0000000..f3b256d
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-users/test-change-user-role/http-response.adoc
@@ -0,0 +1,5 @@
+[source,http,options="nowrap"]
+----
+HTTP/1.1 404 Not Found
+
+----
\ No newline at end of file
diff --git a/service/src/doc/generated-snippets/test-users/test-change-user-role/httpie-request.adoc b/service/src/doc/generated-snippets/test-users/test-change-user-role/httpie-request.adoc
new file mode 100644
index 0000000..c3d8a65
--- /dev/null
+++ b/service/src/doc/generated-snippets/test-users/test-change-user-role/httpie-request.adoc
@@ -0,0 +1,4 @@
+[source,bash]
+----
+$ http PUT 'http://localhost:8080/identity/v1/users/Ahmes1' 'Content-Type:application/json'
+----
\ No newline at end of file
diff --git a/service/src/doc/html5/html5/api-docs.html b/service/src/doc/html5/html5/api-docs.html
new file mode 100644
index 0000000..b435ea6
--- /dev/null
+++ b/service/src/doc/html5/html5/api-docs.html
@@ -0,0 +1,598 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<!--[if IE]><meta http-equiv="X-UA-Compatible" content="IE=edge"><![endif]-->
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="generator" content="Asciidoctor 1.5.3">
+<title>Apache Fineract CN Identity Management API Documentation</title>
+<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700">
+<style>
+/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */
+/* Remove comment around @import statement below when using as a custom stylesheet */
+/*@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400,700";*/
+article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}
+audio,canvas,video{display:inline-block}
+audio:not([controls]){display:none;height:0}
+[hidden],template{display:none}
+script{display:none!important}
+html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
+body{margin:0}
+a{background:transparent}
+a:focus{outline:thin dotted}
+a:active,a:hover{outline:0}
+h1{font-size:2em;margin:.67em 0}
+abbr[title]{border-bottom:1px dotted}
+b,strong{font-weight:bold}
+dfn{font-style:italic}
+hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}
+mark{background:#ff0;color:#000}
+code,kbd,pre,samp{font-family:monospace;font-size:1em}
+pre{white-space:pre-wrap}
+q{quotes:"\201C" "\201D" "\2018" "\2019"}
+small{font-size:80%}
+sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
+sup{top:-.5em}
+sub{bottom:-.25em}
+img{border:0}
+svg:not(:root){overflow:hidden}
+figure{margin:0}
+fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
+legend{border:0;padding:0}
+button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
+button,input{line-height:normal}
+button,select{text-transform:none}
+button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}
+button[disabled],html input[disabled]{cursor:default}
+input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}
+input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}
+input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}
+button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
+textarea{overflow:auto;vertical-align:top}
+table{border-collapse:collapse;border-spacing:0}
+*,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}
+html,body{font-size:100%}
+body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto}
+a:hover{cursor:pointer}
+img,object,embed{max-width:100%;height:auto}
+object,embed{height:100%}
+img{-ms-interpolation-mode:bicubic}
+.left{float:left!important}
+.right{float:right!important}
+.text-left{text-align:left!important}
+.text-right{text-align:right!important}
+.text-center{text-align:center!important}
+.text-justify{text-align:justify!important}
+.hide{display:none}
+body{-webkit-font-smoothing:antialiased}
+img,object,svg{display:inline-block;vertical-align:middle}
+textarea{height:auto;min-height:50px}
+select{width:100%}
+.center{margin-left:auto;margin-right:auto}
+.spread{width:100%}
+p.lead,.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{font-size:1.21875em;line-height:1.6}
+.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
+div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr}
+a{color:#2156a5;text-decoration:underline;line-height:inherit}
+a:hover,a:focus{color:#1d4b8f}
+a img{border:none}
+p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
+p aside{font-size:.875em;line-height:1.35;font-style:italic}
+h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
+h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
+h1{font-size:2.125em}
+h2{font-size:1.6875em}
+h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
+h4,h5{font-size:1.125em}
+h6{font-size:1em}
+hr{border:solid #ddddd8;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0}
+em,i{font-style:italic;line-height:inherit}
+strong,b{font-weight:bold;line-height:inherit}
+small{font-size:60%;line-height:inherit}
+code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9)}
+ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
+ul,ol,ul.no-bullet,ol.no-bullet{margin-left:1.5em}
+ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em}
+ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit}
+ul.square{list-style-type:square}
+ul.circle{list-style-type:circle}
+ul.disc{list-style-type:disc}
+ul.no-bullet{list-style:none}
+ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
+dl dt{margin-bottom:.3125em;font-weight:bold}
+dl dd{margin-bottom:1.25em}
+abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help}
+abbr{text-transform:none}
+blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
+blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)}
+blockquote cite:before{content:"\2014 \0020"}
+blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)}
+blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
+@media only screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
+h1{font-size:2.75em}
+h2{font-size:2.3125em}
+h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
+h4{font-size:1.4375em}}
+table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede}
+table thead,table tfoot{background:#f7f8f7;font-weight:bold}
+table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
+table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
+table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7}
+table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6}
+body{tab-size:4}
+h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
+h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
+.clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:" ";display:table}
+.clearfix:after,.float-group:after{clear:both}
+*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;padding:.1em .5ex;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed}
+pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed}
+.keyseq{color:rgba(51,51,51,.8)}
+kbd{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;display:inline-block;color:rgba(0,0,0,.8);font-size:.65em;line-height:1.45;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:0 .15em;padding:.2em .5em;vertical-align:middle;position:relative;top:-.1em;white-space:nowrap}
+.keyseq kbd:first-child{margin-left:0}
+.keyseq kbd:last-child{margin-right:0}
+.menuseq,.menu{color:rgba(0,0,0,.8)}
+b.button:before,b.button:after{position:relative;top:-1px;font-weight:400}
+b.button:before{content:"[";padding:0 3px 0 2px}
+b.button:after{content:"]";padding:0 2px 0 3px}
+p a>code:hover{color:rgba(0,0,0,.9)}
+#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
+#header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:" ";display:table}
+#header:after,#content:after,#footnotes:after,#footer:after{clear:both}
+#content{margin-top:1.25em}
+#content:before{content:none}
+#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
+#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #ddddd8}
+#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px}
+#header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap}
+#header .details span:first-child{margin-left:-.125em}
+#header .details span.email a{color:rgba(0,0,0,.85)}
+#header .details br{display:none}
+#header .details br+span:before{content:"\00a0\2013\00a0"}
+#header .details br+span.author:before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
+#header .details br+span#revremark:before{content:"\00a0|\00a0"}
+#header #revnumber{text-transform:capitalize}
+#header #revnumber:after{content:"\00a0"}
+#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
+#toc{border-bottom:1px solid #efefed;padding-bottom:.5em}
+#toc>ul{margin-left:.125em}
+#toc ul.sectlevel0>li>a{font-style:italic}
+#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
+#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
+#toc li{line-height:1.3334;margin-top:.3334em}
+#toc a{text-decoration:none}
+#toc a:active{text-decoration:underline}
+#toctitle{color:#7a2518;font-size:1.2em}
+@media only screen and (min-width:768px){#toctitle{font-size:1.375em}
+body.toc2{padding-left:15em;padding-right:0}
+#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #efefed;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
+#toc.toc2 #toctitle{margin-top:0;margin-bottom:.8rem;font-size:1.2em}
+#toc.toc2>ul{font-size:.9em;margin-bottom:0}
+#toc.toc2 ul ul{margin-left:0;padding-left:1em}
+#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
+body.toc2.toc-right{padding-left:0;padding-right:15em}
+body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #efefed;left:auto;right:0}}
+@media only screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
+#toc.toc2{width:20em}
+#toc.toc2 #toctitle{font-size:1.375em}
+#toc.toc2>ul{font-size:.95em}
+#toc.toc2 ul ul{padding-left:1.25em}
+body.toc2.toc-right{padding-left:0;padding-right:20em}}
+#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
+#content #toc>:first-child{margin-top:0}
+#content #toc>:last-child{margin-bottom:0}
+#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em}
+#footer-text{color:rgba(255,255,255,.8);line-height:1.44}
+.sect1{padding-bottom:.625em}
+@media only screen and (min-width:768px){.sect1{padding-bottom:1.25em}}
+.sect1+.sect1{border-top:1px solid #efefed}
+#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
+#content h1>a.anchor:before,h2>a.anchor:before,h3>a.anchor:before,#toctitle>a.anchor:before,.sidebarblock>.content>.title>a.anchor:before,h4>a.anchor:before,h5>a.anchor:before,h6>a.anchor:before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
+#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
+#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
+#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
+.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
+.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
+table.tableblock>caption.title{white-space:nowrap;overflow:visible;max-width:0}
+.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{color:rgba(0,0,0,.85)}
+table.tableblock #preamble>.sectionbody>.paragraph:first-of-type p{font-size:inherit}
+.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
+.admonitionblock>table td.icon{text-align:center;width:80px}
+.admonitionblock>table td.icon img{max-width:none}
+.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
+.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)}
+.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
+.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px}
+.exampleblock>.content>:first-child{margin-top:0}
+.exampleblock>.content>:last-child{margin-bottom:0}
+.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
+.sidebarblock>:first-child{margin-top:0}
+.sidebarblock>:last-child{margin-bottom:0}
+.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
+.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
+.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8}
+.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1}
+.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;padding:1em;font-size:.8125em}
+.literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto;white-space:pre;word-wrap:normal}
+@media only screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}
+@media only screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}
+.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)}
+.listingblock pre.highlightjs{padding:0}
+.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px}
+.listingblock pre.prettyprint{border-width:0}
+.listingblock>.content{position:relative}
+.listingblock code[data-lang]:before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999}
+.listingblock:hover code[data-lang]:before{display:block}
+.listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999}
+.listingblock.terminal pre .command:not([data-prompt]):before{content:"$"}
+table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none}
+table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0;line-height:1.45}
+table.pyhltable td.code{padding-left:.75em;padding-right:0}
+pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8}
+pre.pygments .lineno{display:inline-block;margin-right:.25em}
+table.pyhltable .linenodiv{background:none!important;padding-right:0!important}
+.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
+.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em}
+.quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
+.quoteblock blockquote{margin:0;padding:0;border:0}
+.quoteblock blockquote:before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
+.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
+.quoteblock .attribution{margin-top:.5em;margin-right:.5ex;text-align:right}
+.quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)}
+.quoteblock .quoteblock blockquote{padding:0 0 0 .75em}
+.quoteblock .quoteblock blockquote:before{display:none}
+.verseblock{margin:0 1em 1.25em 1em}
+.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
+.verseblock pre strong{font-weight:400}
+.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
+.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
+.quoteblock .attribution br,.verseblock .attribution br{display:none}
+.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.025em;color:rgba(0,0,0,.6)}
+.quoteblock.abstract{margin:0 0 1.25em 0;display:block}
+.quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0}
+.quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none}
+table.tableblock{max-width:100%;border-collapse:separate}
+table.tableblock td>.paragraph:last-child p>p:last-child,table.tableblock th>p:last-child,table.tableblock td>p:last-child{margin-bottom:0}
+table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
+table.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0}
+table.grid-all tfoot>tr>th.tableblock,table.grid-all tfoot>tr>td.tableblock{border-width:1px 1px 0 0}
+table.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0}
+table.grid-all *>tr>.tableblock:last-child,table.grid-cols *>tr>.tableblock:last-child{border-right-width:0}
+table.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px 0}
+table.grid-all tbody>tr:last-child>th.tableblock,table.grid-all tbody>tr:last-child>td.tableblock,table.grid-all thead:last-child>tr>th.tableblock,table.grid-rows tbody>tr:last-child>th.tableblock,table.grid-rows tbody>tr:last-child>td.tableblock,table.grid-rows thead:last-child>tr>th.tableblock{border-bottom-width:0}
+table.grid-rows tfoot>tr>th.tableblock,table.grid-rows tfoot>tr>td.tableblock{border-width:1px 0 0 0}
+table.frame-all{border-width:1px}
+table.frame-sides{border-width:0 1px}
+table.frame-topbot{border-width:1px 0}
+th.halign-left,td.halign-left{text-align:left}
+th.halign-right,td.halign-right{text-align:right}
+th.halign-center,td.halign-center{text-align:center}
+th.valign-top,td.valign-top{vertical-align:top}
+th.valign-bottom,td.valign-bottom{vertical-align:bottom}
+th.valign-middle,td.valign-middle{vertical-align:middle}
+table thead th,table tfoot th{font-weight:bold}
+tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7}
+tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
+p.tableblock>code:only-child{background:none;padding:0}
+p.tableblock{font-size:1em}
+td>div.verse{white-space:pre}
+ol{margin-left:1.75em}
+ul li ol{margin-left:1.5em}
+dl dd{margin-left:1.125em}
+dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
+ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
+ul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none}
+ul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em}
+ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1em;font-size:.85em}
+ul.checklist li>p:first-child>input[type="checkbox"]:first-child{width:1em;position:relative;top:1px}
+ul.inline{margin:0 auto .625em auto;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden}
+ul.inline>li{list-style:none;float:left;margin-left:1.375em;display:block}
+ul.inline>li>*{display:block}
+.unstyled dl dt{font-weight:400;font-style:normal}
+ol.arabic{list-style-type:decimal}
+ol.decimal{list-style-type:decimal-leading-zero}
+ol.loweralpha{list-style-type:lower-alpha}
+ol.upperalpha{list-style-type:upper-alpha}
+ol.lowerroman{list-style-type:lower-roman}
+ol.upperroman{list-style-type:upper-roman}
+ol.lowergreek{list-style-type:lower-greek}
+.hdlist>table,.colist>table{border:0;background:none}
+.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
+td.hdlist1,td.hdlist2{vertical-align:top;padding:0 .625em}
+td.hdlist1{font-weight:bold;padding-bottom:1.25em}
+.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
+.colist>table tr>td:first-of-type{padding:0 .75em;line-height:1}
+.colist>table tr>td:last-of-type{padding:.25em 0}
+.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd}
+.imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0}
+.imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em}
+.imageblock>.title{margin-bottom:0}
+.imageblock.thumb,.imageblock.th{border-width:6px}
+.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
+.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
+.image.left{margin-right:.625em}
+.image.right{margin-left:.625em}
+a.image{text-decoration:none;display:inline-block}
+a.image object{pointer-events:none}
+sup.footnote,sup.footnoteref{font-size:.875em;position:static;vertical-align:super}
+sup.footnote a,sup.footnoteref a{text-decoration:none}
+sup.footnote a:active,sup.footnoteref a:active{text-decoration:underline}
+#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
+#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em 0;border-width:1px 0 0 0}
+#footnotes .footnote{padding:0 .375em 0 .225em;line-height:1.3334;font-size:.875em;margin-left:1.2em;text-indent:-1.05em;margin-bottom:.2em}
+#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none}
+#footnotes .footnote:last-of-type{margin-bottom:0}
+#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
+.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0}
+.gist .file-data>table td.line-data{width:99%}
+div.unbreakable{page-break-inside:avoid}
+.big{font-size:larger}
+.small{font-size:smaller}
+.underline{text-decoration:underline}
+.overline{text-decoration:overline}
+.line-through{text-decoration:line-through}
+.aqua{color:#00bfbf}
+.aqua-background{background-color:#00fafa}
+.black{color:#000}
+.black-background{background-color:#000}
+.blue{color:#0000bf}
+.blue-background{background-color:#0000fa}
+.fuchsia{color:#bf00bf}
+.fuchsia-background{background-color:#fa00fa}
+.gray{color:#606060}
+.gray-background{background-color:#7d7d7d}
+.green{color:#006000}
+.green-background{background-color:#007d00}
+.lime{color:#00bf00}
+.lime-background{background-color:#00fa00}
+.maroon{color:#600000}
+.maroon-background{background-color:#7d0000}
+.navy{color:#000060}
+.navy-background{background-color:#00007d}
+.olive{color:#606000}
+.olive-background{background-color:#7d7d00}
+.purple{color:#600060}
+.purple-background{background-color:#7d007d}
+.red{color:#bf0000}
+.red-background{background-color:#fa0000}
+.silver{color:#909090}
+.silver-background{background-color:#bcbcbc}
+.teal{color:#006060}
+.teal-background{background-color:#007d7d}
+.white{color:#bfbfbf}
+.white-background{background-color:#fafafa}
+.yellow{color:#bfbf00}
+.yellow-background{background-color:#fafa00}
+span.icon>.fa{cursor:default}
+.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
+.admonitionblock td.icon .icon-note:before{content:"\f05a";color:#19407c}
+.admonitionblock td.icon .icon-tip:before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
+.admonitionblock td.icon .icon-warning:before{content:"\f071";color:#bf6900}
+.admonitionblock td.icon .icon-caution:before{content:"\f06d";color:#bf3400}
+.admonitionblock td.icon .icon-important:before{content:"\f06a";color:#bf0000}
+.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
+.conum[data-value] *{color:#fff!important}
+.conum[data-value]+b{display:none}
+.conum[data-value]:after{content:attr(data-value)}
+pre .conum[data-value]{position:relative;top:-.125em}
+b.conum *{color:inherit!important}
+.conum:not([data-value]):empty{display:none}
+dt,th.tableblock,td.content,div.footnote{text-rendering:optimizeLegibility}
+h1,h2,p,td.content,span.alt{letter-spacing:-.01em}
+p strong,td.content strong,div.footnote strong{letter-spacing:-.005em}
+p,blockquote,dt,td.content,span.alt{font-size:1.0625rem}
+p{margin-bottom:1.25rem}
+.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
+.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc}
+.print-only{display:none!important}
+@media print{@page{margin:1.25cm .75cm}
+*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}
+a{color:inherit!important;text-decoration:underline!important}
+a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
+a[href^="http:"]:not(.bare):after,a[href^="https:"]:not(.bare):after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
+abbr[title]:after{content:" (" attr(title) ")"}
+pre,blockquote,tr,img,object,svg{page-break-inside:avoid}
+thead{display:table-header-group}
+svg{max-width:100%}
+p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
+h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
+#toc,.sidebarblock,.exampleblock>.content{background:none!important}
+#toc{border-bottom:1px solid #ddddd8!important;padding-bottom:0!important}
+.sect1{padding-bottom:0!important}
+.sect1+.sect1{border:0!important}
+#header>h1:first-child{margin-top:1.25rem}
+body.book #header{text-align:center}
+body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em 0}
+body.book #header .details{border:0!important;display:block;padding:0!important}
+body.book #header .details span:first-child{margin-left:0!important}
+body.book #header .details br{display:block}
+body.book #header .details br+span:before{content:none!important}
+body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
+body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
+.listingblock code[data-lang]:before{display:block}
+#footer{background:none!important;padding:0 .9375em}
+#footer-text{color:rgba(0,0,0,.6)!important;font-size:.9em}
+.hide-on-print{display:none!important}
+.print-only{display:block!important}
+.hide-for-print{display:none!important}
+.show-for-print{display:inherit!important}}
+</style>
+</head>
+<body class="book">
+<div id="header">
+</div>
+<div id="content">
+<div class="sect1">
+<h2 id="_apache_fineract_cn_identity_management_api_documentation">Apache Fineract CN Identity Management API Documentation</h2>
+<div class="sectionbody">
+
+</div>
+</div>
+<div class="sect1">
+<h2 id="_users">Users</h2>
+<div class="sectionbody">
+<div class="sect3">
+<h4 id="_add_user">Add User</h4>
+<div class="listingblock">
+<div class="title">curl-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/identity/v1/users/' -i -X POST -H 'Accept: application/json' -H 'Content-Type: application/json'</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-request</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">POST /identity/v1/users/ HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Host: localhost:8080</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-response</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">HTTP/1.1 404 Not Found</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">httpie-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ http POST 'http://localhost:8080/identity/v1/users/' 'Accept:application/json' 'Content-Type:application/json'</code></pre>
+</div>
+</div>
+</div>
+<div class="sect3">
+<h4 id="_change_user_s_role">Change User&#8217;s Role</h4>
+<div class="listingblock">
+<div class="title">curl-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/identity/v1/users/Ahmes1' -i -X PUT -H 'Content-Type: application/json'</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-request</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">PUT /identity/v1/users/Ahmes1 HTTP/1.1
+Content-Type: application/json
+Host: localhost:8080</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-response</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">HTTP/1.1 404 Not Found</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">httpie-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ http PUT 'http://localhost:8080/identity/v1/users/Ahmes1' 'Content-Type:application/json'</code></pre>
+</div>
+</div>
+</div>
+</div>
+</div>
+<div class="sect1">
+<h2 id="_roles">Roles</h2>
+<div class="sectionbody">
+<div class="sect3">
+<h4 id="_create_a_role">Create A Role</h4>
+<div class="listingblock">
+<div class="title">curl-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/identity/v1/roles' -i -X POST -H 'Accept: application/json' -H 'Content-Type: application/json'</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-request</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">POST /identity/v1/roles HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Host: localhost:8080</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-response</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">HTTP/1.1 404 Not Found</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">httpie-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ http POST 'http://localhost:8080/identity/v1/roles' 'Accept:application/json' 'Content-Type:application/json'</code></pre>
+</div>
+</div>
+</div>
+<div class="sect3">
+<h4 id="_update_a_role">Update A Role</h4>
+<div class="listingblock">
+<div class="title">curl-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/identity/v1/roles/scribe1' -i -X PUT -H 'Content-Type: application/json'</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-request</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">PUT /identity/v1/roles/scribe1 HTTP/1.1
+Content-Type: application/json
+Host: localhost:8080</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-response</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">HTTP/1.1 404 Not Found</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">httpie-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ http PUT 'http://localhost:8080/identity/v1/roles/scribe1' 'Content-Type:application/json'</code></pre>
+</div>
+</div>
+</div>
+<div class="sect3">
+<h4 id="_delete_a_role">Delete A Role</h4>
+<div class="listingblock">
+<div class="title">curl-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ curl 'http://localhost:8080/identity/v1/roles/scribe1' -i -X DELETE -H 'Accept: */*' -H 'Content-Type: application/json'</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-request</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">DELETE /identity/v1/roles/scribe1 HTTP/1.1
+Accept: */*
+Content-Type: application/json
+Host: localhost:8080</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">http-response</div>
+<div class="content">
+<pre class="highlightjs highlight nowrap"><code class="language-http" data-lang="http">HTTP/1.1 404 Not Found</code></pre>
+</div>
+</div>
+<div class="listingblock">
+<div class="title">httpie-request</div>
+<div class="content">
+<pre class="highlightjs highlight"><code class="language-bash" data-lang="bash">$ http DELETE 'http://localhost:8080/identity/v1/roles/scribe1' 'Accept:*/*' 'Content-Type:application/json'</code></pre>
+</div>
+</div>
+</div>
+</div>
+</div>
+</div>
+<div id="footer">
+<div id="footer-text">
+Last updated 2018-04-23 11:38:21 +01:00
+</div>
+</div>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/styles/github.min.css">
+<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.9.1/highlight.min.js"></script>
+<script>hljs.initHighlighting()</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/service/src/test/java/AbstractComponentTest.java b/service/src/test/java/AbstractComponentTest.java
new file mode 100644
index 0000000..7f7b984
--- /dev/null
+++ b/service/src/test/java/AbstractComponentTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.fineract.cn.anubis.api.v1.domain.AllowedOperation;
+import org.apache.fineract.cn.anubis.api.v1.domain.Signature;
+import org.apache.fineract.cn.anubis.test.v1.TenantApplicationSecurityEnvironmentTestRule;
+import org.apache.fineract.cn.api.config.EnableApiFactory;
+import org.apache.fineract.cn.api.context.AutoGuest;
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.api.util.ApiFactory;
+import org.apache.fineract.cn.api.util.UserContextHolder;
+import org.apache.fineract.cn.identity.api.v1.PermittableGroupIds;
+import org.apache.fineract.cn.identity.api.v1.client.IdentityManager;
+import org.apache.fineract.cn.identity.api.v1.domain.*;
+import org.apache.fineract.cn.identity.api.v1.events.ApplicationPermissionEvent;
+import org.apache.fineract.cn.identity.api.v1.events.ApplicationSignatureEvent;
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.apache.fineract.cn.identity.config.IdentityServiceConfig;
+import org.apache.fineract.cn.lang.security.RsaKeyPairFactory;
+import org.apache.fineract.cn.test.env.TestEnvironment;
+import org.apache.fineract.cn.test.fixture.TenantDataStoreContextTestRule;
+import org.apache.fineract.cn.test.listener.EnableEventRecording;
+import org.apache.fineract.cn.test.listener.EventRecorder;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.annotation.PostConstruct;
+import java.util.Arrays;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("SpringAutowiredFieldsWarningInspection")
+@RunWith(SpringRunner.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
+        classes = {AbstractComponentTest.TestConfiguration.class})
+@TestPropertySource(properties = {"cassandra.cl.read = LOCAL_QUORUM", "cassandra.cl.write = LOCAL_QUORUM", "cassandra.cl.delete = LOCAL_QUORUM", "identity.token.refresh.secureCookie = false", "identity.passwordExpiresInDays = 93"})
+public class AbstractComponentTest extends SuiteTestEnvironment {
+  @Configuration
+  @EnableApiFactory
+  @EnableEventRecording
+  @Import({IdentityServiceConfig.class})
+  @ComponentScan("listener")
+  public static class TestConfiguration {
+    public TestConfiguration() {
+      super();
+    }
+  }
+
+
+  static final String ADMIN_PASSWORD = "golden_osiris";
+  static final String ADMIN_ROLE = "pharaoh";
+  static final String ADMIN_IDENTIFIER = "antony";
+  static final String AHMES_PASSWORD = "fractions";
+  static final String AHMES_FRIENDS_PASSWORD = "sekhem";
+
+  @ClassRule
+  public final static TenantDataStoreContextTestRule tenantDataStoreContext = TenantDataStoreContextTestRule.forRandomTenantName(cassandraInitializer);
+
+  //Not using this as a rule because initialize in identityManager is different.
+  static final TenantApplicationSecurityEnvironmentTestRule tenantApplicationSecurityEnvironment = new TenantApplicationSecurityEnvironmentTestRule(testEnvironment);
+
+  @Autowired
+  ApiFactory apiFactory;
+
+  @SuppressWarnings("SpringJavaAutowiringInspection")
+  @Autowired
+  EventRecorder eventRecorder;
+
+
+  private IdentityManager identityManager;
+
+  @PostConstruct
+  public void provision() throws Exception {
+    identityManager =  apiFactory.create(IdentityManager.class, testEnvironment.serverURI());
+
+    try (final AutoUserContext ignored
+             = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      identityManager.initialize(TestEnvironment.encodePassword(ADMIN_PASSWORD));
+    }
+  }
+
+  @After
+  public void after() {
+    UserContextHolder.clear();
+    eventRecorder.clear();
+  }
+
+  IdentityManager getTestSubject()
+  {
+    return identityManager;
+  }
+
+  AutoUserContext loginAdmin() throws InterruptedException {
+    final Authentication adminAuthentication =
+            getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+    Assert.assertNotNull(adminAuthentication);
+
+    {
+      final boolean found = eventRecorder
+              .wait(EventConstants.OPERATION_AUTHENTICATE, ADMIN_IDENTIFIER);
+      Assert.assertTrue(found);
+    }
+
+    return new AutoUserContext(ADMIN_IDENTIFIER, adminAuthentication.getAccessToken());
+  }
+
+  /**
+   * In identityManager, the user is created with an expired password.  The user must change the password him- or herself
+   * to access any other endpoint.
+   */
+  String createUserWithNonexpiredPassword(final String password, final String role) throws InterruptedException {
+    final String username = testEnvironment.generateUniqueIdentifer("Ahmes");
+    try (final AutoUserContext ignore = loginAdmin()) {
+      getTestSubject().createUser(new UserWithPassword(username, role, TestEnvironment.encodePassword(password)));
+
+      {
+        final boolean found = eventRecorder.wait(EventConstants.OPERATION_POST_USER, username);
+        Assert.assertTrue(found);
+      }
+
+      final Authentication passwordOnlyAuthentication = getTestSubject().login(username, TestEnvironment.encodePassword(password));
+
+      try (final AutoUserContext ignore2 = new AutoUserContext(username, passwordOnlyAuthentication.getAccessToken()))
+      {
+        getTestSubject().changeUserPassword(username, new Password(TestEnvironment.encodePassword(password)));
+        final boolean found = eventRecorder.wait(EventConstants.OPERATION_PUT_USER_PASSWORD, username);
+        Assert.assertTrue(found);
+      }
+    }
+    return username;
+  }
+
+  String generateRoleIdentifier() {
+    return testEnvironment.generateUniqueIdentifer("scribe");
+  }
+
+  Role buildRole(final String identifier, final Permission... permission) {
+    final Role scribe = new Role();
+    scribe.setIdentifier(identifier);
+    scribe.setPermissions(Arrays.asList(permission));
+    return scribe;
+  }
+
+  Permission buildRolePermission() {
+    final Permission permission = new Permission();
+    permission.setAllowedOperations(AllowedOperation.ALL);
+    permission.setPermittableEndpointGroupIdentifier(PermittableGroupIds.ROLE_MANAGEMENT);
+    return permission;
+  }
+
+  Permission buildUserPermission() {
+    final Permission permission = new Permission();
+    permission.setAllowedOperations(AllowedOperation.ALL);
+    permission.setPermittableEndpointGroupIdentifier(PermittableGroupIds.IDENTITY_MANAGEMENT);
+    return permission;
+  }
+
+  Permission buildSelfPermission() {
+    final Permission permission = new Permission();
+    permission.setAllowedOperations(AllowedOperation.ALL);
+    permission.setPermittableEndpointGroupIdentifier(PermittableGroupIds.SELF_MANAGEMENT);
+    return permission;
+  }
+
+  Permission buildApplicationSelfPermission() {
+    final Permission permission = new Permission();
+    permission.setAllowedOperations(AllowedOperation.ALL);
+    permission.setPermittableEndpointGroupIdentifier(PermittableGroupIds.APPLICATION_SELF_MANAGEMENT);
+    return permission;
+  }
+
+  String createRoleManagementRole() throws InterruptedException {
+    return createRole(buildRolePermission());
+  }
+
+  String createSelfManagementRole() throws InterruptedException {
+    return createRole(buildSelfPermission());
+  }
+
+  String createApplicationSelfManagementRole() throws InterruptedException {
+    return createRole(buildApplicationSelfPermission());
+  }
+
+  String createRole(final Permission... permission) throws InterruptedException {
+    final String roleIdentifier = generateRoleIdentifier();
+    final Role role = buildRole(roleIdentifier, permission);
+
+    getTestSubject().createRole(role);
+
+    eventRecorder.wait(EventConstants.OPERATION_POST_ROLE, roleIdentifier);
+
+    return roleIdentifier;
+  }
+
+  AutoUserContext loginUser(final String userId, final String password) {
+    final Authentication authentication;
+    try (AutoUserContext ignored = new AutoGuest()) {
+      authentication = getTestSubject().login(userId, TestEnvironment.encodePassword(password));
+    }
+    return new AutoUserContext(userId, authentication.getAccessToken());
+  }
+
+  private String createTestApplicationName()
+  {
+    return "test" + RandomStringUtils.randomNumeric(3) + "-v1";
+  }
+
+  static class ApplicationSignatureTestData {
+    private final String applicationIdentifier;
+    private final RsaKeyPairFactory.KeyPairHolder keyPair;
+
+    ApplicationSignatureTestData(final String applicationIdentifier, final RsaKeyPairFactory.KeyPairHolder keyPair) {
+      this.applicationIdentifier = applicationIdentifier;
+      this.keyPair = keyPair;
+    }
+
+    String getApplicationIdentifier() {
+      return applicationIdentifier;
+    }
+
+    RsaKeyPairFactory.KeyPairHolder getKeyPair() {
+      return keyPair;
+    }
+
+    String getKeyTimestamp() {
+      return keyPair.getTimestamp();
+    }
+  }
+
+  ApplicationSignatureTestData setApplicationSignature() throws InterruptedException {
+    final String testApplicationName = createTestApplicationName();
+    final RsaKeyPairFactory.KeyPairHolder keyPair = RsaKeyPairFactory.createKeyPair();
+    final Signature signature = new Signature(keyPair.getPublicKeyMod(), keyPair.getPublicKeyExp());
+
+    getTestSubject().setApplicationSignature(testApplicationName, keyPair.getTimestamp(), signature);
+
+    Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_PUT_APPLICATION_SIGNATURE, new ApplicationSignatureEvent(testApplicationName, keyPair.getTimestamp())));
+    return new ApplicationSignatureTestData(testApplicationName, keyPair);
+  }
+
+  void createApplicationPermission(final String applicationIdentifier, final Permission permission) throws InterruptedException {
+    getTestSubject().createApplicationPermission(applicationIdentifier, permission);
+    Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_APPLICATION_PERMISSION,
+            new ApplicationPermissionEvent(applicationIdentifier,
+                    permission.getPermittableEndpointGroupIdentifier())));
+  }
+}
diff --git a/service/src/test/java/Helpers.java b/service/src/test/java/Helpers.java
new file mode 100644
index 0000000..95057c1
--- /dev/null
+++ b/service/src/test/java/Helpers.java
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * @author Myrle Krantz
+ */
+class Helpers {
+
+  static <T> boolean instancePresent(final List<T> users, Function<T, String> getIdentifier,
+      final String identifier) {
+    return users.stream().map(getIdentifier).filter(i -> i.equals(identifier)).findAny().isPresent();
+  }
+}
diff --git a/service/src/test/java/SuiteTestEnvironment.java b/service/src/test/java/SuiteTestEnvironment.java
new file mode 100644
index 0000000..a1abcab
--- /dev/null
+++ b/service/src/test/java/SuiteTestEnvironment.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+import org.apache.fineract.cn.test.env.TestEnvironment;
+import org.apache.fineract.cn.test.fixture.cassandra.CassandraInitializer;
+import org.junit.ClassRule;
+import org.junit.rules.RuleChain;
+import org.junit.rules.RunExternalResourceOnce;
+import org.junit.rules.TestRule;
+
+/**
+ * @author Myrle Krantz
+ */
+public class SuiteTestEnvironment {
+  static final String APP_NAME = "identity-v1";
+
+  final static TestEnvironment testEnvironment = new TestEnvironment(APP_NAME);
+  final static CassandraInitializer cassandraInitializer = new CassandraInitializer();
+
+  @ClassRule
+  public static TestRule orderClassRules = RuleChain
+      .outerRule(new RunExternalResourceOnce(testEnvironment))
+      .around(new RunExternalResourceOnce(cassandraInitializer));
+}
diff --git a/service/src/test/java/TestApplications.java b/service/src/test/java/TestApplications.java
new file mode 100644
index 0000000..a37a40e
--- /dev/null
+++ b/service/src/test/java/TestApplications.java
@@ -0,0 +1,389 @@
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.fineract.cn.anubis.api.v1.domain.AllowedOperation;
+import org.apache.fineract.cn.anubis.token.TenantRefreshTokenSerializer;
+import org.apache.fineract.cn.anubis.token.TokenSerializationResult;
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.api.util.NotFoundException;
+import org.apache.fineract.cn.identity.api.v1.PermittableGroupIds;
+import org.apache.fineract.cn.identity.api.v1.domain.Authentication;
+import org.apache.fineract.cn.identity.api.v1.domain.CallEndpointSet;
+import org.apache.fineract.cn.identity.api.v1.domain.Permission;
+import org.apache.fineract.cn.identity.api.v1.domain.User;
+import org.apache.fineract.cn.identity.api.v1.events.ApplicationCallEndpointSetEvent;
+import org.apache.fineract.cn.identity.api.v1.events.ApplicationPermissionEvent;
+import org.apache.fineract.cn.identity.api.v1.events.ApplicationPermissionUserEvent;
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestApplications extends AbstractComponentTest {
+
+  private static final String CALL_ENDPOINT_SET_IDENTIFIER = "doughboy";
+
+  @Test
+  public void testSetApplicationSignature() throws InterruptedException {
+    try (final AutoUserContext ignored
+                 = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      final ApplicationSignatureTestData appPlusSig = setApplicationSignature();
+
+      final List<String> foundApplications = getTestSubject().getApplications();
+      Assert.assertTrue(foundApplications.contains(appPlusSig.getApplicationIdentifier()));
+
+      getTestSubject().getApplicationSignature(
+              appPlusSig.getApplicationIdentifier(),
+              appPlusSig.getKeyTimestamp());
+    }
+  }
+
+  @Test
+  public void testCreateAndDeleteApplicationPermission() throws InterruptedException {
+    try (final AutoUserContext ignored
+                 = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      final ApplicationSignatureTestData appPlusSig = setApplicationSignature();
+
+      final Permission identityManagementPermission = new Permission();
+      identityManagementPermission.setPermittableEndpointGroupIdentifier(PermittableGroupIds.IDENTITY_MANAGEMENT);
+      identityManagementPermission.setAllowedOperations(Collections.singleton(AllowedOperation.READ));
+
+      createApplicationPermission(appPlusSig.getApplicationIdentifier(), identityManagementPermission);
+
+      {
+        final List<Permission> applicationPermissions = getTestSubject().getApplicationPermissions(appPlusSig.getApplicationIdentifier());
+        Assert.assertTrue(applicationPermissions.contains(identityManagementPermission));
+
+        final Permission applicationPermission = getTestSubject().getApplicationPermission(appPlusSig.getApplicationIdentifier(), PermittableGroupIds.IDENTITY_MANAGEMENT);
+        Assert.assertEquals(identityManagementPermission, applicationPermission);
+      }
+
+      final Permission roleManagementPermission = new Permission();
+      roleManagementPermission.setPermittableEndpointGroupIdentifier(PermittableGroupIds.ROLE_MANAGEMENT);
+      roleManagementPermission.setAllowedOperations(Collections.singleton(AllowedOperation.READ));
+
+      createApplicationPermission(appPlusSig.getApplicationIdentifier(), roleManagementPermission);
+
+      {
+        final List<Permission> applicationPermissions = getTestSubject().getApplicationPermissions(appPlusSig.getApplicationIdentifier());
+        Assert.assertTrue(applicationPermissions.contains(identityManagementPermission));
+        Assert.assertTrue(applicationPermissions.contains(roleManagementPermission));
+      }
+
+      getTestSubject().deleteApplicationPermission(appPlusSig.getApplicationIdentifier(), identityManagementPermission.getPermittableEndpointGroupIdentifier());
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_DELETE_APPLICATION_PERMISSION,
+              new ApplicationPermissionEvent(appPlusSig.getApplicationIdentifier(), PermittableGroupIds.IDENTITY_MANAGEMENT)));
+
+      {
+        final List<Permission> applicationPermissions = getTestSubject().getApplicationPermissions(appPlusSig.getApplicationIdentifier());
+        Assert.assertFalse(applicationPermissions.contains(identityManagementPermission));
+        Assert.assertTrue(applicationPermissions.contains(roleManagementPermission));
+      }
+    }
+  }
+
+  @Test
+  public void testDeleteApplication() throws InterruptedException {
+    try (final AutoUserContext ignored
+                 = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      final ApplicationSignatureTestData appPlusSig = setApplicationSignature();
+
+      getTestSubject().deleteApplication(appPlusSig.getApplicationIdentifier());
+
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_DELETE_APPLICATION, appPlusSig.getApplicationIdentifier()));
+
+      final List<String> foundApplications = getTestSubject().getApplications();
+      Assert.assertFalse(foundApplications.contains(appPlusSig.getApplicationIdentifier()));
+
+      try {
+        getTestSubject().getApplicationSignature(
+                appPlusSig.getApplicationIdentifier(),
+                appPlusSig.getKeyTimestamp());
+        Assert.fail("Shouldn't find app sig after app was deleted.");
+      }
+      catch (final NotFoundException ignored2) {
+
+      }
+    }
+  }
+
+  @Test
+  public void testApplicationPermissionUserApprovalProvisioning() throws InterruptedException {
+    final ApplicationSignatureTestData appPlusSig;
+    final Permission identityManagementPermission;
+    try (final AutoUserContext ignored
+                 = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      appPlusSig = setApplicationSignature();
+
+      identityManagementPermission = new Permission(
+              PermittableGroupIds.ROLE_MANAGEMENT,
+              Collections.singleton(AllowedOperation.READ));
+
+      createApplicationPermission(appPlusSig.getApplicationIdentifier(), identityManagementPermission);
+    }
+
+    final String user1Password;
+    final String user1id;
+    final String user2Password;
+    final String user2id;
+    try (final AutoUserContext ignored = loginAdmin()) {
+      final String selfManagementRoleId = createSelfManagementRole();
+      final String roleManagementRoleId = createRoleManagementRole();
+
+      user1Password = RandomStringUtils.randomAlphanumeric(5);
+      user1id = createUserWithNonexpiredPassword(user1Password, selfManagementRoleId);
+
+      user2Password = RandomStringUtils.randomAlphanumeric(5);
+      user2id = createUserWithNonexpiredPassword(user2Password, roleManagementRoleId);
+    }
+
+    try (final AutoUserContext ignored = loginUser(user1id, user1Password)) {
+      Assert.assertFalse(getTestSubject().getApplicationPermissionEnabledForUser(
+              appPlusSig.getApplicationIdentifier(),
+              identityManagementPermission.getPermittableEndpointGroupIdentifier(),
+              user1id));
+
+      getTestSubject().setApplicationPermissionEnabledForUser(
+              appPlusSig.getApplicationIdentifier(),
+              identityManagementPermission.getPermittableEndpointGroupIdentifier(),
+              user1id,
+              true);
+
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_PUT_APPLICATION_PERMISSION_USER_ENABLED,
+              new ApplicationPermissionUserEvent(
+                      appPlusSig.getApplicationIdentifier(),
+                      identityManagementPermission.getPermittableEndpointGroupIdentifier(),
+                      user1id)));
+
+      Assert.assertTrue(getTestSubject().getApplicationPermissionEnabledForUser(
+              appPlusSig.getApplicationIdentifier(),
+              identityManagementPermission.getPermittableEndpointGroupIdentifier(),
+              user1id));
+    }
+
+    try (final AutoUserContext ignored = loginUser(user2id, user2Password)) {
+      Assert.assertFalse(getTestSubject().getApplicationPermissionEnabledForUser(
+              appPlusSig.getApplicationIdentifier(),
+              identityManagementPermission.getPermittableEndpointGroupIdentifier(),
+              user2id));
+    }
+
+    try (final AutoUserContext ignored = loginUser(user1id, user1Password)) {
+      getTestSubject().setApplicationPermissionEnabledForUser(
+              appPlusSig.getApplicationIdentifier(),
+              identityManagementPermission.getPermittableEndpointGroupIdentifier(),
+              user1id,
+              false);
+
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_PUT_APPLICATION_PERMISSION_USER_ENABLED,
+              new ApplicationPermissionUserEvent(
+                      appPlusSig.getApplicationIdentifier(),
+                      identityManagementPermission.getPermittableEndpointGroupIdentifier(),
+                      user1id)));
+    }
+
+    //Note that at this point, our imaginary application still cannot do anything in the name of any user,
+    //because neither of the users has the permission the user enabled for the application.
+  }
+
+  @Test
+  public void manageApplicationEndpointSet() throws InterruptedException {
+    try (final AutoUserContext ignored
+                 = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      final ApplicationSignatureTestData appPlusSig = setApplicationSignature();
+
+      final String endpointSetIdentifier = testEnvironment.generateUniqueIdentifer("epset");
+      final CallEndpointSet endpointSet = new CallEndpointSet();
+      endpointSet.setIdentifier(endpointSetIdentifier);
+      endpointSet.setPermittableEndpointGroupIdentifiers(Collections.emptyList());
+
+      getTestSubject().createApplicationCallEndpointSet(appPlusSig.getApplicationIdentifier(), endpointSet);
+
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_APPLICATION_CALLENDPOINTSET,
+              new ApplicationCallEndpointSetEvent(appPlusSig.getApplicationIdentifier(), endpointSetIdentifier)));
+
+      final List<CallEndpointSet> applicationEndpointSets = getTestSubject().getApplicationCallEndpointSets(appPlusSig.getApplicationIdentifier());
+      Assert.assertTrue(applicationEndpointSets.contains(endpointSet));
+
+      final CallEndpointSet storedEndpointSet = getTestSubject().getApplicationCallEndpointSet(
+              appPlusSig.getApplicationIdentifier(),
+              endpointSetIdentifier);
+      Assert.assertEquals(endpointSet, storedEndpointSet);
+
+      endpointSet.setPermittableEndpointGroupIdentifiers(Collections.singletonList(PermittableGroupIds.ROLE_MANAGEMENT));
+      getTestSubject().changeApplicationCallEndpointSet(
+              appPlusSig.getApplicationIdentifier(),
+              endpointSetIdentifier,
+              endpointSet);
+
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_PUT_APPLICATION_CALLENDPOINTSET,
+              new ApplicationCallEndpointSetEvent(appPlusSig.getApplicationIdentifier(), endpointSetIdentifier)));
+
+      final CallEndpointSet storedEndpointSet2 = getTestSubject().getApplicationCallEndpointSet(
+              appPlusSig.getApplicationIdentifier(),
+              endpointSetIdentifier);
+      Assert.assertEquals(endpointSet, storedEndpointSet2);
+
+      final List<CallEndpointSet> applicationEndpointSets2 = getTestSubject().getApplicationCallEndpointSets(appPlusSig.getApplicationIdentifier());
+      Assert.assertTrue(applicationEndpointSets2.size() == 1);
+
+      getTestSubject().deleteApplicationCallEndpointSet(appPlusSig.getApplicationIdentifier(), endpointSetIdentifier);
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_DELETE_APPLICATION_CALLENDPOINTSET,
+              new ApplicationCallEndpointSetEvent(appPlusSig.getApplicationIdentifier(), endpointSetIdentifier)));
+
+      final List<CallEndpointSet> applicationEndpointSets3 = getTestSubject().getApplicationCallEndpointSets(appPlusSig.getApplicationIdentifier());
+      Assert.assertTrue(applicationEndpointSets3.isEmpty());
+    }
+  }
+
+  @Test
+  public void applicationIssuedRefreshTokenHappyCase() throws InterruptedException {
+    final ApplicationSignatureTestData appPlusSig;
+    final Permission rolePermission = buildRolePermission();
+    final Permission userPermission = buildUserPermission();
+    try (final AutoUserContext ignored
+                 = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      appPlusSig = setApplicationSignature();
+
+      createApplicationPermission(appPlusSig.getApplicationIdentifier(), rolePermission);
+      createApplicationPermission(appPlusSig.getApplicationIdentifier(), userPermission);
+
+      getTestSubject().createApplicationCallEndpointSet(
+              appPlusSig.getApplicationIdentifier(),
+              new CallEndpointSet(CALL_ENDPOINT_SET_IDENTIFIER,
+                      Arrays.asList(rolePermission.getPermittableEndpointGroupIdentifier(),
+                              userPermission.getPermittableEndpointGroupIdentifier())));
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_APPLICATION_CALLENDPOINTSET,
+              new ApplicationCallEndpointSetEvent(appPlusSig.getApplicationIdentifier(),
+                      CALL_ENDPOINT_SET_IDENTIFIER)));
+    }
+
+    final String userid;
+    final String userPassword;
+    try (final AutoUserContext ignored = loginAdmin()) {
+      final String selfManagementRoleId = createRole(rolePermission, userPermission);
+
+      userPassword = RandomStringUtils.randomAlphanumeric(5);
+      userid = createUserWithNonexpiredPassword(userPassword, selfManagementRoleId);
+    }
+
+
+    try (final AutoUserContext ignored = loginUser(userid, userPassword)) {
+      getTestSubject().setApplicationPermissionEnabledForUser(
+              appPlusSig.getApplicationIdentifier(),
+              userPermission.getPermittableEndpointGroupIdentifier(),
+              userid,
+              true);
+      getTestSubject().setApplicationPermissionEnabledForUser(
+              appPlusSig.getApplicationIdentifier(),
+              rolePermission.getPermittableEndpointGroupIdentifier(),
+              userid,
+              true);
+    }
+
+    final TokenSerializationResult tokenSerializationResult =
+            new TenantRefreshTokenSerializer().build(new TenantRefreshTokenSerializer.Specification()
+                    .setUser(userid)
+                    .setEndpointSet(CALL_ENDPOINT_SET_IDENTIFIER)
+                    .setSecondsToLive(30)
+                    .setKeyTimestamp(appPlusSig.getKeyTimestamp())
+                    .setPrivateKey(appPlusSig.getKeyPair().privateKey())
+                    .setSourceApplication(appPlusSig.getApplicationIdentifier()));
+
+
+    final Authentication applicationAuthentication = getTestSubject().refresh(tokenSerializationResult.getToken());
+
+    try (final AutoUserContext ignored = new AutoUserContext(userid, applicationAuthentication.getAccessToken())) {
+      final List<User> users = getTestSubject().getUsers();
+      Assert.assertFalse(users.isEmpty());
+    }
+  }
+
+  @Test
+  public void applicationIssuedRefreshTokenToCreatePermissionRequest() throws InterruptedException {
+    final ApplicationSignatureTestData appPlusSig;
+    try (final AutoUserContext ignored
+                 = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      appPlusSig = setApplicationSignature();
+      createApplicationPermission(appPlusSig.getApplicationIdentifier(), buildApplicationSelfPermission());
+    }
+
+    final String userid;
+    final String userid2;
+    final String userPassword;
+    try (final AutoUserContext ignored = loginAdmin()) {
+
+      final String roleId = createApplicationSelfManagementRole();
+
+      userPassword = RandomStringUtils.randomAlphanumeric(5);
+      userid = createUserWithNonexpiredPassword(userPassword, roleId);
+      userid2 = createUserWithNonexpiredPassword(userPassword, roleId);
+
+    }
+
+    try (final AutoUserContext ignored = loginUser(userid, userPassword)) {
+      getTestSubject().setApplicationPermissionEnabledForUser(appPlusSig.getApplicationIdentifier(), PermittableGroupIds.APPLICATION_SELF_MANAGEMENT, userid, true);
+    }
+
+
+    final TokenSerializationResult tokenSerializationResult =
+            new TenantRefreshTokenSerializer().build(new TenantRefreshTokenSerializer.Specification()
+                    .setUser(userid)
+                    .setSecondsToLive(30)
+                    .setKeyTimestamp(appPlusSig.getKeyTimestamp())
+                    .setPrivateKey(appPlusSig.getKeyPair().privateKey())
+                    .setSourceApplication(appPlusSig.getApplicationIdentifier()));
+
+
+    final Authentication applicationAuthentication = getTestSubject().refresh(tokenSerializationResult.getToken());
+
+    try (final AutoUserContext ignored = new AutoUserContext(userid, applicationAuthentication.getAccessToken())) {
+      final Permission rolePermission = buildRolePermission();
+      createApplicationPermission(appPlusSig.getApplicationIdentifier(), rolePermission);
+
+      final List<Permission> appPermissions = getTestSubject().getApplicationPermissions(
+              appPlusSig.getApplicationIdentifier());
+
+      Assert.assertTrue(appPermissions.contains(rolePermission));
+
+      try {
+        getTestSubject().setApplicationPermissionEnabledForUser(appPlusSig.getApplicationIdentifier(), rolePermission.getPermittableEndpointGroupIdentifier(), userid2, true);
+        Assert.fail("This call to create enable permission for another user should've failed.");
+      }
+      catch (final NotFoundException ignored2) {
+
+      }
+
+      try {
+        createApplicationPermission("madeupname-v1", rolePermission);
+        Assert.fail("This call to create application permission should've failed.");
+      }
+      catch (final NotFoundException ignored2) {
+
+      }
+    }
+  }
+}
diff --git a/service/src/test/java/TestAuthentication.java b/service/src/test/java/TestAuthentication.java
new file mode 100644
index 0000000..3e62958
--- /dev/null
+++ b/service/src/test/java/TestAuthentication.java
@@ -0,0 +1,191 @@
+/*
+ * 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.
+ */
+
+import com.google.common.collect.Sets;
+import org.apache.fineract.cn.anubis.api.v1.client.Anubis;
+import org.apache.fineract.cn.anubis.api.v1.domain.*;
+import org.apache.fineract.cn.anubis.test.v1.SystemSecurityEnvironment;
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.api.util.InvalidTokenException;
+import org.apache.fineract.cn.api.util.NotFoundException;
+import org.apache.fineract.cn.identity.api.v1.domain.*;
+import org.apache.fineract.cn.lang.AutoTenantContext;
+import org.apache.fineract.cn.lang.security.RsaPublicKeyBuilder;
+import org.apache.fineract.cn.test.env.TestEnvironment;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.security.PublicKey;
+import java.util.*;
+
+import static org.apache.fineract.cn.identity.api.v1.events.EventConstants.OPERATION_POST_PERMITTABLE_GROUP;
+import static org.apache.fineract.cn.identity.api.v1.events.EventConstants.OPERATION_POST_ROLE;
+import static org.apache.fineract.cn.identity.api.v1.events.EventConstants.OPERATION_POST_USER;
+import static org.apache.fineract.cn.identity.api.v1.events.EventConstants.OPERATION_PUT_USER_PASSWORD;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestAuthentication extends AbstractComponentTest {
+  @Test
+  //@Repeat(25)
+  public void testAdminLogin() throws InterruptedException {
+    //noinspection EmptyTryBlock
+    try (final AutoUserContext ignore = loginAdmin()) {
+    }
+  }
+
+  @Test
+  public void testAdminLogout() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      getTestSubject().logout();
+
+      try {
+        getTestSubject().refresh();
+        Assert.fail("Refresh should fail after logout has occurred.");
+      }
+      catch (final InvalidTokenException ignored)
+      {
+        //Expected.
+      }
+    }
+  }
+
+  @Test(expected = NotFoundException.class)
+  public void testAdminIncorrectLogin() throws InterruptedException {
+    getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword("set"));
+    Assert.fail("login with wrong password should fail with not found exception.");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testAdminMissingTenantHeader() throws InterruptedException {
+    try (final AutoUserContext ignored = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      try (final AutoTenantContext ignored2 = new AutoTenantContext())
+      {
+        getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+      }
+    }
+    Assert.fail("login without tenant header set should fail with bad request.");
+  }
+
+  @Test()
+  public void testPermissionsCorrectInAdminToken() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final Authentication adminAuthentication =
+              getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+      Assert.assertNotNull(adminAuthentication);
+
+      final TokenContent tokenContent = SystemSecurityEnvironment.getTokenContent(adminAuthentication.getAccessToken(), getPublicKey());
+      final Set<TokenPermission> tokenPermissions = new HashSet<>(tokenContent.getTokenPermissions());
+
+      final Set<TokenPermission> expectedTokenPermissions = new HashSet<>();
+      Collections.addAll(expectedTokenPermissions,
+          new TokenPermission("identity-v1/permittablegroups/*", Sets.newHashSet(AllowedOperation.CHANGE, AllowedOperation.DELETE, AllowedOperation.READ)),
+          new TokenPermission("identity-v1/roles/*", Sets.newHashSet(AllowedOperation.CHANGE, AllowedOperation.DELETE, AllowedOperation.READ)),
+          new TokenPermission("identity-v1/users/*", Sets.newHashSet(AllowedOperation.CHANGE, AllowedOperation.DELETE, AllowedOperation.READ)));
+      //This is not a complete list.  This is a spot check.
+
+      Assert.assertTrue("Expected: " + expectedTokenPermissions + "\nActual: " + tokenPermissions,
+              tokenPermissions.containsAll(expectedTokenPermissions));
+    }
+  }
+
+  @Test()
+  public void testPermissionsCorrectInTokenWhenMultiplePermittableGroupsInRole() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final PermittableEndpoint horusEndpoint = buildPermittableEndpoint("horus");
+      final PermittableGroup horusGroup = buildPermittableGroup("horus_Group", horusEndpoint);
+      getTestSubject().createPermittableGroup(horusGroup);
+
+      final PermittableEndpoint maatEndpoint = buildPermittableEndpoint("maat");
+      final PermittableGroup maatGroup = buildPermittableGroup("maat_Group", maatEndpoint);
+      getTestSubject().createPermittableGroup(maatGroup);
+
+      Assert.assertTrue(eventRecorder.wait(OPERATION_POST_PERMITTABLE_GROUP, horusGroup.getIdentifier()));
+      Assert.assertTrue(eventRecorder.wait(OPERATION_POST_PERMITTABLE_GROUP, maatGroup.getIdentifier()));
+
+      final Permission horusGroupPermission = new Permission(horusGroup.getIdentifier(), Collections.singleton(AllowedOperation.READ));
+      final Permission maatGroupPermission = new Permission(maatGroup.getIdentifier(), AllowedOperation.ALL);
+      final Role compositeRole = new Role("composite_role", Arrays.asList(horusGroupPermission, maatGroupPermission));
+      getTestSubject().createRole(compositeRole);
+
+      Assert.assertTrue(eventRecorder.wait(OPERATION_POST_ROLE, compositeRole.getIdentifier()));
+
+      final UserWithPassword user = new UserWithPassword("user_with_composite_role", compositeRole.getIdentifier(), "asdfasdfasdf");
+      getTestSubject().createUser(user);
+
+      Assert.assertTrue(eventRecorder.wait(OPERATION_POST_USER, user.getIdentifier()));
+
+      final Authentication passwordChangeOnlyAuthentication = getTestSubject().login(user.getIdentifier(), user.getPassword());
+      try (final AutoUserContext ignore2 = new AutoUserContext(user.getIdentifier(), passwordChangeOnlyAuthentication.getAccessToken()))
+      {
+        getTestSubject().changeUserPassword(user.getIdentifier(), new Password(user.getPassword()));
+
+        Assert.assertTrue(eventRecorder.wait(OPERATION_PUT_USER_PASSWORD, user.getIdentifier()));
+      }
+
+      final Authentication authentication = getTestSubject().login(user.getIdentifier(), user.getPassword());
+      final TokenContent tokenContent = SystemSecurityEnvironment
+          .getTokenContent(authentication.getAccessToken(), getPublicKey());
+      final Set<TokenPermission> tokenPermissions = new HashSet<>(tokenContent.getTokenPermissions());
+
+      final Set<TokenPermission> expectedTokenPermissions= new HashSet<>();
+      Collections.addAll(expectedTokenPermissions,
+              new TokenPermission(horusEndpoint.getPath(), Collections.singleton(AllowedOperation.READ)),
+              new TokenPermission(maatEndpoint.getPath(), Collections.singleton(AllowedOperation.READ)),
+              new TokenPermission("identity-v1/users/{useridentifier}/password",
+                  Sets.newHashSet(AllowedOperation.READ, AllowedOperation.CHANGE, AllowedOperation.DELETE)),
+              new TokenPermission("identity-v1/users/{useridentifier}/permissions", Sets.newHashSet(AllowedOperation.READ)),
+              new TokenPermission("identity-v1/token/_current", Collections.singleton(AllowedOperation.DELETE)));
+
+      Assert.assertTrue("Expected: " + expectedTokenPermissions + "\nActual: " + tokenPermissions,
+              tokenPermissions.containsAll(expectedTokenPermissions));
+    }
+  }
+
+  private PermittableGroup buildPermittableGroup(final String identifier, final PermittableEndpoint... permittableEndpoint) {
+    final PermittableGroup ret = new PermittableGroup();
+    ret.setIdentifier(identifier);
+    ret.setPermittables(Arrays.asList(permittableEndpoint));
+    return ret;
+  }
+
+  private PermittableEndpoint buildPermittableEndpoint(final String group) {
+    final PermittableEndpoint ret = new PermittableEndpoint();
+    ret.setPath(group + "/v1/x/y/z");
+    ret.setMethod("GET");
+    ret.setGroupId(group);
+    return ret;
+  }
+
+  public PublicKey getPublicKey() {
+    try (final AutoUserContext ignored = tenantApplicationSecurityEnvironment.createAutoSeshatContext())
+    {
+      final Anubis anubis = tenantApplicationSecurityEnvironment.getAnubis();
+      final List<String> signatureKeyTimestamps = anubis.getAllSignatureSets();
+      Assert.assertTrue(!signatureKeyTimestamps.isEmpty());
+      final Signature sig = anubis.getApplicationSignature(signatureKeyTimestamps.get(0));
+
+      return new RsaPublicKeyBuilder()
+              .setPublicKeyMod(sig.getPublicKeyMod())
+              .setPublicKeyExp(sig.getPublicKeyExp())
+              .build();
+    }
+  }
+}
diff --git a/service/src/test/java/TestKeyRotation.java b/service/src/test/java/TestKeyRotation.java
new file mode 100644
index 0000000..dc69a0c
--- /dev/null
+++ b/service/src/test/java/TestKeyRotation.java
@@ -0,0 +1,133 @@
+/*
+ * 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.
+ */
+
+import org.apache.fineract.cn.anubis.api.v1.client.Anubis;
+import org.apache.fineract.cn.anubis.api.v1.domain.ApplicationSignatureSet;
+import org.apache.fineract.cn.anubis.api.v1.domain.Signature;
+import org.apache.fineract.cn.api.context.AutoGuest;
+import org.apache.fineract.cn.api.context.AutoSeshat;
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.api.util.NotFoundException;
+import org.apache.fineract.cn.identity.api.v1.domain.Authentication;
+import org.apache.fineract.cn.test.env.TestEnvironment;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestKeyRotation extends AbstractComponentTest {
+  @Test
+  public void testKeyRotation() throws InterruptedException {
+    final Anubis anubis = tenantApplicationSecurityEnvironment.getAnubis();
+
+    //noinspection EmptyTryBlock
+    try (final AutoUserContext ignored = loginAdmin())
+    {
+      //Don't do anything yet.
+    }
+
+    final String systemToken = tenantApplicationSecurityEnvironment.getSystemSecurityEnvironment().systemToken(APP_NAME);
+
+    try (final AutoUserContext ignored1 = new AutoSeshat(systemToken)) {
+      //Create a signature set then test that it is listed.
+      final String timestamp = getTestSubject().createSignatureSet().getTimestamp();
+      {
+        final List<String> signatureSets = anubis.getAllSignatureSets();
+        Assert.assertTrue(signatureSets.contains(timestamp));
+      }
+
+
+      final Authentication adminAuthenticationOnFirstKeyset;
+      try (final AutoUserContext ignored2 = new AutoGuest()) {
+        adminAuthenticationOnFirstKeyset = getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+      }
+
+      Assert.assertTrue(canAccessResources(adminAuthenticationOnFirstKeyset));
+
+      //For identity, application signature and identity manager signature should be identical.
+      final ApplicationSignatureSet signatureSet = anubis.getSignatureSet(timestamp);
+      Assert.assertEquals(signatureSet.getApplicationSignature(), signatureSet.getIdentityManagerSignature());
+
+      final Signature applicationSignature = anubis.getApplicationSignature(timestamp);
+      Assert.assertEquals(signatureSet.getApplicationSignature(), applicationSignature);
+
+      TimeUnit.SECONDS.sleep(2); //Timestamp has resolution at seconds level -- Make sure that second signature set has different timestamp from the first one.
+
+      //Create a second signature set and test that it and the previous signature set are listed.
+      final String timestamp2 = getTestSubject().createSignatureSet().getTimestamp();
+      {
+        final List<String> signatureSets = anubis.getAllSignatureSets();
+        Assert.assertTrue(signatureSets.contains(timestamp));
+        Assert.assertTrue(signatureSets.contains(timestamp2));
+      }
+
+      final Authentication adminAuthenticationOnSecondKeyset;
+      try (final AutoUserContext ignored2 = new AutoGuest()) {
+        adminAuthenticationOnSecondKeyset = getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment
+            .encodePassword(ADMIN_PASSWORD));
+      }
+
+      Assert.assertTrue(canAccessResources(adminAuthenticationOnFirstKeyset));
+      Assert.assertTrue(canAccessResources(adminAuthenticationOnSecondKeyset));
+
+      //Get the newly created signature set, and test that its contents are correct.
+      final ApplicationSignatureSet signatureSet2 = anubis.getSignatureSet(timestamp2);
+      Assert.assertEquals(signatureSet2.getApplicationSignature(), signatureSet2.getIdentityManagerSignature());
+
+      //Delete one of the signature sets and test that it is no longer listed.
+      anubis.deleteSignatureSet(timestamp);
+      {
+        final List<String> signatureSets = anubis.getAllSignatureSets();
+        Assert.assertFalse(signatureSets.contains(timestamp));
+        Assert.assertTrue(signatureSets.contains(timestamp2));
+      }
+
+      Assert.assertTrue(canAccessResources(adminAuthenticationOnSecondKeyset));
+      Assert.assertFalse(canAccessResources(adminAuthenticationOnFirstKeyset));
+
+      //Getting the newly deleted signature set should fail.
+      try {
+        anubis.getSignatureSet(timestamp);
+        Assert.fail("Not found exception should be thrown.");
+      } catch (final NotFoundException ignored) {
+      }
+
+      //Getting the newly deleted application signature set should likewise fail.
+      try {
+        anubis.getApplicationSignature(timestamp);
+        Assert.fail("Not found exception should be thrown.");
+      } catch (final NotFoundException ignored) {
+      }
+    }
+  }
+
+  private boolean canAccessResources(final Authentication adminAuthentication) {
+    try (final AutoUserContext ignored = new AutoUserContext(ADMIN_IDENTIFIER, adminAuthentication.getAccessToken())) {
+      getTestSubject().getUsers();
+      return true;
+    }
+    catch (Throwable ignored) {
+      return false;
+    }
+  }
+}
diff --git a/service/src/test/java/TestPasswords.java b/service/src/test/java/TestPasswords.java
new file mode 100644
index 0000000..11a74a4
--- /dev/null
+++ b/service/src/test/java/TestPasswords.java
@@ -0,0 +1,200 @@
+/*
+ * 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.
+ */
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.api.util.NotFoundException;
+import org.apache.fineract.cn.identity.api.v1.domain.Authentication;
+import org.apache.fineract.cn.identity.api.v1.domain.Password;
+import org.apache.fineract.cn.identity.api.v1.domain.UserWithPassword;
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.apache.fineract.cn.test.domain.TimeStampChecker;
+import org.apache.fineract.cn.test.env.TestEnvironment;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.time.Duration;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestPasswords extends AbstractComponentTest {
+
+  @Test
+  public void testAdminChangeUserPassword() throws InterruptedException {
+    final String username = createUserWithNonexpiredPassword(AHMES_PASSWORD, ADMIN_ROLE);
+
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final String newPassword = TestEnvironment.encodePassword(
+              AHMES_PASSWORD + "make_it_a_little_longer");
+
+      {
+        //Important here is that we are changing the password *as*the*admin*.
+        getTestSubject().changeUserPassword(username, new Password(newPassword));
+        boolean found = eventRecorder.wait(EventConstants.OPERATION_PUT_USER_PASSWORD, username);
+        Assert.assertTrue(found);
+      }
+
+      final Authentication newPasswordAuthentication = getTestSubject().login(username, newPassword);
+      try (final AutoUserContext ignore2 = new AutoUserContext(username, newPasswordAuthentication.getAccessToken()))
+      {
+        getTestSubject().createUser(new UserWithPassword("Ahmes_friend", "scribe",
+                TestEnvironment.encodePassword(AHMES_FRIENDS_PASSWORD)));
+        Assert.fail("createUser should've thrown an exception because the password is admin reset.");
+      }
+      catch (final NotFoundException ex)
+      {
+        //Should throw because under the new password, the user has only the right to change the password.
+      }
+
+      try (final AutoUserContext ignore3 = new AutoUserContext(username, newPasswordAuthentication.getAccessToken()))
+      {
+        getTestSubject().changeUserPassword(username, new Password(newPassword));
+        boolean found = eventRecorder.wait(EventConstants.OPERATION_PUT_USER_PASSWORD, username);
+        Assert.assertTrue(found);
+      }
+
+      final Authentication newPasswordAuthenticationAsFullyPermissionedUser = getTestSubject().login(username, newPassword);
+      try (final AutoUserContext ignore4 = new AutoUserContext(username, newPasswordAuthenticationAsFullyPermissionedUser.getAccessToken()))
+      {
+        //Now it should be possible to create a user since the user changed the password herself.
+        getTestSubject().createUser(new UserWithPassword("Ahmes_friend", "scribe",
+                TestEnvironment.encodePassword(AHMES_FRIENDS_PASSWORD)));
+      }
+    }
+  }
+
+  @Test
+  public void testAdminChangeAdminPassword() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final String newPassword = TestEnvironment.encodePassword(
+              ADMIN_PASSWORD + "make_it_a_little_longer");
+
+      {
+        getTestSubject().changeUserPassword(ADMIN_IDENTIFIER, new Password(newPassword));
+
+        final boolean found = eventRecorder.wait(EventConstants.OPERATION_PUT_USER_PASSWORD, ADMIN_IDENTIFIER);
+        Assert.assertTrue(found);
+      }
+
+      try {
+        final String oldPassword = TestEnvironment.encodePassword(ADMIN_PASSWORD);
+        getTestSubject().login(ADMIN_IDENTIFIER, oldPassword);
+        Assert.fail("Login with the old password should not succeed.");
+      } catch (final NotFoundException ignored) {
+      }
+
+      getTestSubject().login(ADMIN_IDENTIFIER, newPassword);
+
+      {
+        //Change the password back so the tests after this don't fail.
+        getTestSubject().changeUserPassword(ADMIN_IDENTIFIER, new Password(TestEnvironment.encodePassword(ADMIN_PASSWORD)));
+        boolean found = eventRecorder.wait(EventConstants.OPERATION_PUT_USER_PASSWORD, ADMIN_IDENTIFIER);
+        Assert.assertTrue(found);
+      }
+    }
+  }
+
+  @Test
+  public void testUserChangeOwnPasswordButNotAdminPassword() throws InterruptedException {
+    final String username = createUserWithNonexpiredPassword(AHMES_PASSWORD, "scribe");
+
+    try (final AutoUserContext ignored = loginUser(username, AHMES_PASSWORD))
+    {
+      final String newPassword = "new password";
+      {
+        getTestSubject().changeUserPassword(username, new Password(TestEnvironment.encodePassword(newPassword)));
+
+        boolean found = eventRecorder.wait(EventConstants.OPERATION_PUT_USER_PASSWORD, username);
+        Assert.assertTrue(found);
+      }
+
+      final TimeStampChecker passwordExpirationChecker = TimeStampChecker.inTheFutureWithWiggleRoom(Duration.ofDays(93), Duration.ofHours(24));
+      final Authentication userAuthenticationAfterPasswordChange = getTestSubject().login(username, TestEnvironment.encodePassword(newPassword));
+      final String passwordExpiration = userAuthenticationAfterPasswordChange.getPasswordExpiration();
+      passwordExpirationChecker.assertCorrect(passwordExpiration);
+
+      //noinspection EmptyCatchBlock
+      try {
+        getTestSubject().changeUserPassword(ADMIN_IDENTIFIER, new Password(TestEnvironment.encodePassword(newPassword)));
+        Assert.fail("trying to change the admins password should fail.");
+      }
+      catch (final NotFoundException ex) {
+        boolean found = eventRecorder.wait(EventConstants.OPERATION_PUT_USER_PASSWORD, ADMIN_IDENTIFIER);
+        Assert.assertFalse(found);
+      }
+
+
+      try {
+        getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(newPassword));
+        Assert.fail("logging into admin with the new password should likewise fail.");
+      }
+      catch (final NotFoundException ex) {
+        //Not found is expected.
+      }
+
+      //noinspection EmptyTryBlock
+      try (final AutoUserContext ignored2 = loginAdmin()) { //logging into admin with the old password should *not* fail.
+      }
+    }
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void loginWithUnencodedPasswordShouldThrowIllegalArgumentException() throws InterruptedException {
+
+    try (final AutoUserContext ignored = loginAdmin()) {
+      final String selfManagementRoleId = createSelfManagementRole();
+
+      final String userPassword = RandomStringUtils.randomAlphanumeric(5);
+      final String userid = createUserWithNonexpiredPassword(userPassword, selfManagementRoleId);
+
+      getTestSubject().login(userid, userPassword);
+    }
+
+  }
+
+  @Test
+  public void activatedAntonyPasswordDoesntExpire() throws InterruptedException {
+
+    try (final AutoUserContext ignored = loginAdmin()) {
+      final Authentication adminAuthentication =
+              getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+      Assert.assertEquals(null, adminAuthentication.getPasswordExpiration());
+    }
+  }
+
+  @Test
+  public void onlyAntonyCanSetAntonyPassword() throws InterruptedException {
+    try (final AutoUserContext ignored = loginAdmin()) {
+
+      final String roleIdentifier = createRole(buildUserPermission(), buildSelfPermission(), buildRolePermission());
+      final String username = createUserWithNonexpiredPassword(AHMES_PASSWORD, roleIdentifier);
+
+      try (final AutoUserContext ignored2 = loginUser(username, AHMES_PASSWORD)) {
+        getTestSubject().changeUserPassword(ADMIN_IDENTIFIER, new Password(TestEnvironment.encodePassword(AHMES_FRIENDS_PASSWORD)));
+        Assert.fail("Should not be able to change antony's password from any account other than antony's.");
+      }
+      catch (final IllegalArgumentException expected) {
+        //noinspection EmptyCatchBlock
+      }
+    }
+
+    getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+  }
+}
diff --git a/service/src/test/java/TestPermittableGroups.java b/service/src/test/java/TestPermittableGroups.java
new file mode 100644
index 0000000..2c1636d
--- /dev/null
+++ b/service/src/test/java/TestPermittableGroups.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+import org.apache.fineract.cn.anubis.api.v1.domain.PermittableEndpoint;
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.identity.api.v1.PermittableGroupIds;
+import org.apache.fineract.cn.identity.api.v1.domain.PermittableGroup;
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestPermittableGroups extends AbstractComponentTest {
+  @Test
+  public void getPermittableGroups() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final PermittableGroup identityManagementPermittableGroup
+              = getTestSubject().getPermittableGroup(PermittableGroupIds.IDENTITY_MANAGEMENT);
+      Assert.assertNotNull(identityManagementPermittableGroup);
+      Assert.assertEquals(PermittableGroupIds.IDENTITY_MANAGEMENT, identityManagementPermittableGroup.getIdentifier());
+
+      final PermittableGroup roleManagementPermittableGroup
+              = getTestSubject().getPermittableGroup(PermittableGroupIds.ROLE_MANAGEMENT);
+      Assert.assertNotNull(roleManagementPermittableGroup);
+      Assert.assertEquals(PermittableGroupIds.ROLE_MANAGEMENT, roleManagementPermittableGroup.getIdentifier());
+
+      final PermittableGroup selfManagementPermittableGroup
+              = getTestSubject().getPermittableGroup(PermittableGroupIds.SELF_MANAGEMENT);
+      Assert.assertNotNull(selfManagementPermittableGroup);
+      Assert.assertEquals(PermittableGroupIds.SELF_MANAGEMENT, selfManagementPermittableGroup.getIdentifier());
+    }
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void createWithIllegalMethodThrows() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final String identifier = testEnvironment.generateUniqueIdentifer("group");
+
+      final PermittableEndpoint permittableEndpoint = buildPermittableEndpoint();
+      permittableEndpoint.setMethod("blah");
+      final PermittableGroup group = buildPermittableGroup(identifier, permittableEndpoint);
+
+      getTestSubject().createPermittableGroup(group);
+      Assert.assertFalse("create should throw because 'blah' is an illegal method name.", true);
+    }
+  }
+
+  @Test
+  public void create() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final String identifier = testEnvironment.generateUniqueIdentifer("group");
+
+      final PermittableEndpoint permittableEndpoint = buildPermittableEndpoint();
+      final PermittableGroup group = buildPermittableGroup(identifier, permittableEndpoint);
+
+      getTestSubject().createPermittableGroup(group);
+
+      {
+        final boolean found = eventRecorder.wait(EventConstants.OPERATION_POST_PERMITTABLE_GROUP, group.getIdentifier());
+        Assert.assertTrue(found);
+      }
+
+      final List<PermittableGroup> permittableGroups = getTestSubject().getPermittableGroups();
+      Assert.assertTrue(Helpers.instancePresent(permittableGroups, PermittableGroup::getIdentifier, identifier));
+
+      final PermittableGroup createdGroup = getTestSubject().getPermittableGroup(identifier);
+      Assert.assertNotNull(createdGroup);
+      Assert.assertEquals(createdGroup, group);
+      Assert.assertEquals(Collections.singletonList(permittableEndpoint), createdGroup.getPermittables());
+    }
+  }
+
+  private PermittableGroup buildPermittableGroup(final String identifier, final PermittableEndpoint permittableEndpoint) {
+    final PermittableGroup ret = new PermittableGroup();
+    ret.setIdentifier(identifier);
+    ret.setPermittables(Collections.singletonList(permittableEndpoint));
+    return ret;
+  }
+
+  private PermittableEndpoint buildPermittableEndpoint() {
+    final PermittableEndpoint ret = new PermittableEndpoint();
+    ret.setPath("/x/y/z");
+    ret.setMethod("POST");
+    return ret;
+  }
+}
diff --git a/service/src/test/java/TestProvisioning.java b/service/src/test/java/TestProvisioning.java
new file mode 100644
index 0000000..d7c035d
--- /dev/null
+++ b/service/src/test/java/TestProvisioning.java
@@ -0,0 +1,165 @@
+/*
+ * 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.
+ */
+
+import org.apache.fineract.cn.anubis.api.v1.RoleConstants;
+import org.apache.fineract.cn.anubis.api.v1.domain.ApplicationSignatureSet;
+import org.apache.fineract.cn.anubis.api.v1.domain.Signature;
+import org.apache.fineract.cn.anubis.token.SystemAccessTokenSerializer;
+import org.apache.fineract.cn.api.context.AutoSeshat;
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.api.util.InvalidTokenException;
+import org.apache.fineract.cn.identity.api.v1.client.IdentityManager;
+import org.apache.fineract.cn.lang.AutoTenantContext;
+import org.apache.fineract.cn.lang.TenantContextHolder;
+import org.apache.fineract.cn.test.env.TestEnvironment;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.RSAPrivateKeySpec;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestProvisioning extends AbstractComponentTest {
+
+  @Test
+  public void testBoundaryInitializeCases() throws InterruptedException {
+    final IdentityManager testSubject = getTestSubject();
+
+
+    final ApplicationSignatureSet firstTenantSignatureSet;
+    final Signature firstTenantIdentityManagerSignature;
+
+    //Create tenant keyspaces.
+    final String tenant1 = TestEnvironment.getRandomTenantName();
+    final String tenant2 = TestEnvironment.getRandomTenantName();
+    cassandraInitializer.initializeTenant(tenant1);
+    cassandraInitializer.initializeTenant(tenant2);
+    TimeUnit.SECONDS.sleep(1);
+    // This gives cassandra a chance to complete saving the new keyspaces.
+    // Theoretically, the creation of keyspaces is synchronous, but I've
+    // found that the cassandra driver needs just a little bit longer
+    // To show up in the request for metadata for that keyspace.
+
+
+    try (final AutoTenantContext ignored = new AutoTenantContext(tenant1)) {
+
+      final String invalidSeshatToken = "notBearer";
+      try (final AutoSeshat ignored2 = new AutoSeshat(invalidSeshatToken)){
+        testSubject.initialize(TestEnvironment.encodePassword(ADMIN_PASSWORD));
+        Assert.fail("The key had the wrong format.  This should've failed.");
+      }
+      catch (final InvalidTokenException ignored2)
+      {
+      }
+
+
+      final String wrongSystemToken = systemTokenFromWrongKey();
+      try (final AutoSeshat ignored2 = new AutoSeshat(wrongSystemToken)){
+        testSubject.initialize(TestEnvironment.encodePassword(ADMIN_PASSWORD));
+        Assert.fail("The key was signed by the wrong source.  This should've failed.");
+      }
+      catch (final Exception e)
+      {
+        Assert.assertTrue("The exception should be 'invalid token'", (e instanceof InvalidTokenException));
+      }
+
+
+      try (final AutoUserContext ignored2 = tenantApplicationSecurityEnvironment.createAutoSeshatContext("goober")) {
+        testSubject.initialize(TestEnvironment.encodePassword(ADMIN_PASSWORD));
+        Assert.fail("The key was intended for a different tenant.  This should've failed.");
+      }
+      catch (final Exception e)
+      {
+        Assert.assertTrue("The exception should be 'not found'", (e instanceof InvalidTokenException));
+      }
+
+      // The second otherwise valid call to initialize for the same tenant should
+      // not fail even though the tenant is now already initialized.
+      try (final AutoUserContext ignored2 = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+        firstTenantSignatureSet = testSubject.initialize(TestEnvironment.encodePassword(ADMIN_PASSWORD));
+
+        final Signature applicationSignature = tenantApplicationSecurityEnvironment.getAnubis().getApplicationSignature(firstTenantSignatureSet.getTimestamp());
+        firstTenantIdentityManagerSignature = tenantApplicationSecurityEnvironment.getAnubis().getSignatureSet(firstTenantSignatureSet.getTimestamp()).getIdentityManagerSignature();
+        Assert.assertEquals(applicationSignature, firstTenantIdentityManagerSignature);
+
+        testSubject.initialize("golden_osiris");
+      }
+    }
+
+
+    final ApplicationSignatureSet secondTenantSignatureSet;
+    try (final AutoTenantContext ignored = new AutoTenantContext(tenant2)) {
+      try (final AutoUserContext ignored2
+                   = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+        secondTenantSignatureSet = testSubject.initialize(TestEnvironment.encodePassword(ADMIN_PASSWORD));
+        final Signature secondTenantIdentityManagerSignature = tenantApplicationSecurityEnvironment.getAnubis().getApplicationSignature(secondTenantSignatureSet.getTimestamp());
+        Assert.assertNotEquals(firstTenantIdentityManagerSignature, secondTenantIdentityManagerSignature);
+      }
+    }
+    catch (final Exception e)
+    {
+      Assert.fail("Call to initialize for a second tenant should succeed. "
+          + "The exception was " + e
+      );
+      throw e;
+    }
+
+
+
+    TenantContextHolder.clear();
+  }
+
+  private String systemTokenFromWrongKey()
+  {
+    final SystemAccessTokenSerializer.Specification tokenSpecification
+        = new SystemAccessTokenSerializer.Specification();
+
+    tokenSpecification.setKeyTimestamp("rando");
+    tokenSpecification.setPrivateKey(getWrongPrivateKey());
+
+    tokenSpecification.setRole(RoleConstants.SYSTEM_ADMIN_ROLE_IDENTIFIER);
+    tokenSpecification.setSecondsToLive(TimeUnit.HOURS.toSeconds(12L));
+    tokenSpecification.setTargetApplicationName(APP_NAME);
+    tokenSpecification.setTenant(TenantContextHolder.checkedGetIdentifier());
+
+    return new SystemAccessTokenSerializer().build(tokenSpecification).getToken();
+  }
+
+
+  private PrivateKey getWrongPrivateKey() {
+    try {
+      final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+      final BigInteger privateKeyMod = new BigInteger("17492023076590407419772677529630320569634203626210733433914657600705550392421401008213478702016995697617177968917710500448063776244435761300358170637857566629780506514059676334317863416316403254208652514809444684705031748559737773841335114470369449872988726825545007588731959817361831095583721775522968972071928027030514641182819255368960492742269021132488312466659639538013906582095129294788911611410130509557024329936361580892139238423117992298190557606490543083859770282260174239092737213765902825945545746379786237952115129023474946280230282545899883335448866567923667432417504919606306921621754480829178419392063");
+      final BigInteger privateKeyExp = new BigInteger("3836197074627064495542864246660307880240969356539464297200899853440665208817504565223497099105700278649491111086168927826113808321425686210385705579717210204462139251515628239821027066889171978771395739740240603830895850009141569242130546108040040023566336125601696661013541334741315567340965150672011734372736240827969821590544366269533567400051316301569296349329670063250330460924547069022975441956699127698164632663315582302411984903513839691646332895582584509587803859003388718326640891827257180737700763719907116123118603418352134643169731657061459925351503055596019271348089711003706283690698717182672701958953");
+      final RSAPrivateKeySpec privateKeySpec = new RSAPrivateKeySpec(privateKeyMod, privateKeyExp);
+
+      return keyFactory.generatePrivate(privateKeySpec);
+
+    } catch (final InvalidKeySpecException | NoSuchAlgorithmException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+}
diff --git a/service/src/test/java/TestRefreshToken.java b/service/src/test/java/TestRefreshToken.java
new file mode 100644
index 0000000..db51294
--- /dev/null
+++ b/service/src/test/java/TestRefreshToken.java
@@ -0,0 +1,185 @@
+/*
+ * 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.
+ */
+
+import org.apache.fineract.cn.anubis.api.v1.TokenConstants;
+import org.apache.fineract.cn.anubis.token.TenantRefreshTokenSerializer;
+import org.apache.fineract.cn.anubis.token.TokenSerializationResult;
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.api.util.FeignTargetWithCookieJar;
+import org.apache.fineract.cn.api.util.InvalidTokenException;
+import org.apache.fineract.cn.api.util.NotFoundException;
+import org.apache.fineract.cn.identity.api.v1.client.IdentityManager;
+import org.apache.fineract.cn.identity.api.v1.domain.Authentication;
+import org.apache.fineract.cn.identity.api.v1.domain.Password;
+import org.apache.fineract.cn.identity.api.v1.domain.Permission;
+import org.apache.fineract.cn.identity.api.v1.domain.User;
+import org.apache.fineract.cn.test.domain.TimeStampChecker;
+import org.apache.fineract.cn.test.env.TestEnvironment;
+import org.apache.fineract.cn.test.fixture.TenantDataStoreTestContext;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestRefreshToken extends AbstractComponentTest {
+  private static final int ACCESS_TOKEN_TIME_TO_LIVE = 5;
+  private static final int REFRESH_TOKEN_TIME_TO_LIVE = 10;
+
+  @BeforeClass
+  public static void setup() throws Exception {
+    //Shorten access time to 5 seconds for test purposes.;
+    System.getProperties().setProperty("identity.token.access.ttl", String.valueOf(ACCESS_TOKEN_TIME_TO_LIVE));
+    System.getProperties().setProperty("identity.token.refresh.ttl", String.valueOf(REFRESH_TOKEN_TIME_TO_LIVE));
+  }
+
+  @Test(expected = InvalidTokenException.class)
+  public void adminLoginAccessTokenShouldTimeOut() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      Thread.sleep(TimeUnit.SECONDS.toMillis(ACCESS_TOKEN_TIME_TO_LIVE + 1));
+      getTestSubject().getUser(ADMIN_IDENTIFIER);
+    }
+  }
+
+  @Test(expected = InvalidTokenException.class)
+  public void adminLoginRefreshTokenShouldTimeOut() throws InterruptedException {
+    getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+
+    Thread.sleep(TimeUnit.SECONDS.toMillis(REFRESH_TOKEN_TIME_TO_LIVE + 1));
+
+    getTestSubject().refresh();
+  }
+
+  @Test
+  public void afterAccessTokenExpiresRefreshTokenShouldAcquireNewAccessToken() throws InterruptedException {
+    getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+
+    Thread.sleep(TimeUnit.SECONDS.toMillis(ACCESS_TOKEN_TIME_TO_LIVE + 1));
+
+    final Authentication refreshAccessTokenAuthentication =
+            getTestSubject().refresh();
+
+    try (final AutoUserContext ignored = new AutoUserContext(ADMIN_IDENTIFIER, refreshAccessTokenAuthentication.getAccessToken())) {
+      getTestSubject().changeUserPassword(ADMIN_IDENTIFIER, new Password(TestEnvironment.encodePassword(ADMIN_PASSWORD)));
+    }
+  }
+
+  @Test(expected = InvalidTokenException.class)
+  public void refreshTokenShouldGrantAccessOnlyToOneTenant()
+  {
+    getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+
+    try (final TenantDataStoreTestContext ignored = TenantDataStoreTestContext.forRandomTenantName(cassandraInitializer)) {
+      try (final AutoUserContext ignored2
+                   = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+        getTestSubject().initialize(TestEnvironment.encodePassword(ADMIN_PASSWORD));
+      }
+
+      getTestSubject().refresh();
+    }
+  }
+
+  @Test
+  public void expirationDatesShouldBeCorrectIsoDateTimes() throws InterruptedException {
+    final Authentication authentication =
+            getTestSubject().login(ADMIN_IDENTIFIER, TestEnvironment.encodePassword(ADMIN_PASSWORD));
+
+    final TimeStampChecker preRefreshAccessTokenTimeStampChecker = TimeStampChecker.inTheFuture(Duration.ofSeconds(ACCESS_TOKEN_TIME_TO_LIVE));
+    final TimeStampChecker refreshTokenTimeStampChecker = TimeStampChecker.inTheFuture(Duration.ofSeconds(REFRESH_TOKEN_TIME_TO_LIVE));
+
+    Assert.assertNotNull(authentication);
+
+    preRefreshAccessTokenTimeStampChecker.assertCorrect(authentication.getAccessTokenExpiration());
+    refreshTokenTimeStampChecker.assertCorrect(authentication.getRefreshTokenExpiration());
+
+    TimeUnit.SECONDS.sleep(3);
+    final TimeStampChecker postRefreshAccessTokenTimeStampChecker = TimeStampChecker.inTheFuture(Duration.ofSeconds(ACCESS_TOKEN_TIME_TO_LIVE));
+
+    final Authentication refreshedAuthentication = getTestSubject().refresh();
+
+    postRefreshAccessTokenTimeStampChecker.assertCorrect(refreshedAuthentication.getAccessTokenExpiration());
+    refreshTokenTimeStampChecker.assertCorrect(refreshedAuthentication.getRefreshTokenExpiration());
+  }
+
+  @Test
+  public void bothRefreshMethodsShouldProduceSamePermissions() throws InterruptedException {
+    final Permission userPermission = buildUserPermission();
+    final ApplicationSignatureTestData appPlusSig;
+    try (final AutoUserContext ignored
+                 = tenantApplicationSecurityEnvironment.createAutoSeshatContext()) {
+      appPlusSig = setApplicationSignature();
+      createApplicationPermission(appPlusSig.getApplicationIdentifier(), userPermission);
+    }
+
+    try (final AutoUserContext ignored = loginAdmin()) {
+      getTestSubject().setApplicationPermissionEnabledForUser(
+              appPlusSig.getApplicationIdentifier(),
+              userPermission.getPermittableEndpointGroupIdentifier(),
+              ADMIN_IDENTIFIER,
+              true);
+    }
+
+    final TenantRefreshTokenSerializer refreshTokenSerializer = new TenantRefreshTokenSerializer();
+
+    final TokenSerializationResult tokenSerializationResult =
+            refreshTokenSerializer.build(new TenantRefreshTokenSerializer.Specification()
+                    .setUser(ADMIN_IDENTIFIER)
+                    .setSecondsToLive(30)
+                    .setKeyTimestamp(appPlusSig.getKeyTimestamp())
+                    .setPrivateKey(appPlusSig.getKeyPair().privateKey())
+                    .setSourceApplication(appPlusSig.getApplicationIdentifier()));
+
+    final FeignTargetWithCookieJar<IdentityManager> identityManagerWithCookieJar
+            = apiFactory.createWithCookieJar(IdentityManager.class, testEnvironment.serverURI());
+
+    identityManagerWithCookieJar.putCookie("/token", TokenConstants.REFRESH_TOKEN_COOKIE_NAME, tokenSerializationResult.getToken());
+
+    final Authentication applicationAuthenticationViaCookie = identityManagerWithCookieJar.getFeignTarget().refresh();
+
+    final Authentication applicationAuthenticationViaParam = getTestSubject().refresh(tokenSerializationResult.getToken());
+
+    try (final AutoUserContext ignored = new AutoUserContext(ADMIN_IDENTIFIER, applicationAuthenticationViaCookie.getAccessToken()))
+    {
+      checkAccessToUsersAndOnlyUsers();
+    }
+
+    try (final AutoUserContext ignored = new AutoUserContext(ADMIN_IDENTIFIER, applicationAuthenticationViaParam.getAccessToken()))
+    {
+      checkAccessToUsersAndOnlyUsers();
+    }
+
+  }
+
+  private void checkAccessToUsersAndOnlyUsers() {
+    final List<User> users = getTestSubject().getUsers();
+    Assert.assertFalse(users.isEmpty());
+
+    try {
+      getTestSubject().getRoles();
+      Assert.fail("Shouldn't be able to get roles with token for application for which roles are not permitted.");
+    }
+    catch (final NotFoundException ignored2) { }
+  }
+
+}
diff --git a/service/src/test/java/TestRoles.java b/service/src/test/java/TestRoles.java
new file mode 100644
index 0000000..b0553d3
--- /dev/null
+++ b/service/src/test/java/TestRoles.java
@@ -0,0 +1,261 @@
+/*
+ * 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.
+ */
+
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.api.util.NotFoundException;
+import org.apache.fineract.cn.identity.api.v1.domain.Permission;
+import org.apache.fineract.cn.identity.api.v1.domain.Role;
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.apache.fineract.cn.identity.internal.util.IdentityConstants.SU_ROLE;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestRoles extends AbstractComponentTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("src/doc/generated-snippets/test-roles");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  final String path = "/identity/v1";
+
+  @Before
+  public void setUp(){
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void testRolesSortedAlphabetically() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final Permission rolePermission = buildRolePermission();
+
+      final Role role1 = buildRole(testEnvironment.generateUniqueIdentifer("abba"), rolePermission);
+      final Role role2 = buildRole(testEnvironment.generateUniqueIdentifer("bubba"), rolePermission);
+      final Role role3 = buildRole(testEnvironment.generateUniqueIdentifer("c1"), rolePermission);
+      final Role role4 = buildRole(testEnvironment.generateUniqueIdentifer("calla"), rolePermission);
+      final Role role5 = buildRole(testEnvironment.generateUniqueIdentifer("uelf"), rolePermission);
+      final Role role6 = buildRole(testEnvironment.generateUniqueIdentifer("ulf"), rolePermission);
+
+      getTestSubject().createRole(role2);
+      getTestSubject().createRole(role1);
+      getTestSubject().createRole(role6);
+      getTestSubject().createRole(role4);
+      getTestSubject().createRole(role3);
+      getTestSubject().createRole(role5);
+
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_ROLE, role1.getIdentifier()));
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_ROLE, role2.getIdentifier()));
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_ROLE, role3.getIdentifier()));
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_ROLE, role4.getIdentifier()));
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_ROLE, role5.getIdentifier()));
+      Assert.assertTrue(eventRecorder.wait(EventConstants.OPERATION_POST_ROLE, role6.getIdentifier()));
+
+      final List<Role> roles = getTestSubject().getRoles();
+      final List<String> idList = roles.stream().map(Role::getIdentifier).collect(Collectors.toList());
+      final List<String> sortedList = Arrays.asList(
+              role1.getIdentifier(),
+              role2.getIdentifier(),
+              role3.getIdentifier(),
+              role4.getIdentifier(),
+              role5.getIdentifier(),
+              role6.getIdentifier());
+      final List<String> filterOutIdsFromOtherTests = idList.stream().filter(sortedList::contains).collect(Collectors.toList());
+      Assert.assertEquals(sortedList, filterOutIdsFromOtherTests);
+    }
+  }
+
+  @Test
+  public void testCreateRole() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final String roleIdentifier = generateRoleIdentifier();
+
+      final Permission rolePermission = buildRolePermission();
+      final Role scribe = buildRole(roleIdentifier, rolePermission);
+
+      getTestSubject().createRole(scribe);
+
+      {
+        final boolean found = eventRecorder.wait(EventConstants.OPERATION_POST_ROLE, scribe.getIdentifier());
+        Assert.assertTrue(found);
+      }
+
+      final List<Role> roles = getTestSubject().getRoles();
+      Assert.assertTrue(Helpers.instancePresent(roles, Role::getIdentifier, roleIdentifier));
+
+      final Role role = getTestSubject().getRole(roleIdentifier);
+      Assert.assertNotNull(role);
+      Assert.assertEquals(roleIdentifier, role.getIdentifier());
+      Assert.assertEquals(Collections.singletonList(rolePermission), role.getPermissions());
+    }
+
+    try {
+      this.mockMvc.perform(post(path + "/roles")
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE))
+              .andExpect(status().is4xxClientError());
+    } catch (Exception e) {e.printStackTrace();}
+
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void shouldNotBeAbleToCreateRoleNamedDeactivated() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final Permission rolePermission = buildRolePermission();
+      final Role deactivated = buildRole("deactivated", rolePermission);
+
+      getTestSubject().createRole(deactivated);
+    }
+  }
+
+  @Test
+  public void deleteRole() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final String roleIdentifier = createRoleManagementRole();
+
+      final Role role = getTestSubject().getRole(roleIdentifier);
+      Assert.assertNotNull(role);
+
+      getTestSubject().deleteRole(role.getIdentifier());
+
+      {
+        final boolean found = eventRecorder.wait(EventConstants.OPERATION_DELETE_ROLE, roleIdentifier);
+        Assert.assertTrue(found);
+      }
+
+      final List<Role> roles = getTestSubject().getRoles();
+      Assert.assertFalse(Helpers.instancePresent(roles, Role::getIdentifier, roleIdentifier));
+
+      try
+      {
+        this.mockMvc.perform(delete(path + "/roles/" + roleIdentifier)
+                .accept(MediaType.ALL_VALUE)
+                .contentType(MediaType.APPLICATION_JSON_VALUE))
+                .andExpect(status().isNotFound());
+      } catch (Exception exception){ exception.printStackTrace(); }
+    }
+  }
+
+  @Test(expected= NotFoundException.class)
+  public void deleteRoleThatDoesntExist() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final String randomIdentifier = generateRoleIdentifier();
+
+      getTestSubject().deleteRole(randomIdentifier);
+    }
+  }
+
+  @Test()
+  public void changeRole() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final String roleIdentifier = createRoleManagementRole();
+
+      final Role role = getTestSubject().getRole(roleIdentifier);
+      role.getPermissions().add(buildUserPermission());
+
+      getTestSubject().changeRole(roleIdentifier, role);
+
+      {
+        final boolean found = eventRecorder.wait(EventConstants.OPERATION_PUT_ROLE, role.getIdentifier());
+        Assert.assertTrue(found);
+      }
+
+      final Role changedRole = getTestSubject().getRole(roleIdentifier);
+      Assert.assertEquals(role, changedRole);
+
+      try
+      {
+        this.mockMvc.perform(put(path + "/roles/" + roleIdentifier )
+                .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().is4xxClientError());
+      } catch (Exception E){ E.printStackTrace();}
+
+    }
+  }
+
+  @Test
+  public void testChangePharaohRoleFails() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final Role referenceRole = getTestSubject().getRole(SU_ROLE);
+      final Role roleChangeRequest = buildRole(SU_ROLE, buildSelfPermission());
+
+      try {
+        getTestSubject().changeRole(SU_ROLE, roleChangeRequest);
+        Assert.fail("Should not be able to change the pharaoh role.");
+      }
+      catch (final IllegalArgumentException expected) {
+        //noinspection EmptyCatchBlock
+      }
+
+      final Role unChangedRole = getTestSubject().getRole(SU_ROLE);
+      Assert.assertEquals(referenceRole, unChangedRole);
+    }
+  }
+
+  @Test
+  public void testDeletePharaohRoleFails() throws InterruptedException {
+
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final Role adminRole = getTestSubject().getRole(ADMIN_ROLE);
+      try {
+        getTestSubject().deleteRole(ADMIN_ROLE);
+        Assert.fail("It should not be possible to delete the admin role.");
+      }
+      catch (final IllegalArgumentException expected) {
+        //noinspection EmptyCatchBlock
+      }
+
+      final Role adminRoleStillThere = getTestSubject().getRole(ADMIN_ROLE);
+      Assert.assertEquals(adminRole, adminRoleStillThere);
+    }
+  }
+}
\ No newline at end of file
diff --git a/service/src/test/java/TestSuite.java b/service/src/test/java/TestSuite.java
new file mode 100644
index 0000000..8a40a9f
--- /dev/null
+++ b/service/src/test/java/TestSuite.java
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * @author Myrle Krantz
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    TestApplications.class,
+    TestAuthentication.class,
+    TestKeyRotation.class,
+    //TestPasswords.class,
+    TestPermittableGroups.class,
+    TestProvisioning.class,
+    //TestRefreshToken.class,
+    TestRoles.class,
+    TestUsers.class,
+})
+public class TestSuite extends SuiteTestEnvironment {
+  //TODO: Add TestPasswords and TestRefreshToken back in.
+  // For some reason, they fail in the test suite even though
+  // they succeed when run individually.
+}
diff --git a/service/src/test/java/TestUsers.java b/service/src/test/java/TestUsers.java
new file mode 100644
index 0000000..3fc9c2f
--- /dev/null
+++ b/service/src/test/java/TestUsers.java
@@ -0,0 +1,203 @@
+/*
+ * 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.
+ */
+
+import org.apache.fineract.cn.anubis.api.v1.domain.AllowedOperation;
+import org.apache.fineract.cn.api.context.AutoUserContext;
+import org.apache.fineract.cn.identity.api.v1.PermittableGroupIds;
+import org.apache.fineract.cn.identity.api.v1.domain.*;
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.apache.fineract.cn.test.env.TestEnvironment;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.restdocs.JUnitRestDocumentation;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.apache.fineract.cn.identity.internal.util.IdentityConstants.SU_NAME;
+import static org.apache.fineract.cn.identity.internal.util.IdentityConstants.SU_ROLE;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TestUsers extends AbstractComponentTest {
+
+  @Rule
+  public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("src/doc/generated-snippets/test-users");
+
+  @Autowired
+  private WebApplicationContext context;
+
+  private MockMvc mockMvc;
+
+  final String path = "/identity/v1";
+
+  @Before
+  public void setUp(){
+
+    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
+            .apply(documentationConfiguration(this.restDocumentation))
+            .alwaysDo(document("{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
+            .build();
+  }
+
+  @Test
+  public void testAddLogin() throws InterruptedException {
+
+    final String username = createUserWithNonexpiredPassword(AHMES_PASSWORD, ADMIN_ROLE);
+
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final User user = getTestSubject().getUser(username);
+      Assert.assertNotNull(user);
+      Assert.assertEquals("Correct user identifier?", username, user.getIdentifier());
+      Assert.assertEquals("Correct role?", ADMIN_ROLE, user.getRole());
+    }
+
+    final Authentication userAuthentication =
+            getTestSubject().login(username, TestEnvironment.encodePassword(AHMES_PASSWORD));
+
+    Assert.assertNotNull(userAuthentication);
+
+    try (final AutoUserContext ignored = new AutoUserContext(username, userAuthentication.getAccessToken())) {
+      getTestSubject().createUser(new UserWithPassword("Ahmes_friend", "scribe",
+              TestEnvironment.encodePassword(AHMES_FRIENDS_PASSWORD)));
+
+      final boolean found = eventRecorder.wait(EventConstants.OPERATION_POST_USER, "Ahmes_friend");
+      Assert.assertTrue(found);
+    }
+
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final List<User> users = getTestSubject().getUsers();
+      Assert.assertTrue(Helpers.instancePresent(users, User::getIdentifier, username));
+      Assert.assertTrue(Helpers.instancePresent(users, User::getIdentifier, "Ahmes_friend"));
+    }
+
+    try {
+      this.mockMvc.perform(post(path + "/users/")
+              .accept(MediaType.APPLICATION_JSON_VALUE)
+              .contentType(MediaType.APPLICATION_JSON_VALUE))
+              .andExpect(status().is4xxClientError());
+    } catch (Exception e) {e.printStackTrace();}
+  }
+
+  @Test
+  public void testChangeUserRole() throws InterruptedException {
+    final String userIdentifier = createUserWithNonexpiredPassword(AHMES_PASSWORD, ADMIN_ROLE);
+
+    final Authentication ahmesAuthentication =
+            getTestSubject().login(userIdentifier, TestEnvironment.encodePassword(AHMES_PASSWORD));
+
+    try (final AutoUserContext ignored = new AutoUserContext(userIdentifier, ahmesAuthentication.getAccessToken())) {
+      List<User> users = getTestSubject().getUsers();
+      Assert.assertEquals(2, users.size());
+
+      getTestSubject().changeUserRole(userIdentifier, new RoleIdentifier("scribe"));
+
+      final boolean found = eventRecorder.wait(EventConstants.OPERATION_PUT_USER_ROLEIDENTIFIER, userIdentifier);
+      Assert.assertTrue(found);
+
+      final User ahmes = getTestSubject().getUser(userIdentifier);
+      Assert.assertEquals("scribe", ahmes.getRole());
+
+      final Set<Permission> userPermittableGroups = getTestSubject().getUserPermissions(userIdentifier);
+      Assert.assertTrue(userPermittableGroups.contains(new Permission(PermittableGroupIds.SELF_MANAGEMENT, AllowedOperation.ALL)));
+
+      users = getTestSubject().getUsers();
+      Assert.assertEquals(2, users.size());
+    }
+
+    try
+    {
+      this.mockMvc.perform(put(path + "/users/" + userIdentifier )
+              .contentType(MediaType.APPLICATION_JSON))
+              .andExpect(status().is4xxClientError());
+    } catch (Exception E){ E.printStackTrace();}
+  }
+
+  @Test
+  public void testChangeAntonyRoleFails() throws InterruptedException {
+    final String userIdentifier = createUserWithNonexpiredPassword(AHMES_PASSWORD, ADMIN_ROLE);
+
+    final Authentication ahmesAuthentication =
+            getTestSubject().login(userIdentifier, TestEnvironment.encodePassword(AHMES_PASSWORD));
+
+    try (final AutoUserContext ignored = new AutoUserContext(userIdentifier, ahmesAuthentication.getAccessToken())) {
+      try {
+        getTestSubject().changeUserRole(SU_NAME, new RoleIdentifier("scribe"));
+        Assert.fail("Should not be able to change the role set for antony.");
+      }
+      catch (final IllegalArgumentException expected) {
+        //noinspection EmptyCatchBlock
+      }
+
+      final User antony = getTestSubject().getUser(SU_NAME);
+      Assert.assertEquals(SU_ROLE, antony.getRole());
+    }
+  }
+
+  @Test
+  public void testAdminProvisioning() throws InterruptedException {
+    try (final AutoUserContext ignore = loginAdmin()) {
+      final List<Role> roleIdentifiers = getTestSubject().getRoles();
+      Assert.assertTrue(Helpers.instancePresent(roleIdentifiers, Role::getIdentifier, ADMIN_ROLE));
+
+      final Role role = getTestSubject().getRole(ADMIN_ROLE);
+      Assert.assertNotNull(role);
+      Assert.assertTrue(role.getPermissions().contains(constructFullAccessPermission(PermittableGroupIds.IDENTITY_MANAGEMENT)));
+      Assert.assertTrue(role.getPermissions().contains(constructFullAccessPermission(PermittableGroupIds.ROLE_MANAGEMENT)));
+
+      final List<User> userIdentifiers = getTestSubject().getUsers();
+      Assert.assertTrue(Helpers.instancePresent(userIdentifiers, User::getIdentifier, ADMIN_IDENTIFIER));
+
+      final User user = getTestSubject().getUser(ADMIN_IDENTIFIER);
+      Assert.assertNotNull(user);
+      Assert.assertEquals(ADMIN_IDENTIFIER, user.getIdentifier());
+      Assert.assertEquals(ADMIN_ROLE, user.getRole());
+
+      final Set<Permission> adminPermittableGroups = getTestSubject().getUserPermissions(ADMIN_IDENTIFIER);
+      Assert.assertTrue(adminPermittableGroups.contains(new Permission(PermittableGroupIds.SELF_MANAGEMENT, AllowedOperation.ALL)));
+      Assert.assertTrue(adminPermittableGroups.contains(new Permission(PermittableGroupIds.IDENTITY_MANAGEMENT, AllowedOperation.ALL)));
+      Assert.assertTrue(adminPermittableGroups.contains(new Permission(PermittableGroupIds.ROLE_MANAGEMENT, AllowedOperation.ALL)));
+    }
+  }
+
+  private Permission constructFullAccessPermission(final String permittableGroupId) {
+    final HashSet<AllowedOperation> allowedOperations = new HashSet<>();
+    allowedOperations.add(AllowedOperation.CHANGE);
+    allowedOperations.add(AllowedOperation.DELETE);
+    allowedOperations.add(AllowedOperation.READ);
+    return new Permission(permittableGroupId, allowedOperations);
+  }
+}
diff --git a/service/src/test/java/listener/ApplicationEventListener.java b/service/src/test/java/listener/ApplicationEventListener.java
new file mode 100644
index 0000000..9e22d07
--- /dev/null
+++ b/service/src/test/java/listener/ApplicationEventListener.java
@@ -0,0 +1,131 @@
+/*
+ * 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 listener;
+
+import org.apache.fineract.cn.identity.api.v1.events.*;
+import org.apache.fineract.cn.lang.config.TenantHeaderFilter;
+import org.apache.fineract.cn.test.listener.EventRecorder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Component
+public class ApplicationEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public ApplicationEventListener(@SuppressWarnings("SpringJavaAutowiringInspection") final EventRecorder eventRecorder)
+  {
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+          subscription = EventConstants.DESTINATION,
+          destination = EventConstants.DESTINATION,
+          selector = EventConstants.SELECTOR_PUT_APPLICATION_SIGNATURE
+  )
+  public void onSetApplicationSignature(
+          @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+          final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_PUT_APPLICATION_SIGNATURE, payload, ApplicationSignatureEvent.class);
+  }
+
+  @JmsListener(
+          subscription = EventConstants.DESTINATION,
+          destination = EventConstants.DESTINATION,
+          selector = EventConstants.SELECTOR_DELETE_APPLICATION
+  )
+  public void onDeleteApplication(
+          @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+          final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_DELETE_APPLICATION, payload, String.class);
+  }
+
+  @JmsListener(
+          subscription = EventConstants.DESTINATION,
+          destination = EventConstants.DESTINATION,
+          selector = EventConstants.SELECTOR_POST_APPLICATION_PERMISSION
+  )
+  public void onCreateApplicationPermission(
+          @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+          final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_POST_APPLICATION_PERMISSION, payload, ApplicationPermissionEvent.class);
+  }
+
+  @JmsListener(
+          subscription = EventConstants.DESTINATION,
+          destination = EventConstants.DESTINATION,
+          selector = EventConstants.SELECTOR_DELETE_APPLICATION_PERMISSION
+  )
+  public void onDeleteApplicationPermission(
+          @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+          final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_DELETE_APPLICATION_PERMISSION, payload, ApplicationPermissionEvent.class);
+  }
+
+  @JmsListener(
+          subscription = EventConstants.DESTINATION,
+          destination = EventConstants.DESTINATION,
+          selector = EventConstants.SELECTOR_PUT_APPLICATION_PERMISSION_USER_ENABLED
+  )
+  public void onPutApplicationPermissionEnabledForUser(
+          @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+          final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_PUT_APPLICATION_PERMISSION_USER_ENABLED, payload, ApplicationPermissionUserEvent.class);
+  }
+
+  @JmsListener(
+          subscription = EventConstants.DESTINATION,
+          destination = EventConstants.DESTINATION,
+          selector = EventConstants.SELECTOR_POST_APPLICATION_CALLENDPOINTSET
+  )
+  public void onCreateApplicationEndpointSet(
+          @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+          final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_POST_APPLICATION_CALLENDPOINTSET, payload, ApplicationCallEndpointSetEvent.class);
+  }
+
+  @JmsListener(
+          subscription = EventConstants.DESTINATION,
+          destination = EventConstants.DESTINATION,
+          selector = EventConstants.SELECTOR_PUT_APPLICATION_CALLENDPOINTSET
+  )
+  public void onSetApplicationEndpointSet(
+          @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+          final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_PUT_APPLICATION_CALLENDPOINTSET, payload, ApplicationCallEndpointSetEvent.class);
+  }
+
+  @JmsListener(
+          subscription = EventConstants.DESTINATION,
+          destination = EventConstants.DESTINATION,
+          selector = EventConstants.SELECTOR_DELETE_APPLICATION_CALLENDPOINTSET
+  )
+  public void onDeleteApplicationEndpointSet(
+          @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+          final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_DELETE_APPLICATION_CALLENDPOINTSET, payload, ApplicationCallEndpointSetEvent.class);
+  }
+}
\ No newline at end of file
diff --git a/service/src/test/java/listener/AuthenticationEventListener.java b/service/src/test/java/listener/AuthenticationEventListener.java
new file mode 100644
index 0000000..7d62ad1
--- /dev/null
+++ b/service/src/test/java/listener/AuthenticationEventListener.java
@@ -0,0 +1,54 @@
+/*
+ * 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 listener;
+
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.apache.fineract.cn.lang.config.TenantHeaderFilter;
+import org.apache.fineract.cn.test.listener.EventRecorder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Component
+public class AuthenticationEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public AuthenticationEventListener(@SuppressWarnings("SpringJavaAutowiringInspection") final EventRecorder eventRecorder)
+  {
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_AUTHENTICATE
+  )
+  public void onAuthentication(
+      @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+      final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_AUTHENTICATE, payload, String.class);
+  }
+}
diff --git a/service/src/test/java/listener/PermittableGroupEventListener.java b/service/src/test/java/listener/PermittableGroupEventListener.java
new file mode 100644
index 0000000..94ce73b
--- /dev/null
+++ b/service/src/test/java/listener/PermittableGroupEventListener.java
@@ -0,0 +1,54 @@
+/*
+ * 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 listener;
+
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.apache.fineract.cn.lang.config.TenantHeaderFilter;
+import org.apache.fineract.cn.test.listener.EventRecorder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Component
+public class PermittableGroupEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public PermittableGroupEventListener(@SuppressWarnings("SpringJavaAutowiringInspection") final EventRecorder eventRecorder)
+  {
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+          subscription = EventConstants.DESTINATION,
+          destination = EventConstants.DESTINATION,
+          selector = EventConstants.SELECTOR_POST_PERMITTABLE_GROUP
+  )
+  public void onCreatePermittableGroup(
+          @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+          final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_POST_PERMITTABLE_GROUP, payload, String.class);
+  }
+}
diff --git a/service/src/test/java/listener/RoleEventListener.java b/service/src/test/java/listener/RoleEventListener.java
new file mode 100644
index 0000000..d1618f3
--- /dev/null
+++ b/service/src/test/java/listener/RoleEventListener.java
@@ -0,0 +1,76 @@
+/*
+ * 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 listener;
+
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.apache.fineract.cn.lang.config.TenantHeaderFilter;
+import org.apache.fineract.cn.test.listener.EventRecorder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Component
+public class RoleEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public RoleEventListener(@SuppressWarnings("SpringJavaAutowiringInspection") final EventRecorder eventRecorder)
+  {
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_POST_ROLE
+  )
+  public void onCreateRole(
+      @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+      final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_POST_ROLE, payload, String.class);
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_PUT_ROLE
+  )
+  public void onChangeRole(
+      @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+      final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_PUT_ROLE, payload, String.class);
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_DELETE_ROLE
+  )
+  public void onDeleteRole(
+      @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+      final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_DELETE_ROLE, payload, String.class);
+  }
+}
diff --git a/service/src/test/java/listener/UserEventListener.java b/service/src/test/java/listener/UserEventListener.java
new file mode 100644
index 0000000..5be5e57
--- /dev/null
+++ b/service/src/test/java/listener/UserEventListener.java
@@ -0,0 +1,76 @@
+/*
+ * 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 listener;
+
+import org.apache.fineract.cn.identity.api.v1.events.EventConstants;
+import org.apache.fineract.cn.lang.config.TenantHeaderFilter;
+import org.apache.fineract.cn.test.listener.EventRecorder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.messaging.handler.annotation.Header;
+import org.springframework.stereotype.Component;
+
+/**
+ * @author Myrle Krantz
+ */
+@SuppressWarnings("unused")
+@Component
+public class UserEventListener {
+
+  private final EventRecorder eventRecorder;
+
+  @Autowired
+  public UserEventListener(@SuppressWarnings("SpringJavaAutowiringInspection") final EventRecorder eventRecorder)
+  {
+    this.eventRecorder = eventRecorder;
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_POST_USER
+  )
+  public void onCreateUser(
+      @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+      final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_POST_USER, payload, String.class);
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_PUT_USER_ROLEIDENTIFIER
+  )
+  public void onChangeUserRole(
+      @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+      final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_PUT_USER_ROLEIDENTIFIER, payload, String.class);
+  }
+
+  @JmsListener(
+      subscription = EventConstants.DESTINATION,
+      destination = EventConstants.DESTINATION,
+      selector = EventConstants.SELECTOR_PUT_USER_PASSWORD
+  )
+  public void onChangeUserPassword(
+      @Header(TenantHeaderFilter.TENANT_HEADER)final String tenant,
+      final String payload) throws Exception {
+    eventRecorder.event(tenant, EventConstants.OPERATION_PUT_USER_PASSWORD, payload, String.class);
+  }
+}


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services