killbill-aplcache

Details

diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
index efd0828..2ba796a 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AccountResource.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -121,6 +121,7 @@ import org.killbill.billing.util.callcontext.CallContext;
 import org.killbill.billing.util.callcontext.TenantContext;
 import org.killbill.billing.util.config.definition.JaxrsConfig;
 import org.killbill.billing.util.config.definition.PaymentConfig;
+import org.killbill.billing.util.customfield.CustomField;
 import org.killbill.billing.util.entity.Pagination;
 import org.killbill.billing.util.tag.ControlTagType;
 import org.killbill.billing.util.tag.Tag;
@@ -1190,6 +1191,25 @@ public class AccountResource extends JaxRsResourceBase {
     }
 
     @TimedResource
+    @GET
+    @Path("/{accountId:" + UUID_PATTERN + "}/" + ALL_CUSTOM_FIELDS)
+    @Produces(APPLICATION_JSON)
+    @ApiOperation(value = "Retrieve account customFields", response = CustomFieldJson.class, responseContainer = "List")
+    @ApiResponses(value = {@ApiResponse(code = 400, message = "Invalid account id supplied"),
+                           @ApiResponse(code = 404, message = "Account not found")})
+    public Response getAllCustomFields(@PathParam(ID_PARAM_NAME) final String accountIdString,
+                                       @QueryParam(QUERY_OBJECT_TYPE) final ObjectType objectType,
+                                       @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+                                       @javax.ws.rs.core.Context final HttpServletRequest request) {
+        final UUID accountId = UUID.fromString(accountIdString);
+        final TenantContext tenantContext = context.createContext(request);
+        final List<CustomField> customFields = objectType != null ?
+                                               customFieldUserApi.getCustomFieldsForAccountType(accountId, objectType, tenantContext) :
+                                               customFieldUserApi.getCustomFieldsForAccount(accountId, tenantContext);
+        return createCustomFieldResponse(customFields, auditMode, tenantContext);
+    }
+
+    @TimedResource
     @POST
     @Path("/{accountId:" + UUID_PATTERN + "}/" + CUSTOM_FIELDS)
     @Consumes(APPLICATION_JSON)
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
index 38a3986..5bb10b5 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxrsResource.java
@@ -221,6 +221,7 @@ public interface JaxrsResource {
     public static final String TAGS = "tags";
     public static final String TAGS_PATH = PREFIX + "/" + TAGS;
 
+    public static final String ALL_CUSTOM_FIELDS = "allCustomFields";
     public static final String CUSTOM_FIELDS = "customFields";
     public static final String CUSTOM_FIELDS_PATH = PREFIX + "/" + CUSTOM_FIELDS;
 
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index 1e139a7..97c9b8c 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -241,8 +241,11 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
 
     protected Response getCustomFields(final UUID id, final AuditMode auditMode, final TenantContext context) {
         final List<CustomField> fields = customFieldUserApi.getCustomFieldsForObject(id, getObjectType(), context);
+        return createCustomFieldResponse(fields, auditMode, context);
+    }
 
-        final List<CustomFieldJson> result = new LinkedList<CustomFieldJson>();
+    protected Response createCustomFieldResponse(final Iterable<CustomField> fields, final AuditMode auditMode, final TenantContext context) {
+        final Collection<CustomFieldJson> result = new LinkedList<CustomFieldJson>();
         for (final CustomField cur : fields) {
             // TODO PIERRE - Bulk API
             final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(cur.getId(), ObjectType.CUSTOM_FIELD, auditMode.getLevel(), context);

NEWS 3(+3 -0)

diff --git a/NEWS b/NEWS
index e3a0cb7..c1e7da5 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,6 @@
+0.18.13
+    See https://github.com/killbill/killbill/releases/tag/killbill-0.18.13
+
 0.18.12
     See https://github.com/killbill/killbill/releases/tag/killbill-0.18.12
 
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LoggingFilterObfuscator.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LoggingFilterObfuscator.java
new file mode 100644
index 0000000..536682f
--- /dev/null
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/LoggingFilterObfuscator.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.regex.Pattern;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import com.google.common.collect.ImmutableList;
+
+public class LoggingFilterObfuscator extends Obfuscator {
+
+    private static final String[] DEFAULT_SENSITIVE_HEADERS = {
+            "Authorization",
+            "X-Killbill-ApiSecret",
+    };
+
+    private final Collection<Pattern> patterns = new LinkedList<Pattern>();
+
+    public LoggingFilterObfuscator() {
+        this(ImmutableList.<Pattern>of());
+    }
+
+    public LoggingFilterObfuscator(final Collection<Pattern> extraPatterns) {
+        super();
+
+        for (final String sensitiveKey : DEFAULT_SENSITIVE_HEADERS) {
+            this.patterns.add(buildPattern(sensitiveKey));
+        }
+        this.patterns.addAll(extraPatterns);
+    }
+
+    @Override
+    public String obfuscate(final String originalString, final ILoggingEvent event) {
+        return obfuscate(originalString, patterns, event);
+    }
+
+    private Pattern buildPattern(final String key) {
+        return Pattern.compile("\\s*" + key + ":\\s*([^\\n]+)", DEFAULT_PATTERN_FLAGS);
+    }
+}
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
index 39b9306..6e522d6 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/log/obfuscators/ObfuscatorConverter.java
@@ -45,6 +45,7 @@ import com.google.common.collect.ImmutableList;
 public class ObfuscatorConverter extends ClassicConverter {
 
     private final Collection<Obfuscator> obfuscators = ImmutableList.<Obfuscator>of(new ConfigMagicObfuscator(),
+                                                                                    new LoggingFilterObfuscator(),
                                                                                     new PatternObfuscator(),
                                                                                     new LuhnMaskingObfuscator());
 
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
index 49057fb..eaf9e55 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/modules/KillBillShiroWebModule.java
@@ -44,6 +44,7 @@ import org.killbill.billing.util.glue.KillBillShiroModule;
 import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
 import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
 import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
+import org.killbill.billing.util.security.shiro.realm.KillBillOktaRealm;
 import org.skife.config.ConfigSource;
 import org.skife.config.ConfigurationObjectFactory;
 
@@ -87,6 +88,9 @@ public class KillBillShiroWebModule extends ShiroWebModuleWith435 {
         if (KillBillShiroModule.isLDAPEnabled()) {
             bindRealm().to(KillBillJndiLdapRealm.class).asEagerSingleton();
         }
+        if (KillBillShiroModule.isOktaEnabled()) {
+            bindRealm().to(KillBillOktaRealm.class).asEagerSingleton();
+        }
 
         bindListener(new AbstractMatcher<TypeLiteral<?>>() {
                          @Override
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java
index 66a3af7..123bb59 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCustomField.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2014 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
  *
  * The Billing Project 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
@@ -27,6 +27,7 @@ import org.killbill.billing.client.KillBillClientException;
 import org.killbill.billing.client.model.Account;
 import org.killbill.billing.client.model.CustomField;
 import org.killbill.billing.client.model.CustomFields;
+import org.killbill.billing.util.api.AuditLevel;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
@@ -39,13 +40,13 @@ public class TestCustomField extends TestJaxrsBase {
             final CustomField customField = new CustomField();
             customField.setName(UUID.randomUUID().toString().substring(0, 5));
             customField.setValue(UUID.randomUUID().toString().substring(0, 5));
-            killBillClient.createAccountCustomField(account.getAccountId(), customField, createdBy, reason, comment);
+            killBillClient.createAccountCustomField(account.getAccountId(), customField, requestOptions);
         }
 
-        final CustomFields allCustomFields = killBillClient.getCustomFields();
+        final CustomFields allCustomFields = killBillClient.getCustomFields(requestOptions);
         Assert.assertEquals(allCustomFields.size(), 5);
 
-        CustomFields page = killBillClient.getCustomFields(0L, 1L);
+        CustomFields page = killBillClient.getCustomFields(0L, 1L, requestOptions);
         for (int i = 0; i < 5; i++) {
             Assert.assertNotNull(page);
             Assert.assertEquals(page.size(), 1);
@@ -60,15 +61,21 @@ public class TestCustomField extends TestJaxrsBase {
             doSearchCustomField(customField.getValue(), customField);
         }
 
-        final CustomFields customFields = killBillClient.searchCustomFields(ObjectType.ACCOUNT.toString());
+        final CustomFields customFields = killBillClient.searchCustomFields(ObjectType.ACCOUNT.toString(), requestOptions);
         Assert.assertEquals(customFields.size(), 5);
         Assert.assertEquals(customFields.getPaginationCurrentOffset(), 0);
         Assert.assertEquals(customFields.getPaginationTotalNbRecords(), 5);
         Assert.assertEquals(customFields.getPaginationMaxNbRecords(), 5);
+
+        final CustomFields allAccountCustomFields = killBillClient.getAllAccountCustomFields(account.getAccountId(), null, AuditLevel.FULL, requestOptions);
+        Assert.assertEquals(allAccountCustomFields.size(), 5);
+
+        final CustomFields allBundleCustomFieldsForAccount = killBillClient.getAllAccountCustomFields(account.getAccountId(), ObjectType.ACCOUNT.name(), AuditLevel.FULL, requestOptions);
+        Assert.assertEquals(allBundleCustomFieldsForAccount.size(), 5);
     }
 
     private void doSearchCustomField(final String searchKey, @Nullable final CustomField expectedCustomField) throws KillBillClientException {
-        final CustomFields customFields = killBillClient.searchCustomFields(searchKey);
+        final CustomFields customFields = killBillClient.searchCustomFields(searchKey, requestOptions);
         if (expectedCustomField == null) {
             Assert.assertTrue(customFields.isEmpty());
             Assert.assertEquals(customFields.getPaginationCurrentOffset(), 0);
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTag.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTag.java
index 44a3f3d..5ba7ba8 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTag.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestTag.java
@@ -79,9 +79,6 @@ public class TestTag extends TestJaxrsBase {
         List<TagDefinition> objFromJson = killBillClient.getTagDefinitions(requestOptions);
         final int sizeSystemTag = objFromJson.isEmpty() ? 0 : objFromJson.size();
 
-        for (final TagDefinition cur : objFromJson) {
-            Assert.assertFalse(SystemTags.isSystemTag(cur.getId()));
-        }
 
         final TagDefinition inputBlue = new TagDefinition(null, false, "blue", "relaxing color", ImmutableList.<ObjectType>of());
         killBillClient.createTagDefinition(inputBlue, requestOptions);
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLoggingFilterObfuscator.java b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLoggingFilterObfuscator.java
new file mode 100644
index 0000000..76351fd
--- /dev/null
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/log/obfuscators/TestLoggingFilterObfuscator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.server.log.obfuscators;
+
+import org.killbill.billing.server.log.ServerTestSuiteNoDB;
+import org.mockito.Mockito;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+
+public class TestLoggingFilterObfuscator extends ServerTestSuiteNoDB {
+
+    private final LoggingFilterObfuscator obfuscator = new LoggingFilterObfuscator();
+
+    @Test(groups = "fast")
+    public void testAuthorization() throws Exception {
+        verify("2017-08-26T10:28:21,959+0000 lvl='INFO', log='LoggingFilter', th='qtp1071550332-34', xff='', rId='70394abe-7ab6-4b7c-aaf5-17abfcdb9622', aRId='', tRId='', 1 * Server in-bound request\n" +
+               "1 > GET http://127.0.0.1:8080/1.0/kb/security/permissions\n" +
+               "1 > User-Agent: killbill/1.9.0; jruby 9.1.12.0 (2.3.3) 2017-06-15 33c6439 Java HotSpot(TM) 64-Bit Server VM 25.121-b13 on 1.8.0_121-b13 +jit [darwin-x86_64]\n" +
+               "1 > Authorization: Basic YWRtaW46cGFzc3dvcmQ=\n" +
+               "1 > Host: 127.0.0.1:8080\n" +
+               "1 > Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\n" +
+               "1 > Accept: application/json\n" +
+               "1 >",
+               "2017-08-26T10:28:21,959+0000 lvl='INFO', log='LoggingFilter', th='qtp1071550332-34', xff='', rId='70394abe-7ab6-4b7c-aaf5-17abfcdb9622', aRId='', tRId='', 1 * Server in-bound request\n" +
+               "1 > GET http://127.0.0.1:8080/1.0/kb/security/permissions\n" +
+               "1 > User-Agent: killbill/1.9.0; jruby 9.1.12.0 (2.3.3) 2017-06-15 33c6439 Java HotSpot(TM) 64-Bit Server VM 25.121-b13 on 1.8.0_121-b13 +jit [darwin-x86_64]\n" +
+               "1 > Authorization: **************************\n" +
+               "1 > Host: 127.0.0.1:8080\n" +
+               "1 > Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\n" +
+               "1 > Accept: application/json\n" +
+               "1 >");
+    }
+
+    @Test(groups = "fast")
+    public void testApiSecret() throws Exception {
+        verify("2017-08-25T15:28:34,331+0000 lvl='INFO', log='LoggingFilter', th='qtp288887829-1845', xff='', rId='59c40009-ea68-4d87-9580-fe95e9a82c23', aRId='', tRId='11', 3896 * Server in-bound request\n" +
+               "3896 > GET http://127.0.0.1:8080/1.0/kb/paymentMethods/069a4daa-e752-486c-8e40-c9c4f9a732c4?withPluginInfo=true\n" +
+               "3896 > Cookie: JSESSIONID=64faafa1-da74-4ac7-afc7-947cc9871fe5\n" +
+               "3896 > X-Killbill-Apikey: bob\n" +
+               "3896 > Accept: application/json\n" +
+               "3896 > X-Request-Id: 59c40009-ea68-4d87-9580-fe95e9a82c23\n" +
+               "3896 > X-Killbill-Apisecret: lazar\n" +
+               "3896 > User-Agent: killbill/1.9.0; ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin16]\n" +
+               "3896 > Host: 127.0.0.1:8080\n" +
+               "3896 > Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\n" +
+               "3896 >",
+               "2017-08-25T15:28:34,331+0000 lvl='INFO', log='LoggingFilter', th='qtp288887829-1845', xff='', rId='59c40009-ea68-4d87-9580-fe95e9a82c23', aRId='', tRId='11', 3896 * Server in-bound request\n" +
+               "3896 > GET http://127.0.0.1:8080/1.0/kb/paymentMethods/069a4daa-e752-486c-8e40-c9c4f9a732c4?withPluginInfo=true\n" +
+               "3896 > Cookie: JSESSIONID=64faafa1-da74-4ac7-afc7-947cc9871fe5\n" +
+               "3896 > X-Killbill-Apikey: bob\n" +
+               "3896 > Accept: application/json\n" +
+               "3896 > X-Request-Id: 59c40009-ea68-4d87-9580-fe95e9a82c23\n" +
+               "3896 > X-Killbill-Apisecret: *****\n" +
+               "3896 > User-Agent: killbill/1.9.0; ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin16]\n" +
+               "3896 > Host: 127.0.0.1:8080\n" +
+               "3896 > Accept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\n" +
+               "3896 >");
+    }
+
+    private void verify(final String input, final String output) {
+        final String obfuscated = obfuscator.obfuscate(input, Mockito.mock(ILoggingEvent.class));
+        Assert.assertEquals(obfuscated, output, obfuscated);
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/config/definition/SecurityConfig.java b/util/src/main/java/org/killbill/billing/util/config/definition/SecurityConfig.java
index cfd6a84..f0e5c99 100644
--- a/util/src/main/java/org/killbill/billing/util/config/definition/SecurityConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/definition/SecurityConfig.java
@@ -41,6 +41,11 @@ public interface SecurityConfig extends KillbillConfig {
     @Description("LDAP server's User DN format (e.g. uid={0},ou=users,dc=mycompany,dc=com)")
     public String getShiroLDAPUserDnTemplate();
 
+    @Config("org.killbill.security.ldap.dnSearchTemplate")
+    @DefaultNull
+    @Description("LDAP server's DN search template (e.g. sAMAccountName={0}) for search-then-bind authentication (in case a static DN format template isn't enough)")
+    public String getShiroLDAPDnSearchTemplate();
+
     @Config("org.killbill.security.ldap.searchBase")
     @DefaultNull
     @Description("LDAP search base to use")
@@ -87,4 +92,28 @@ public interface SecurityConfig extends KillbillConfig {
     @Default("false")
     @Description("Whether to ignore SSL certificates checks")
     public boolean disableShiroLDAPSSLCheck();
+
+    @Config("org.killbill.security.ldap.followReferrals")
+    @Default("false")
+    @Description("Whether to follow referrals")
+    public boolean followShiroLDAPReferrals();
+
+    // Okta realm
+
+    @Config("org.killbill.security.okta.url")
+    @DefaultNull
+    @Description("Okta org full url")
+    public String getShiroOktaUrl();
+
+    @Config("org.killbill.security.okta.apiToken")
+    @DefaultNull
+    @Description("Okta API token")
+    public String getShiroOktaAPIToken();
+
+    @Config("org.killbill.security.okta.permissionsByGroup")
+    @Default("admin = *:*\n" +
+             "finance = invoice:*, payment:*\n" +
+             "support = entitlement:*, invoice:item_adjust")
+    @Description("Okta permissions by Okta group")
+    public String getShiroOktaPermissionsByGroup();
 }
diff --git a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
index bd9ceb2..c103789 100644
--- a/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
+++ b/util/src/main/java/org/killbill/billing/util/glue/KillBillShiroModule.java
@@ -28,6 +28,7 @@ import org.killbill.billing.util.config.definition.RbacConfig;
 import org.killbill.billing.util.security.shiro.dao.JDBCSessionDao;
 import org.killbill.billing.util.security.shiro.realm.KillBillJdbcRealm;
 import org.killbill.billing.util.security.shiro.realm.KillBillJndiLdapRealm;
+import org.killbill.billing.util.security.shiro.realm.KillBillOktaRealm;
 import org.skife.config.ConfigSource;
 import org.skife.config.ConfigurationObjectFactory;
 
@@ -38,6 +39,7 @@ import com.google.inject.binder.AnnotatedBindingBuilder;
 public class KillBillShiroModule extends ShiroModule {
 
     public static final String KILLBILL_LDAP_PROPERTY = "killbill.server.ldap";
+    public static final String KILLBILL_OKTA_PROPERTY = "killbill.server.okta";
     public static final String KILLBILL_RBAC_PROPERTY = "killbill.server.rbac";
 
 
@@ -45,6 +47,10 @@ public class KillBillShiroModule extends ShiroModule {
         return Boolean.parseBoolean(System.getProperty(KILLBILL_LDAP_PROPERTY, "false"));
     }
 
+    public static boolean isOktaEnabled() {
+        return Boolean.parseBoolean(System.getProperty(KILLBILL_OKTA_PROPERTY, "false"));
+    }
+
     public static boolean isRBACEnabled() {
         return Boolean.parseBoolean(System.getProperty(KILLBILL_RBAC_PROPERTY, "true"));
     }
@@ -81,6 +87,12 @@ public class KillBillShiroModule extends ShiroModule {
         }
     }
 
+    protected void configureOktaRealm() {
+        if (isOktaEnabled()) {
+            bindRealm().to(KillBillOktaRealm.class).asEagerSingleton();
+        }
+    }
+
     @Override
     protected void bindSecurityManager(final AnnotatedBindingBuilder<? super SecurityManager> bind) {
         super.bindSecurityManager(bind);
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
index 7779670..ac15666 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJndiLdapRealm.java
@@ -39,16 +39,17 @@ import org.apache.shiro.realm.ldap.JndiLdapRealm;
 import org.apache.shiro.realm.ldap.LdapContextFactory;
 import org.apache.shiro.realm.ldap.LdapUtils;
 import org.apache.shiro.subject.PrincipalCollection;
+import org.killbill.billing.util.config.definition.SecurityConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.killbill.billing.util.config.definition.SecurityConfig;
-
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
+import com.google.common.base.Functions;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
 import com.google.common.base.Splitter;
+import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterators;
@@ -74,6 +75,7 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
     private final String groupSearchFilter;
     private final String groupNameId;
     private final Map<String, Collection<String>> permissionsByGroup = Maps.newLinkedHashMap();
+    private final String dnSearchFilter;
 
     @Inject
     public KillBillJndiLdapRealm(final SecurityConfig securityConfig) {
@@ -87,6 +89,7 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
         if (securityConfig.disableShiroLDAPSSLCheck()) {
             contextFactory.getEnvironment().put("java.naming.ldap.factory.socket", SkipSSLCheckSocketFactory.class.getName());
         }
+        contextFactory.getEnvironment().put("java.naming.referral", securityConfig.followShiroLDAPReferrals() ? "follow" : "ignore");
         if (securityConfig.getShiroLDAPUrl() != null) {
             contextFactory.setUrl(securityConfig.getShiroLDAPUrl());
         }
@@ -101,6 +104,8 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
         }
         setContextFactory(contextFactory);
 
+        dnSearchFilter = securityConfig.getShiroLDAPDnSearchTemplate();
+
         searchBase = securityConfig.getShiroLDAPSearchBase();
         groupSearchFilter = securityConfig.getShiroLDAPGroupSearchFilter();
         groupNameId = securityConfig.getShiroLDAPGroupNameID();
@@ -110,8 +115,10 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
             // When passing properties on the command line, \n can be escaped
             ini.load(securityConfig.getShiroLDAPPermissionsByGroup().replace("\\n", "\n"));
             for (final Section section : ini.getSections()) {
-                for (final String role : section.keySet()) {
-                    final Collection<String> permissions = ImmutableList.<String>copyOf(SPLITTER.split(section.get(role)));
+                for (final String rawRole : section.keySet()) {
+                    // Un-escape manually = (required if the role name is a DN)
+                    final Collection<String> permissions = ImmutableList.<String>copyOf(SPLITTER.split(section.get(rawRole)));
+                    final String role = rawRole.replace("\\=", "=");
                     permissionsByGroup.put(role, permissions);
                 }
             }
@@ -119,6 +126,35 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
     }
 
     @Override
+    protected String getUserDn(final String principal) throws IllegalArgumentException, IllegalStateException {
+        if (dnSearchFilter != null) {
+            return findUserDN(principal, getContextFactory());
+        } else {
+            // Use template
+            return super.getUserDn(principal);
+        }
+    }
+
+    private String findUserDN(final String userName, final LdapContextFactory ldapContextFactory) {
+        LdapContext systemLdapCtx = null;
+        try {
+            systemLdapCtx = ldapContextFactory.getSystemLdapContext();
+            final NamingEnumeration<SearchResult> usersFound = systemLdapCtx.search(searchBase,
+                                                                                    dnSearchFilter.replace(USERDN_SUBSTITUTION_TOKEN, userName),
+                                                                                    SUBTREE_SCOPE);
+            return usersFound.hasMore() ? usersFound.next().getNameInNamespace() : null;
+        } catch (final AuthenticationException ex) {
+            log.info("LDAP authentication exception='{}'", ex.getLocalizedMessage());
+            throw new IllegalArgumentException(ex);
+        } catch (final NamingException e) {
+            log.info("LDAP exception='{}'", e.getLocalizedMessage());
+            throw new IllegalArgumentException(e);
+        } finally {
+            LdapUtils.closeContext(systemLdapCtx);
+        }
+    }
+
+    @Override
     protected AuthorizationInfo queryForAuthorizationInfo(final PrincipalCollection principals, final LdapContextFactory ldapContextFactory) throws NamingException {
         final Set<String> userGroups = findLDAPGroupsForUser(principals, ldapContextFactory);
 
@@ -136,7 +172,7 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
         try {
             systemLdapCtx = ldapContextFactory.getSystemLdapContext();
             return findLDAPGroupsForUser(username, systemLdapCtx);
-        } catch (AuthenticationException ex) {
+        } catch (final AuthenticationException ex) {
             log.info("LDAP authentication exception='{}'", ex.getLocalizedMessage());
             return ImmutableSet.<String>of();
         } finally {
@@ -149,21 +185,20 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
                                                                            groupSearchFilter.replace(USERDN_SUBSTITUTION_TOKEN, userName),
                                                                            SUBTREE_SCOPE);
 
+        if (!foundGroups.hasMoreElements()) {
+            return ImmutableSet.<String>of();
+        }
+
+        // There should really only be one entry
+        final SearchResult result = foundGroups.next();
+
         // Extract the name of all the groups
-        final Iterator<SearchResult> groupsIterator = Iterators.<SearchResult>forEnumeration(foundGroups);
-        final Iterator<String> groupsNameIterator = Iterators.<SearchResult, String>transform(groupsIterator,
-                                                                                              new Function<SearchResult, String>() {
-                                                                                                  @Override
-                                                                                                  public String apply(final SearchResult groupEntry) {
-                                                                                                      return extractGroupNameFromSearchResult(groupEntry);
-                                                                                                  }
-                                                                                              });
-        final Iterator<String> finalGroupsNameIterator = Iterators.<String>filter(groupsNameIterator, Predicates.notNull());
-
-        return Sets.newHashSet(finalGroupsNameIterator);
+        final Collection<String> finalGroupsNames = Collections2.<String>filter(extractGroupNamesFromSearchResult(result), Predicates.notNull());
+
+        return Sets.newHashSet(finalGroupsNames);
     }
 
-    private String extractGroupNameFromSearchResult(final SearchResult searchResult) {
+    private Collection<String> extractGroupNamesFromSearchResult(final SearchResult searchResult) {
         // Get all attributes for that group
         final Iterator<? extends Attribute> attributesIterator = Iterators.forEnumeration(searchResult.getAttributes().getAll());
 
@@ -178,31 +213,23 @@ public class KillBillJndiLdapRealm extends JndiLdapRealm {
 
         // Extract the group name from the attribute
         // Note: at this point, groupNameAttributesIterator should really contain a single element
-        final Iterator<String> groupNamesIterator = Iterators.transform(groupNameAttributesIterator,
-                                                                        new Function<Attribute, String>() {
+        final Iterator<Iterator<?>> groupNamesIterator = Iterators.transform(groupNameAttributesIterator,
+                                                                        new Function<Attribute, Iterator<?>>() {
                                                                             @Override
-                                                                            public String apply(final Attribute groupNameAttribute) {
+                                                                            public Iterator<?> apply(final Attribute groupNameAttribute) {
                                                                                 try {
                                                                                     final NamingEnumeration<?> enumeration = groupNameAttribute.getAll();
-                                                                                    if (enumeration.hasMore()) {
-                                                                                        return enumeration.next().toString();
-                                                                                    } else {
-                                                                                        return null;
-                                                                                    }
-                                                                                } catch (NamingException namingException) {
-                                                                                    log.warn("Unable to read group name", namingException);
+                                                                                    return Iterators.forEnumeration(enumeration);
+                                                                                } catch (final NamingException namingException) {
+                                                                                    log.warn("Unable to read group name(s)", namingException);
                                                                                     return null;
                                                                                 }
                                                                             }
                                                                         });
-        final Iterator<String> finalGroupNamesIterator = Iterators.<String>filter(groupNamesIterator, Predicates.notNull());
 
-        if (finalGroupNamesIterator.hasNext()) {
-            return finalGroupNamesIterator.next();
-        } else {
-            log.warn("Unable to find an attribute matching {}", groupNameId);
-            return null;
-        }
+        final Iterator<?> finalGroupNamesIterator = Iterators.filter(Iterators.concat(groupNamesIterator), Predicates.notNull());
+
+        return ImmutableList.<String>copyOf(Iterators.transform(finalGroupNamesIterator, Functions.toStringFunction()));
     }
 
     private Set<String> groupsPermissions(final Set<String> groups) {
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillOktaRealm.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillOktaRealm.java
new file mode 100644
index 0000000..8d75bab
--- /dev/null
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillOktaRealm.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.realm;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.SimpleAuthenticationInfo;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.authz.SimpleAuthorizationInfo;
+import org.apache.shiro.config.Ini;
+import org.apache.shiro.config.Ini.Section;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.killbill.billing.util.config.definition.SecurityConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.http.client.AsyncCompletionHandler;
+import com.ning.http.client.AsyncHttpClient;
+import com.ning.http.client.AsyncHttpClient.BoundRequestBuilder;
+import com.ning.http.client.AsyncHttpClientConfig;
+import com.ning.http.client.ListenableFuture;
+import com.ning.http.client.Response;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+public class KillBillOktaRealm extends AuthorizingRealm {
+
+    private static final Logger log = LoggerFactory.getLogger(KillBillOktaRealm.class);
+    private static final ObjectMapper mapper = new ObjectMapper();
+    private static final int DEFAULT_TIMEOUT_SECS = 15;
+    private static final Splitter SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
+
+    private final Map<String, Collection<String>> permissionsByGroup = Maps.newLinkedHashMap();
+
+    private final SecurityConfig securityConfig;
+    private final AsyncHttpClient httpClient;
+
+    @Inject
+    public KillBillOktaRealm(final SecurityConfig securityConfig) {
+        this.securityConfig = securityConfig;
+        this.httpClient = new AsyncHttpClient(new AsyncHttpClientConfig.Builder().setRequestTimeout(DEFAULT_TIMEOUT_SECS * 1000).build());
+
+        if (securityConfig.getShiroOktaPermissionsByGroup() != null) {
+            final Ini ini = new Ini();
+            // When passing properties on the command line, \n can be escaped
+            ini.load(securityConfig.getShiroOktaPermissionsByGroup().replace("\\n", "\n"));
+            for (final Section section : ini.getSections()) {
+                for (final String role : section.keySet()) {
+                    final Collection<String> permissions = ImmutableList.<String>copyOf(SPLITTER.split(section.get(role)));
+                    permissionsByGroup.put(role, permissions);
+                }
+            }
+        }
+    }
+
+    @Override
+    protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
+        final String username = (String) getAvailablePrincipal(principals);
+        final String userId = findOktaUserId(username);
+        final Set<String> userGroups = findOktaGroupsForUser(userId);
+
+        final SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(userGroups);
+        final Set<String> stringPermissions = groupsPermissions(userGroups);
+        simpleAuthorizationInfo.setStringPermissions(stringPermissions);
+
+        return simpleAuthorizationInfo;
+    }
+
+    @Override
+    protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token) throws AuthenticationException {
+        final UsernamePasswordToken upToken = (UsernamePasswordToken) token;
+        if (doAuthenticate(upToken)) {
+            // Credentials are valid
+            return new SimpleAuthenticationInfo(token.getPrincipal(), token.getCredentials(), getName());
+        } else {
+            throw new AuthenticationException("Okta authentication failed");
+        }
+    }
+
+    private boolean doAuthenticate(final UsernamePasswordToken upToken) {
+        final BoundRequestBuilder builder = httpClient.preparePost(securityConfig.getShiroOktaUrl() + "/api/v1/authn");
+        try {
+            final ImmutableMap<String, String> body = ImmutableMap.<String, String>of("username", upToken.getUsername(),
+                                                                                      "password", String.valueOf(upToken.getPassword()));
+            builder.setBody(mapper.writeValueAsString(body));
+        } catch (final JsonProcessingException e) {
+            log.warn("Error while generating Okta payload");
+            throw new AuthenticationException(e);
+        }
+        builder.addHeader("Authorization", "SSWS " + securityConfig.getShiroOktaAPIToken());
+        builder.addHeader("Content-Type", "application/json; charset=UTF-8");
+        final Response response;
+        try {
+            final ListenableFuture<Response> futureStatus =
+                    builder.execute(new AsyncCompletionHandler<Response>() {
+                        @Override
+                        public Response onCompleted(final Response response) throws Exception {
+                            return response;
+                        }
+                    });
+            response = futureStatus.get(DEFAULT_TIMEOUT_SECS, TimeUnit.SECONDS);
+        } catch (final TimeoutException toe) {
+            log.warn("Timeout while connecting to Okta");
+            throw new AuthenticationException(toe);
+        } catch (final Exception e) {
+            log.warn("Error while connecting to Okta");
+            throw new AuthenticationException(e);
+        }
+
+        return isAuthenticated(response);
+    }
+
+    private boolean isAuthenticated(final Response oktaRawResponse) {
+        try {
+            final Map oktaResponse = mapper.readValue(oktaRawResponse.getResponseBodyAsStream(), Map.class);
+            if ("SUCCESS".equals(oktaResponse.get("status"))) {
+                return true;
+            } else {
+                log.warn("Okta authentication failed: " + oktaResponse);
+                return false;
+            }
+        } catch (final IOException e) {
+            log.warn("Unable to read response from Okta");
+            throw new AuthenticationException(e);
+        }
+    }
+
+    private String findOktaUserId(final String login) {
+        final String path;
+        try {
+            path = "/api/v1/users/" + URLEncoder.encode(login, "UTF-8");
+        } catch (final UnsupportedEncodingException e) {
+            // Should never happen
+            throw new IllegalStateException(e);
+        }
+
+        final Response oktaRawResponse = doGetRequest(path);
+        try {
+            final Map oktaResponse = mapper.readValue(oktaRawResponse.getResponseBodyAsStream(), Map.class);
+            return (String) oktaResponse.get("id");
+        } catch (final IOException e) {
+            log.warn("Unable to read response from Okta");
+            throw new AuthorizationException(e);
+        }
+    }
+
+    private Set<String> findOktaGroupsForUser(final String userId) {
+        final String path = "/api/v1/users/" + userId + "/groups";
+        final Response response = doGetRequest(path);
+        return getGroups(response);
+    }
+
+    private Response doGetRequest(final String path) {
+        final BoundRequestBuilder builder = httpClient.prepareGet(securityConfig.getShiroOktaUrl() + path);
+        builder.addHeader("Authorization", "SSWS " + securityConfig.getShiroOktaAPIToken());
+        builder.addHeader("Content-Type", "application/json; charset=UTF-8");
+        final Response response;
+        try {
+            final ListenableFuture<Response> futureStatus =
+                    builder.execute(new AsyncCompletionHandler<Response>() {
+                        @Override
+                        public Response onCompleted(final Response response) throws Exception {
+                            return response;
+                        }
+                    });
+            response = futureStatus.get(DEFAULT_TIMEOUT_SECS, TimeUnit.SECONDS);
+        } catch (final TimeoutException toe) {
+            log.warn("Timeout while connecting to Okta");
+            throw new AuthorizationException(toe);
+        } catch (final Exception e) {
+            log.warn("Error while connecting to Okta");
+            throw new AuthorizationException(e);
+        }
+        return response;
+    }
+
+    private Set<String> getGroups(final Response oktaRawResponse) {
+        try {
+            final List<Map> oktaResponse = mapper.readValue(oktaRawResponse.getResponseBodyAsStream(), new TypeReference<List<Map>>() {});
+            final Set<String> groups = new HashSet<String>();
+            for (final Map group : oktaResponse) {
+                final Object groupProfile = group.get("profile");
+                if (groupProfile != null && groupProfile instanceof Map) {
+                    groups.add((String) ((Map) groupProfile).get("name"));
+                }
+            }
+            return groups;
+        } catch (final IOException e) {
+            log.warn("Unable to read response from Okta");
+            throw new AuthorizationException(e);
+        }
+    }
+
+    private Set<String> groupsPermissions(final Iterable<String> groups) {
+        final Set<String> permissions = new HashSet<String>();
+        for (final String group : groups) {
+            final Collection<String> permissionsForGroup = permissionsByGroup.get(group);
+            if (permissionsForGroup != null) {
+                permissions.addAll(permissionsForGroup);
+            }
+        }
+        return permissions;
+    }
+}
diff --git a/util/src/main/java/org/killbill/billing/util/tag/api/DefaultTagUserApi.java b/util/src/main/java/org/killbill/billing/util/tag/api/DefaultTagUserApi.java
index b461cc2..55314d1 100644
--- a/util/src/main/java/org/killbill/billing/util/tag/api/DefaultTagUserApi.java
+++ b/util/src/main/java/org/killbill/billing/util/tag/api/DefaultTagUserApi.java
@@ -75,7 +75,7 @@ public class DefaultTagUserApi implements TagUserApi {
 
     @Override
     public List<TagDefinition> getTagDefinitions(final TenantContext context) {
-        return ImmutableList.<TagDefinition>copyOf(Collections2.transform(tagDefinitionDao.getTagDefinitions(false, internalCallContextFactory.createInternalTenantContextWithoutAccountRecordId(context)),
+        return ImmutableList.<TagDefinition>copyOf(Collections2.transform(tagDefinitionDao.getTagDefinitions(true, internalCallContextFactory.createInternalTenantContextWithoutAccountRecordId(context)),
                                                                           new Function<TagDefinitionModelDao, TagDefinition>() {
                                                                               @Override
                                                                               public TagDefinition apply(final TagDefinitionModelDao input) {
diff --git a/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillOktaRealm.java b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillOktaRealm.java
new file mode 100644
index 0000000..edc374a
--- /dev/null
+++ b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillOktaRealm.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2014-2017 Groupon, Inc
+ * Copyright 2014-2017 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.util.security.shiro.realm;
+
+import java.util.Properties;
+
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.subject.SimplePrincipalCollection;
+import org.killbill.billing.util.UtilTestSuiteNoDB;
+import org.killbill.billing.util.config.definition.SecurityConfig;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.skife.config.SimplePropertyConfigSource;
+import org.testng.annotations.Test;
+
+public class TestKillBillOktaRealm extends UtilTestSuiteNoDB {
+
+    @Test(groups = "external", enabled = false)
+    public void testCheckOktaConnection() throws Exception {
+        // Convenience method to verify your Okta connectivity
+        final Properties props = new Properties();
+        props.setProperty("org.killbill.security.okta.url", "https://dev-XXXXXX.oktapreview.com");
+        props.setProperty("org.killbill.security.okta.apiToken", "YYYYYY");
+        props.setProperty("org.killbill.security.okta.permissionsByGroup", "support-group: entitlement:*\n" +
+                                                                           "finance-group: invoice:*, payment:*\n" +
+                                                                           "ops-group: *:*");
+        final ConfigSource customConfigSource = new SimplePropertyConfigSource(props);
+        final SecurityConfig securityConfig = new ConfigurationObjectFactory(customConfigSource).build(SecurityConfig.class);
+        final KillBillOktaRealm oktaRealm = new KillBillOktaRealm(securityConfig);
+
+        final String username = "pierre";
+        final String password = "password";
+
+        // Check authentication
+        final UsernamePasswordToken token = new UsernamePasswordToken(username, password);
+        final AuthenticationInfo authenticationInfo = oktaRealm.getAuthenticationInfo(token);
+        System.out.println(authenticationInfo);
+
+        // Check permissions
+        final SimplePrincipalCollection principals = new SimplePrincipalCollection(username, username);
+        final AuthorizationInfo authorizationInfo = oktaRealm.doGetAuthorizationInfo(principals);
+        System.out.println("Roles: " + authorizationInfo.getRoles());
+        System.out.println("Permissions: " + authorizationInfo.getStringPermissions());
+    }
+}