From f098b470ee98ecd478845cd0442a141e6a9b3a54 Mon Sep 17 00:00:00 2001
From: Yannick Lecaillez <yannick.lecaillez@forgerock.com>
Date: Wed, 22 Jun 2016 10:02:34 +0000
Subject: [PATCH] OPENDJ-3015: Oauth2 HTTP client must support HTTPS

---
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java                |   70 ++--
 opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties     |    2 
 opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json       |  669 +++++++++++++++++++++++++++----------------
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java                    |    9 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java |  105 ++++++
 5 files changed, 569 insertions(+), 286 deletions(-)

diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
index 460d55b..5b16ad3 100644
--- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
+++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
@@ -1,9 +1,47 @@
 {
+    "security": {
+        // Specifies the policy for trusting server certificates exchanged
+        // during SSL/StartTLS negotiation. This setting and the following
+        // trust policy settings will be ignored if there is no connection
+        // security. Acceptable values are:
+        //
+        // "trustAll" - blindly trust all server certificates
+        // "jvm"      - only certificates signed by the authorities
+        //              associated with the host JVM will be accepted (default)
+        // "file"     - use a file-based trust store for validating
+        //              certificates. This option requires the following
+        //              "fileBasedTrustManager*" settings to be configured.
+        //
+        "trustManager": "jvm",
+
+        // File based trust manager configuration (see above).
+        "fileBasedTrustManagerType": "JKS",
+        "fileBasedTrustManagerFile": "/path/to/truststore",
+        "fileBasedTrustManagerPasswordFile": "/path/to/pinfile",
+
+        // Specifies the key-manager for authenticating the http client performing
+        // the request against the access-token resolver endpoint. Acceptable values are:
+        //
+        // "jvm"      - use the JVM's default keystore for retrieving certificates. (default)
+        // "keystore" - use the named key store file for retrieving certificates.
+        // "pkcs11"   - use a PKCS#11 token for retrieving certificates.
+        "keyManager": "jvm",
+
+        // Keystore based key manager configuration (see above).
+        "keyStoreFile": "/path/to/keystore",
+        "keyStorePasswordFile": "/path/to/pinfile",
+        "keyStoreFormat": "JKS",
+        "keyStoreProvider": "",
+
+        // PKCS11 based key manager configuration
+        "pkcs11PasswordFile": "/path/to/pinfile"
+    },
+
     // The array of connection factories which will be used by the Rest2LDAP
     // Servlet and authentication filter.
-    "ldapConnectionFactories" : {
+    "ldapConnectionFactories": {
         // Unauthenticated connections used for performing bind requests.
-        "bind" : {
+        "bind": {
             // Indicates whether LDAP connections should be secured using
             // SSL or StartTLS. Acceptable values are:
             //
@@ -11,44 +49,27 @@
             // "ssl"      - secure connection using LDAPS
             // "startTLS" - secure connection using LDAP+StartTLS
             //
-            "connectionSecurity"       : "none",
+            "connectionSecurity": "none",
 
-            // Specifies the policy for trusting server certificates exchanged
-            // during SSL/StartTLS negotiation. This setting and the following
-            // trust policy settings will be ignored if there is no connection
-            // security. Acceptable values are:
-            //
-            // "trustAll" - blindly trust all server certificates (default)
-            // "jvm"      - only certificates signed by the authorities
-            //              associated with the host JVM will be accepted
-            // "file"     - use a file-based trust store for validating
-            //              certificates. This option requires the following
-            //              "fileBasedTrustManager*" settings to be configured.
-            //
-            "trustManager"             : "trustAll",
-
-            // File based trust manager configuration (see above).
-            "fileBasedTrustManagerType"     : "JKS",
-            "fileBasedTrustManagerFile"     : "/path/to/truststore",
-            "fileBasedTrustManagerPassword" : "password",
+            // This alias points at an existing certificate that is used for SSL authentication for secure 
+            // communication between this gateway and the remote LDAP server.
+            "sslCertAlias": "client-cert",
 
             // Re-usable pool of 24 connections per server.
-            "connectionPoolSize"       : 24,
+            "connectionPoolSize": 24,
 
             // Check pooled connections are alive every 30 seconds with a 500ms
             // heart beat timeout.
-            "heartBeatIntervalSeconds"    : 30,
-            "heartBeatTimeoutMilliSeconds" : 500,
+            "heartBeatIntervalSeconds": 30,
+            "heartBeatTimeoutMilliSeconds": 500,
 
             // The preferred load-balancing pool.
-            "primaryLDAPServers"       : [
-                {
-                    "hostname" : "localhost",
-                    "port"     : 1389
-                }
-            ],
+            "primaryLDAPServers": [{
+                "hostname": "localhost",
+                "port": 1389
+            }],
             // The fail-over load-balancing pool (optional).
-            "secondaryLDAPServers"     : [
+            "secondaryLDAPServers": [
                 // Empty.
             ]
         },
@@ -56,25 +77,25 @@
         // Authenticated connections which will be used for searches during
         // authentication and proxied operations (if enabled). This factory
         // will re-use the server "bind" configuration.
-        "root" : {
-            "inheritFrom"    : "bind",
+        "root": {
+            "inheritFrom": "bind",
 
             // Defines how authentication should be performed. Only "simple"
             // authentication is supported at the moment.
             // If the OAuth 2.0 authorization policy is configured below,
             // then the directory service must be configured
             // to allow the user configured here to perform proxied authorization.
-            "authentication" : {
-                "simple" : {
-                    "bindDN"       : "cn=directory manager",
-                    "bindPassword" : "password"
+            "authentication": {
+                "simple": {
+                    "bindDN": "cn=directory manager",
+                    "bindPassword": "password"
                 }
             }
         }
     },
 
     "authorization": {
-		// The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2".
+        // The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2".
         "policies": [ "basic" ],
 
         // Perform all operations using a pre-authorization connection. 
@@ -84,77 +105,77 @@
             "ldapConnectionFactory": "root"
         },
 
-		// Use HTTP Basic authentication's information to bind to the LDAP server.
-		"basic": {
-			// Indicates whether the filter should allow alternative authentication
-			// and, if so, which HTTP headers it should obtain the username and
-			// password from.
-			"supportAltAuthentication"        : true,
-			"altAuthenticationUsernameHeader" : "X-OpenIDM-Username",
-			"altAuthenticationPasswordHeader" : "X-OpenIDM-Password",
+        // Use HTTP Basic authentication's information to bind to the LDAP server.
+        "basic": {
+            // Indicates whether the filter should allow alternative authentication
+            // and, if so, which HTTP headers it should obtain the username and
+            // password from.
+            "supportAltAuthentication": true,
+            "altAuthenticationUsernameHeader": "X-OpenIDM-Username",
+            "altAuthenticationPasswordHeader": "X-OpenIDM-Password",
 
-			// Define which LDAP bind mechanism to use
-			// Supported mechanisms are "simple", "sasl-plain", "search"
-			"bind": "search",
+            // Define which LDAP bind mechanism to use
+            // Supported mechanisms are "simple", "sasl-plain", "search"
+            "bind": "search",
 
-			// Bind to the LDAP server using the DN built from the HTTP Basic's username
-			"simple": {
-				// Connection factory used to perform the bind operation.
-				// If missing, "bind" factory will be used.
-				"ldapConnectionFactory": "bind",
+            // Bind to the LDAP server using the DN built from the HTTP Basic's username
+            "simple": {
+                // Connection factory used to perform the bind operation.
+                // If missing, "bind" factory will be used.
+                "ldapConnectionFactory": "bind",
 
-				// The Bind DN Template containing a single {username} which will be replaced by the authenticating
-				// user's name. (i.e: uid={username},ou=People,dc=example,dc=com)
-				// If missing, "{username}" is used.
-				"bindDNTemplate": "uid={username},ou=People,dc=example,dc=com"
-			},
+                // The Bind DN Template containing a single {username} which will be replaced by the authenticating
+                // user's name. (i.e: uid={username},ou=People,dc=example,dc=com)
+                // If missing, "{username}" is used.
+                "bindDNTemplate": "uid={username},ou=People,dc=example,dc=com"
+            },
 
-			// Bind to the LDAP server using a SASL Plain request
-			"sasl-plain": {
-				// Connection factory used to perform the bind operation.
-				// If missing, "bind" factory will be used.
-			    "ldapConnectionFactory": "bind",
+            // Bind to the LDAP server using a SASL Plain request
+            "sasl-plain": {
+                // Connection factory used to perform the bind operation.
+                // If missing, "bind" factory will be used.
+                "ldapConnectionFactory": "bind",
 
-				// Authentication identity template containing a single {username} which will be replaced by the authenticating
+                // Authentication identity template containing a single {username} which will be replaced by the authenticating
                 // user's name. (i.e: u:{username})
-				"authzIdTemplate": "u:{username}"
-			},
-			
-			// Bind to the LDAP server using the resulting DN of a search request. 
-			"search": {
-				// Connection factory used to perform the search operation.
-				// If missing, "root" factory will be used.
-				"searchLDAPConnectionFactory": "root",
-				
-				// Connection factory used to perform the bind operation.
-				// If missing, "bind" factory will be used.
-				"bindLDAPConnectionFactory": "bind",
-			
+                "authzIdTemplate": "u:{username}"
+            },
+
+            // Bind to the LDAP server using the resulting DN of a search request. 
+            "search": {
+                // Connection factory used to perform the search operation.
+                // If missing, "root" factory will be used.
+                "searchLDAPConnectionFactory": "root",
+
+                // Connection factory used to perform the bind operation.
+                // If missing, "bind" factory will be used.
+                "bindLDAPConnectionFactory": "bind",
+
                 // The {username} filter format parameters will be substituted with the client-provided username,
                 // using LDAP filter string character escaping.
-                "baseDN"         : "ou=people,dc=example,dc=com",
-                "scope"          : "sub", // Or "one".
-                "filterTemplate" : "(&(uid={username})(objectClass=inetOrgPerson))"
-			}
-			// TODO: support for HTTP sessions?
-		},
+                "baseDN": "ou=people,dc=example,dc=com",
+                "scope": "sub", // Or "one".
+                "filterTemplate": "(&(uid={username})(objectClass=inetOrgPerson))"
+            }
+            // TODO: support for HTTP sessions?
+        },
 
         // Use OAuth2 authorization method. If used, LDAP requests will be performed with proxied authorization control.
         // This field is optional.
         "oauth2": {
-			// Access tokens associated realm.
-			// This attribute is optional and has a string syntax.
-			"realm": "myrealm",
+            // Access tokens associated realm.
+            // This attribute is optional and has a string syntax.
+            "realm": "myrealm",
 
             // Defines the list of required scopes required to access the service.
             // This field is required and cannot be empty.
             "requiredScopes": [ "read", "write", "uid" ],
 
-			// Specify the resolver to use to resolve OAuth2 access token.
-			// This attribute is required and its value must be one of "openam", "rfc7662", "cts".
-			// Note that the JSON object corresponding to this attribute value must be present
-			// and well formed in the "oauth2" JSON attribute.
-			"resolver": "openam",
+            // Specify the resolver to use to resolve OAuth2 access token.
+            // This attribute is required and its value must be one of "openam", "rfc7662", "cts".
+            // Note that the JSON object corresponding to this attribute value must be present
+            // and well formed in the "oauth2" JSON attribute.
+            "resolver": "openam",
 
             // Configures caching of access token introspection results.
             // This attribute is optional, if it is not present, no token caching will be performed.
@@ -180,17 +201,21 @@
                 // This attribute is required and must have a string syntax.
                 "endpointURL": "http://openam.example.com:8080/openam/oauth2/tokeninfo",
 
-				// The default authzIdTemplate demonstrates how an authorization DN may be constructed
-				// from the "uid" field in the following example OpenAM tokeninfo response:
-				// {
-				//     "scope":["uid"],
-				//     "realm":"/",
-				//     "expires_in":45,
-				//     "uid" : "bjensen",
-			    // }
-				// This attribute is required and has a string syntax.
-				// It must start with either 'dn:' or 'u:'.
-				"authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com"
+                // This alias points at an existing certificate that is used for SSL authentication for secure 
+                // communication between this gateway and the OpenAM access-token resolver.
+                "sslCertAlias": "client-cert",
+
+                // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+                // from the "uid" field in the following example OpenAM tokeninfo response:
+                // {
+                //     "scope":["uid"],
+                //     "realm":"/",
+                //     "expires_in":45,
+                //     "uid" : "bjensen",
+                // }
+                // This attribute is required and has a string syntax.
+                // It must start with either 'dn:' or 'u:'.
+                "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com"
             },
 
             // The RFC-7662 (see https://tools.ietf.org/html/rfc7662) access token resolver configuration.
@@ -201,52 +226,56 @@
                 // This attribute is required and must have a string syntax.
                 "endpointURL": "http://openam.example.com:8080/openam/oauth2/myrealm/introspect",
 
-				// Token introspect endpoint requires authentication.
-				// It should support HTTP basic authorization (a base64-encoded string of clientId:clientSecret)
-				// These attributes are mandatory.
-				"clientId": "client_id",
-				"clientSecret": "client_secret",
+                // This alias points at an existing certificate that is used for SSL authentication for secure 
+                // communication between this gateway and the introspection access-token resolver.
+                "sslCertAlias": "client-cert",
 
-				// The default authzIdTemplate demonstrates how an authorization DN may be constructed
-				// from the "username" field in the following example introspect response:
-				// {
-				//     "active": true,
-				//     "token_type": "access_token",
-				//     "exp": 3524,
-				//     "username" : "bjensen",
-				// }
-				// This attribute is required and has a string syntax.
-				// It must start with either 'dn:' or 'u:'.
-				"authzIdTemplate": "dn:uid={username},ou=People,dc=example,dc=com"
+                // Token introspect endpoint requires authentication.
+                // It should support HTTP basic authorization (a base64-encoded string of clientId:clientSecret)
+                // These attributes are mandatory.
+                "clientId": "client_id",
+                "clientSecret": "client_secret",
+
+                // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+                // from the "username" field in the following example introspect response:
+                // {
+                //     "active": true,
+                //     "token_type": "access_token",
+                //     "exp": 3524,
+                //     "username" : "bjensen",
+                // }
+                // This attribute is required and has a string syntax.
+                // It must start with either 'dn:' or 'u:'.
+                "authzIdTemplate": "dn:uid={username},ou=People,dc=example,dc=com"
             },
 
             // The CTS access token resolver.
             // This attribute must be present if the "oauth2/resolver" is equal to "cts".
             // If "oauth2/resolver" is set to another resolver, this attribute will be ignored.
-			// Note: You can use {userName/0} in authzIdTemplate configuration to access
-			//       user id from the default CTS access token content config.
+            // Note: You can use {userName/0} in authzIdTemplate configuration to access
+            // user id from the default CTS access token content config.
             "cts": {
-				// The connection factory to use to access CTS.
-				// This value is only used in gateway mode.
-				// This attribute must reference a connection factory defined in the "ldapConnectionFactories" section.
-				// Default value: "root" (i.e the root connection factory will be used to access the CTS).
-				"ldapConnectionFactory": "root",
+                // The connection factory to use to access CTS.
+                // This value is only used in gateway mode.
+                // This attribute must reference a connection factory defined in the "ldapConnectionFactories" section.
+                // Default value: "root" (i.e the root connection factory will be used to access the CTS).
+                "ldapConnectionFactory": "root",
 
                 // The access token base DN.
                 // This attribute is required and must have a string syntax.
                 "baseDN": "ou=famrecords,ou=openam-session,ou=tokens,dc=example,dc=com",
 
-				// The default authzIdTemplate demonstrates how an authorization DN may be constructed
-				// from the "userName" field in the following example CTS access token entry:
-				// {
-				//     "active": true,
-				//     "tokenName": ["access_token"],
-				//     "exp": [3524],
-				//     "userName" : ["bjensen"],
-				// }
-				// This attribute is required and has a string syntax.
-				// It must start with either 'dn:' or 'u:'.
-				"authzIdTemplate": "dn:uid={userName/0},ou=People,dc=example,dc=com"
+                // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+                // from the "userName" field in the following example CTS access token entry:
+                // {
+                //     "active": true,
+                //     "tokenName": ["access_token"],
+                //     "exp": [3524],
+                //     "userName" : ["bjensen"],
+                // }
+                // This attribute is required and has a string syntax.
+                // It must start with either 'dn:' or 'u:'.
+                "authzIdTemplate": "dn:uid={userName/0},ou=People,dc=example,dc=com"
             },
 
             // ONLY FOR TEST PURPOSE: A File based access token resolver
@@ -257,125 +286,263 @@
                 // You can test the rest2ldap OAuth2 authorization support by providing some json token files under
                 // the directory set in the configuration below.
                 // File names must be equal to the token strings.
-				// The file content must a JSON object with the following attributes:
-				// 'scope', 'expireTime' and all the field(s) needed to resolve the authzIdTemplate.
+                // The file content must a JSON object with the following attributes:
+                // 'scope', 'expireTime' and all the field(s) needed to resolve the authzIdTemplate.
                 "folderPath": "/path/to/test/folder",
 
-				// The default authzIdTemplate demonstrates how an authorization DN may be constructed
-				// from the "uid" field extracted from a fake token file:
-				// {
-				//      "scope": ["read", "uid", "write"],
-				//      "expireTime": 1961336698000,
-				//      "uid": "bjensen"
-			    // }
-				// This attribute is required and has a string syntax.
-				// It must start with either 'dn:' or 'u:'.
-				"authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com"
+                // The default authzIdTemplate demonstrates how an authorization DN may be constructed
+                // from the "uid" field extracted from a fake token file:
+                // {
+                //     "scope": ["read", "uid", "write"],
+                //     "expireTime": 1961336698000,
+                //     "uid": "bjensen"
+                // }
+                // This attribute is required and has a string syntax.
+                // It must start with either 'dn:' or 'u:'.
+                "authzIdTemplate": "dn:uid={uid},ou=People,dc=example,dc=com"
             }
         }
     },
 
 
-	// The REST APIs and their LDAP attribute mappings.
-	"mappings" : {
-	    "/users" : {
-			"baseDN"              : "ou=people,dc=example,dc=com",
-			"readOnUpdatePolicy"  : "controls",
-			"useSubtreeDelete"    : false,
-			"usePermissiveModify" : true,
-			"etagAttribute"       : "etag",
-			"namingStrategy"      : {
-			    "strategy"    : "clientDNNaming",
-			    "dnAttribute" : "uid"
-			},
-			"additionalLDAPAttributes" : [
-			    {
-					"type" : "objectClass",
-					"values" : [
-					    "top",
-					    "person",
-					    "organizationalPerson",
-				    	"inetOrgPerson"
-					]
-			    }
-			],
-			"attributes" : {
-			    "schemas"     : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
-			    "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
-			    "_rev"        : { "simple"   : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
-			    "userName"    : { "simple"   : { "ldapAttribute" : "mail", "isSingleValued" : true, "writability" : "readOnly" } },
-			    "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true } },
-			    "name"        : { "object"   : {
-					"givenName"  : { "simple" : { "ldapAttribute" : "givenName", "isSingleValued" : true } },
-					"familyName" : { "simple" : { "ldapAttribute" : "sn", "isSingleValued" : true, "isRequired" : true } }
-			    } },
-			    "manager"     : { "reference" : {
-					"ldapAttribute" : "manager",
-					"baseDN"        : "ou=people,dc=example,dc=com",
-					"primaryKey"    : "uid",
-					"mapper"         : { "object" : {
-					    "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
-					    "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
-					} }
-			    } },
-			    "groups"     : { "reference" : {
-					"ldapAttribute" : "isMemberOf",
-					"baseDN"        : "ou=groups,dc=example,dc=com",
-					"writability"   : "readOnly",
-					"primaryKey"    : "cn",
-					"mapper"        : { "object" : {
-					    "_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true } }
-					} }
-			    } },
-			    "contactInformation" : { "object" : {
-					"telephoneNumber" : { "simple" : { "ldapAttribute" : "telephoneNumber", "isSingleValued" : true } },
-					"emailAddress"    : { "simple" : { "ldapAttribute" : "mail", "isSingleValued" : true } }
-			    } },
-			    "meta"        : { "object" : {
-					"created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
-					"lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
-			    } }
-			}
-	    },
-	    "/groups" : {
-			"baseDN"              : "ou=groups,dc=example,dc=com",
-			"readOnUpdatePolicy"  : "controls",
-			"useSubtreeDelete"    : false,
-			"usePermissiveModify" : true,
-			"etagAttribute"       : "etag",
-			"namingStrategy"      : {
-				"strategy"    : "clientDNNaming",
-				"dnAttribute" : "cn"
-			},
-			"additionalLDAPAttributes" : [
-				{
-					"type" : "objectClass",
-					"values" : [
-						"top",
-						"groupOfUniqueNames"
-					]
-				}
-			],
-			"attributes" : {
-				"schemas"     : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
-				"_id"         : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
-				"_rev"        : { "simple"   : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
-				"displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true, "writability" : "readOnly" } },
-				"members"    : { "reference" : {
-					"ldapAttribute" : "uniqueMember",
-					"baseDN"        : "dc=example,dc=com",
-					"primaryKey"    : "uid",
-					"mapper"        : { "object" : {
-						"_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true } },
-						"displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "writability" : "readOnlyDiscardWrites" } }
-					} }
-				} },
-				"meta"        : { "object" : {
-					"created"      : { "simple" : { "ldapAttribute" : "createTimestamp", "isSingleValued" : true, "writability" : "readOnly" } },
-					"lastModified" : { "simple" : { "ldapAttribute" : "modifyTimestamp", "isSingleValued" : true, "writability" : "readOnly" } }
-				} }
-			}
-	    }
-	}
+    // The REST APIs and their LDAP attribute mappings.
+    "mappings": {
+        "/users": {
+            "baseDN": "ou=people,dc=example,dc=com",
+            "readOnUpdatePolicy": "controls",
+            "useSubtreeDelete": false,
+            "usePermissiveModify": true,
+            "etagAttribute": "etag",
+            "namingStrategy": {
+                "strategy": "clientDNNaming",
+                "dnAttribute": "uid"
+            },
+            "additionalLDAPAttributes": [{
+                "type": "objectClass",
+                "values": [
+                    "top",
+                    "person",
+                    "organizationalPerson",
+                    "inetOrgPerson"
+                ]
+            }],
+            "attributes": {
+                "schemas": {
+                    "constant": ["urn:scim:schemas:core:1.0"]
+                },
+                "_id": {
+                    "simple": {
+                        "ldapAttribute": "uid",
+                        "isSingleValued": true,
+                        "isRequired": true,
+                        "writability": "createOnly"
+                    }
+                },
+                "_rev": {
+                    "simple": {
+                        "ldapAttribute": "etag",
+                        "isSingleValued": true,
+                        "writability": "readOnly"
+                    }
+                },
+                "userName": {
+                    "simple": {
+                        "ldapAttribute": "mail",
+                        "isSingleValued": true,
+                        "writability": "readOnly"
+                    }
+                },
+                "displayName": {
+                    "simple": {
+                        "ldapAttribute": "cn",
+                        "isSingleValued": true,
+                        "isRequired": true
+                    }
+                },
+                "name": {
+                    "object": {
+                        "givenName": {
+                            "simple": {
+                                "ldapAttribute": "givenName",
+                                "isSingleValued": true
+                            }
+                        },
+                        "familyName": {
+                            "simple": {
+                                "ldapAttribute": "sn",
+                                "isSingleValued": true,
+                                "isRequired": true
+                            }
+                        }
+                    }
+                },
+                "manager": {
+                    "reference": {
+                        "ldapAttribute": "manager",
+                        "baseDN": "ou=people,dc=example,dc=com",
+                        "primaryKey": "uid",
+                        "mapper": {
+                            "object": {
+                                "_id": {
+                                    "simple": {
+                                        "ldapAttribute": "uid",
+                                        "isSingleValued": true,
+                                        "isRequired": true
+                                    }
+                                },
+                                "displayName": {
+                                    "simple": {
+                                        "ldapAttribute": "cn",
+                                        "isSingleValued": true,
+                                        "writability": "readOnlyDiscardWrites"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                "groups": {
+                    "reference": {
+                        "ldapAttribute": "isMemberOf",
+                        "baseDN": "ou=groups,dc=example,dc=com",
+                        "writability": "readOnly",
+                        "primaryKey": "cn",
+                        "mapper": {
+                            "object": {
+                                "_id": {
+                                    "simple": {
+                                        "ldapAttribute": "cn",
+                                        "isSingleValued": true
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                "contactInformation": {
+                    "object": {
+                        "telephoneNumber": {
+                            "simple": {
+                                "ldapAttribute": "telephoneNumber",
+                                "isSingleValued": true
+                            }
+                        },
+                        "emailAddress": {
+                            "simple": {
+                                "ldapAttribute": "mail",
+                                "isSingleValued": true
+                            }
+                        }
+                    }
+                },
+                "meta": {
+                    "object": {
+                        "created": {
+                            "simple": {
+                                "ldapAttribute": "createTimestamp",
+                                "isSingleValued": true,
+                                "writability": "readOnly"
+                            }
+                        },
+                        "lastModified": {
+                            "simple": {
+                                "ldapAttribute": "modifyTimestamp",
+                                "isSingleValued": true,
+                                "writability": "readOnly"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/groups": {
+            "baseDN": "ou=groups,dc=example,dc=com",
+            "readOnUpdatePolicy": "controls",
+            "useSubtreeDelete": false,
+            "usePermissiveModify": true,
+            "etagAttribute": "etag",
+            "namingStrategy": {
+                "strategy": "clientDNNaming",
+                "dnAttribute": "cn"
+            },
+            "additionalLDAPAttributes": [{
+                "type": "objectClass",
+                "values": [
+                    "top",
+                    "groupOfUniqueNames"
+                ]
+            }],
+            "attributes": {
+                "schemas": {
+                    "constant": ["urn:scim:schemas:core:1.0"]
+                },
+                "_id": {
+                    "simple": {
+                        "ldapAttribute": "cn",
+                        "isSingleValued": true,
+                        "isRequired": true,
+                        "writability": "createOnly"
+                    }
+                },
+                "_rev": {
+                    "simple": {
+                        "ldapAttribute": "etag",
+                        "isSingleValued": true,
+                        "writability": "readOnly"
+                    }
+                },
+                "displayName": {
+                    "simple": {
+                        "ldapAttribute": "cn",
+                        "isSingleValued": true,
+                        "isRequired": true,
+                        "writability": "readOnly"
+                    }
+                },
+                "members": {
+                    "reference": {
+                        "ldapAttribute": "uniqueMember",
+                        "baseDN": "dc=example,dc=com",
+                        "primaryKey": "uid",
+                        "mapper": {
+                            "object": {
+                                "_id": {
+                                    "simple": {
+                                        "ldapAttribute": "uid",
+                                        "isSingleValued": true,
+                                        "isRequired": true
+                                    }
+                                },
+                                "displayName": {
+                                    "simple": {
+                                        "ldapAttribute": "cn",
+                                        "isSingleValued": true,
+                                        "writability": "readOnlyDiscardWrites"
+                                    }
+                                }
+                            }
+                        }
+                    }
+                },
+                "meta": {
+                    "object": {
+                        "created": {
+                            "simple": {
+                                "ldapAttribute": "createTimestamp",
+                                "isSingleValued": true,
+                                "writability": "readOnly"
+                            }
+                        },
+                        "lastModified": {
+                            "simple": {
+                                "ldapAttribute": "modifyTimestamp",
+                                "isSingleValued": true,
+                                "writability": "readOnly"
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
 }
-
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 4aa085f..967b0b3 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -31,8 +31,8 @@
 import static org.forgerock.opendj.rest2ldap.Utils.newLocalizedIllegalArgumentException;
 import static org.forgerock.opendj.rest2ldap.Utils.newJsonValueException;
 import static org.forgerock.util.time.Duration.*;
+import static org.forgerock.opendj.ldap.KeyManagers.useSingleCertificate;
 
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
@@ -42,6 +42,9 @@
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
+
 import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.CollectionResourceProvider;
 import org.forgerock.json.resource.ResourceException;
@@ -68,7 +71,6 @@
 import org.forgerock.opendj.ldap.SSLContextBuilder;
 import org.forgerock.opendj.ldap.SearchScope;
 import org.forgerock.opendj.ldap.TimeoutResultException;
-import org.forgerock.opendj.ldap.TrustManagers;
 import org.forgerock.opendj.ldap.requests.BindRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
@@ -88,10 +90,18 @@
      * Specifies the mechanism which should be used for trusting certificates
      * presented by the LDAP server.
      */
-    private enum TrustManagerType {
+    enum TrustManagerType {
         TRUSTALL, JVM, FILE
     }
 
+    /**
+     * Specifies the mechanism which manage which X509 certificate-based key pairs should be used to authenticate the
+     * local side of a secure socket.
+     */
+    enum KeyManagerType {
+        JVM, KEYSTORE, PKCS11
+    }
+
     /** A builder for incrementally constructing LDAP resource collections. */
     public static final class Builder {
         private final List<Attribute> additionalLDAPAttributes = new LinkedList<>();
@@ -784,6 +794,12 @@
      *            The JSON configuration.
      * @param name
      *            The name of the connection factory configuration to be parsed.
+     * @param trustManager
+     *            The trust manager to use for secure connection. Can be {@code null}
+     *            to use the default JVM trust manager.
+     * @param keyManager
+     *            The key manager to use for secure connection. Can be {@code null}
+     *            to use the default JVM key manager.
      * @param providerClassLoader
      *            The {@link ClassLoader} used to fetch the
      *            {@link org.forgerock.opendj.ldap.spi.TransportProvider}.
@@ -794,10 +810,12 @@
      */
     public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
                                                                final String name,
+                                                               final TrustManager trustManager,
+                                                               final X509KeyManager keyManager,
                                                                final ClassLoader providerClassLoader) {
         final JsonValue normalizedConfiguration =
                 normalizeConnectionFactory(configuration, name, 0);
-        return configureConnectionFactory(normalizedConfiguration, providerClassLoader);
+        return configureConnectionFactory(normalizedConfiguration, trustManager, keyManager, providerClassLoader);
     }
 
     /**
@@ -809,13 +827,19 @@
      *            The JSON configuration.
      * @param name
      *            The name of the connection factory configuration to be parsed.
+     * @param trustManager
+     *            The trust manager to use for secure connection. Can be {@code null}
+     *            to use the default JVM trust manager.
+     * @param keyManager
+     *            The key manager to use for secure connection. Can be {@code null}
+     *            to use the default JVM key manager.
      * @return A new connection factory using the provided JSON configuration.
      * @throws IllegalArgumentException
      *             If the configuration is invalid.
      */
     public static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
-            final String name) {
-        return configureConnectionFactory(configuration, name, null);
+            final String name, final TrustManager trustManager, final X509KeyManager keyManager) {
+        return configureConnectionFactory(configuration, name, trustManager, keyManager, null);
     }
 
     /**
@@ -908,6 +932,8 @@
     }
 
     private static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
+                                                                final TrustManager trustManager,
+                                                                final X509KeyManager keyManager,
                                                                 final ClassLoader providerClassLoader) {
         final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong();
         final Duration heartBeatInterval = duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS);
@@ -948,32 +974,14 @@
             try {
                 // Configure SSL.
                 final SSLContextBuilder builder = new SSLContextBuilder();
-
-                // Parse trust store configuration.
-                final TrustManagerType trustManagerType =
-                        configuration.get("trustManager").defaultTo(TrustManagerType.TRUSTALL)
-                                .asEnum(TrustManagerType.class);
-                switch (trustManagerType) {
-                case TRUSTALL:
-                    builder.setTrustManager(TrustManagers.trustAll());
-                    break;
-                case JVM:
-                    // Do nothing: JVM trust manager is the default.
-                    break;
-                case FILE:
-                    final String fileName =
-                            configuration.get("fileBasedTrustManagerFile").required().asString();
-                    final String password =
-                            configuration.get("fileBasedTrustManagerPassword").asString();
-                    final String type = configuration.get("fileBasedTrustManagerType").asString();
-                    builder.setTrustManager(TrustManagers.checkUsingTrustStore(fileName,
-                            password != null ? password.toCharArray() : null, type));
-                    break;
-                }
+                builder.setTrustManager(trustManager);
+                final String sslCertAlias = configuration.get("sslCertAlias").asString();
+                builder.setKeyManager(sslCertAlias != null
+                        ? useSingleCertificate(sslCertAlias, keyManager)
+                        : keyManager);
                 options.set(SSL_CONTEXT, builder.getSSLContext());
-                options.set(SSL_USE_STARTTLS,
-                            connectionSecurity == ConnectionSecurity.STARTTLS);
-            } catch (GeneralSecurityException | IOException e) {
+                options.set(SSL_USE_STARTTLS, connectionSecurity == ConnectionSecurity.STARTTLS);
+            } catch (GeneralSecurityException e) {
                 // Rethrow as unchecked exception.
                 throw new IllegalArgumentException(e);
             }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
index a59c1a7..5727ba4 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
@@ -17,6 +17,10 @@
 package org.forgerock.opendj.rest2ldap;
 
 import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
+import static org.forgerock.http.handler.HttpClientHandler.*;
+import static org.forgerock.opendj.ldap.KeyManagers.*;
+import static org.forgerock.opendj.ldap.TrustManagers.checkUsingTrustStore;
+import static org.forgerock.opendj.ldap.TrustManagers.trustAll;
 import static org.forgerock.http.util.Json.readJsonLenient;
 import static org.forgerock.json.JsonValueFunctions.duration;
 import static org.forgerock.json.JsonValueFunctions.enumConstant;
@@ -31,12 +35,14 @@
 import static org.forgerock.util.Reject.checkNotNull;
 import static org.forgerock.util.Utils.closeSilently;
 import static org.forgerock.util.Utils.joinAsString;
+import static org.forgerock.opendj.rest2ldap.Utils.readPasswordFromFile;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -46,6 +52,10 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509KeyManager;
+
 import org.forgerock.openig.oauth2.AccessTokenInfo;
 import org.forgerock.openig.oauth2.AccessTokenException;
 import org.forgerock.openig.oauth2.AccessTokenResolver;
@@ -71,11 +81,14 @@
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.SearchScope;
 import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.rest2ldap.Rest2LDAP.KeyManagerType;
+import org.forgerock.opendj.rest2ldap.Rest2LDAP.TrustManagerType;
 import org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategy;
 import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter;
 import org.forgerock.services.context.SecurityContext;
 import org.forgerock.util.Factory;
 import org.forgerock.util.Function;
+import org.forgerock.util.Options;
 import org.forgerock.util.Pair;
 import org.forgerock.util.PerItemEvictionStrategyCache;
 import org.forgerock.util.annotations.VisibleForTesting;
@@ -113,6 +126,9 @@
     /** Used for token caching. */
     private ScheduledExecutorService executorService;
 
+    private TrustManager trustManager;
+    private X509KeyManager keyManager;
+
     /** Define the method which should be used to resolve an OAuth2 access token. */
     private enum OAuth2ResolverType {
         RFC7662, OPENAM, CTS, FILE;
@@ -175,6 +191,7 @@
         try {
             final JsonValue configuration = readJson(configurationUrl);
             executorService = Executors.newSingleThreadScheduledExecutor();
+            configureSecurity(configuration.get("security"));
             configureConnectionFactories(configuration.get("ldapConnectionFactories"));
             return Handlers.chainOf(
                     CrestHttp.newHttpHandler(configureRest2Ldap(configuration)),
@@ -204,10 +221,76 @@
         return router;
     }
 
+    private void configureSecurity(final JsonValue configuration) {
+        try {
+            trustManager = configureTrustManager(configuration, TrustManagerType.JVM);
+        } catch (GeneralSecurityException | IOException e) {
+            throw new IllegalArgumentException(ERR_CONFIG_INVALID_TRUST_MANAGER
+                    .get(configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
+        }
+
+        try {
+            keyManager = configureKeyManager(configuration, KeyManagerType.JVM);
+        } catch (GeneralSecurityException | IOException e) {
+            throw new IllegalArgumentException(ERR_CONFIG_INVALID_KEY_MANAGER
+                    .get(configuration.getPointer(), e.getLocalizedMessage()).toString(), e);
+        }
+    }
+
+    private TrustManager configureTrustManager(JsonValue config, TrustManagerType defaultIfMissing)
+            throws GeneralSecurityException, IOException {
+        // Parse trust store configuration.
+        final TrustManagerType trustManagerType =
+                config.get("trustManager").defaultTo(defaultIfMissing).as(enumConstant(TrustManagerType.class));
+        switch (trustManagerType) {
+        case TRUSTALL:
+            return trustAll();
+        case JVM:
+            return null;
+        case FILE:
+            final String fileName = config.get("fileBasedTrustManagerFile").required().asString();
+            final String passwordFile = config.get("fileBasedTrustManagerPasswordFile").asString();
+            final String password = passwordFile != null
+                    ? readPasswordFromFile(passwordFile)
+                    : config.get("fileBasedTrustManagerPassword").asString();
+            final String type = config.get("fileBasedTrustManagerType").asString();
+            return checkUsingTrustStore(fileName, password != null ? password.toCharArray() : null, type);
+        default:
+            throw new IllegalArgumentException("Unsupported trust-manager type: " + trustManagerType);
+        }
+    }
+
+    private X509KeyManager configureKeyManager(JsonValue config, KeyManagerType defaultIfMissing)
+            throws GeneralSecurityException, IOException {
+        // Parse trust store configuration.
+        final KeyManagerType keyManagerType = config.get("keyManager").defaultTo(defaultIfMissing)
+                .as(enumConstant(KeyManagerType.class));
+        switch (keyManagerType) {
+        case JVM:
+            return useJvmDefaultKeyStore();
+        case KEYSTORE:
+            final String fileName = config.get("keyStoreFile").required().asString();
+            final String passwordFile = config.get("keyStorePasswordFile").asString();
+            final String password = passwordFile != null
+                    ? readPasswordFromFile(passwordFile)
+                    : config.get("keyStorePassword").asString();
+            final String format = config.get("keyStoreFormat").asString();
+            final String provider = config.get("keyStoreProvider").asString();
+            return useKeyStoreFile(fileName, password != null ? password.toCharArray() : null, format, provider);
+        case PKCS11:
+            final String pkcs11PasswordFile = config.get("pkcs11PasswordFile").asString();
+            return usePKCS11Token(pkcs11PasswordFile != null
+                    ? readPasswordFromFile(pkcs11PasswordFile).toCharArray()
+                    : null);
+        default:
+            throw new IllegalArgumentException("Unsupported key-manager type: " + keyManagerType);
+        }
+    }
+
     private void configureConnectionFactories(final JsonValue config) {
         connectionFactories.clear();
         for (String name : config.keys()) {
-            connectionFactories.put(name, configureConnectionFactory(config, name));
+            connectionFactories.put(name, configureConnectionFactory(config, name, trustManager, keyManager));
         }
     }
 
@@ -282,9 +365,10 @@
         case RFC7662:
             return parseRfc7662Resolver(configuration);
         case OPENAM:
-            return new OpenAmAccessTokenResolver(new HttpClientHandler(),
+            final JsonValue openAm = configuration.get("openam");
+            return new OpenAmAccessTokenResolver(newHttpClientHandler(openAm),
                                                  TimeService.SYSTEM,
-                                                 configuration.get("openam").get("endpointURL").required().asString());
+                                                 openAm.get("endpointURL").required().asString());
         case CTS:
             final JsonValue cts = configuration.get("cts").required();
             return newCtsAccessTokenResolver(
@@ -303,7 +387,7 @@
         final JsonValue rfc7662 = configuration.get("rfc7662").required();
         final String introspectionEndPointURL = rfc7662.get("endpointURL").required().asString();
         try {
-            return newRfc7662AccessTokenResolver(new HttpClientHandler(),
+            return newRfc7662AccessTokenResolver(newHttpClientHandler(rfc7662),
                                                  new URI(introspectionEndPointURL),
                                                  rfc7662.get("clientId").required().asString(),
                                                  rfc7662.get("clientSecret").required().asString());
@@ -313,6 +397,19 @@
         }
     }
 
+    private HttpClientHandler newHttpClientHandler(final JsonValue config) throws HttpApplicationException {
+        final Options httpOptions = Options.defaultOptions();
+        if (trustManager != null) {
+            httpOptions.set(OPTION_TRUST_MANAGERS, new TrustManager[] { trustManager });
+        }
+        if (keyManager != null) {
+            final String keyAlias = config.get("sslCertAlias").asString();
+            httpOptions.set(OPTION_KEY_MANAGERS,
+                    new KeyManager[] { keyAlias != null ? useSingleCertificate(keyAlias, keyManager) : keyManager });
+        }
+        return new HttpClientHandler(httpOptions);
+    }
+
     private Duration parseCacheExpiration(final JsonValue expirationJson) {
         try {
             final Duration expiration = expirationJson.as(duration());
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
index b67dda7..e7d155c 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -27,6 +27,10 @@
 import static org.forgerock.opendj.ldap.schema.CoreSchema.getGeneralizedTimeSyntax;
 import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax;
 
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -70,6 +74,11 @@
                 }
             };
 
+    static String readPasswordFromFile(String fileName) throws IOException {
+        try (final BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)))) {
+            return reader.readLine();
+        }
+    }
 
     static Object attributeToJson(final Attribute a) {
         final Function<ByteString, Object, NeverThrowsException> f = byteStringToJson(a.getAttributeDescription());
diff --git a/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties b/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
index 4a6f321..bc38afb 100644
--- a/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
+++ b/opendj-rest2ldap/src/main/resources/org/forgerock/opendj/rest2ldap/rest2ldap.properties
@@ -107,3 +107,5 @@
 ERR_RUNTIME_EXCEPTION_61=A runtime exception occurred wile processing the request '%s': '%s'
 ERR_PASSWORD_MODIFY_REQUEST_IS_INVALID_62=The password modify request has been rejected because it is invalid. \
  A password modify request may contain two string valued fields 'oldPassword' and 'newPassword'
+ERR_CONFIG_INVALID_TRUST_MANAGER_63=The trust-manager defined in '%s' is invalid: %s
+ERR_CONFIG_INVALID_KEY_MANAGER_64=The key-manager defined in '%s' is invalid: %s

--
Gitblit v1.10.0