You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LdapBasedUnitTest.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. package com.gitblit.tests;
  2. import java.io.File;
  3. import java.util.Arrays;
  4. import java.util.Collection;
  5. import java.util.EnumSet;
  6. import java.util.HashMap;
  7. import java.util.Map;
  8. import org.apache.commons.io.FileUtils;
  9. import org.junit.AfterClass;
  10. import org.junit.Before;
  11. import org.junit.BeforeClass;
  12. import org.junit.Rule;
  13. import org.junit.rules.TemporaryFolder;
  14. import org.junit.runners.Parameterized.Parameter;
  15. import org.junit.runners.Parameterized.Parameters;
  16. import com.gitblit.Keys;
  17. import com.gitblit.tests.mock.MemorySettings;
  18. import com.unboundid.ldap.listener.InMemoryDirectoryServer;
  19. import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
  20. import com.unboundid.ldap.listener.InMemoryDirectoryServerSnapshot;
  21. import com.unboundid.ldap.listener.InMemoryListenerConfig;
  22. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedRequest;
  23. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedResult;
  24. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchEntry;
  25. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchRequest;
  26. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
  27. import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSimpleBindResult;
  28. import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
  29. import com.unboundid.ldap.sdk.BindRequest;
  30. import com.unboundid.ldap.sdk.BindResult;
  31. import com.unboundid.ldap.sdk.LDAPException;
  32. import com.unboundid.ldap.sdk.LDAPResult;
  33. import com.unboundid.ldap.sdk.OperationType;
  34. import com.unboundid.ldap.sdk.ResultCode;
  35. import com.unboundid.ldap.sdk.SimpleBindRequest;
  36. /**
  37. * Base class for Unit (/Integration) tests that test going against an
  38. * in-memory UnboundID LDAP server.
  39. *
  40. * This base class creates separate in-memory LDAP servers for different scenarios:
  41. * - ANONYMOUS: anonymous bind to LDAP.
  42. * - DS_MANAGER: The DIRECTORY_MANAGER is set as DN to bind as an admin.
  43. * Normal users are prohibited to search the DS, they can only bind.
  44. * - USR_MANAGER: The USER_MANAGER is set as DN to bind as an admin.
  45. * This account can only search users but not groups. Normal users can search groups.
  46. *
  47. * @author Florian Zschocke
  48. *
  49. */
  50. public abstract class LdapBasedUnitTest extends GitblitUnitTest {
  51. protected static final String RESOURCE_DIR = "src/test/resources/ldap/";
  52. private static final String DIRECTORY_MANAGER = "cn=Directory Manager";
  53. private static final String USER_MANAGER = "cn=UserManager";
  54. protected static final String ACCOUNT_BASE = "OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain";
  55. private static final String GROUP_BASE = "OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain";
  56. protected static final String DN_USER_ONE = "CN=UserOne,OU=US," + ACCOUNT_BASE;
  57. protected static final String DN_USER_TWO = "CN=UserTwo,OU=US," + ACCOUNT_BASE;
  58. protected static final String DN_USER_THREE = "CN=UserThree,OU=Canada," + ACCOUNT_BASE;
  59. /**
  60. * Enumeration of different test modes, representing different use scenarios.
  61. * With ANONYMOUS anonymous binds are used to search LDAP.
  62. * DS_MANAGER will use a DIRECTORY_MANAGER to search LDAP. Normal users are prohibited to search the DS.
  63. * With USR_MANAGER, a USER_MANAGER account is used to search in LDAP. This account can only search users
  64. * but not groups. Normal users can search groups, though.
  65. *
  66. */
  67. protected enum AuthMode {
  68. ANONYMOUS,
  69. DS_MANAGER,
  70. USR_MANAGER;
  71. private int ldapPort;
  72. private InMemoryDirectoryServer ds;
  73. private InMemoryDirectoryServerSnapshot dsSnapshot;
  74. private BindTracker bindTracker;
  75. void setLdapPort(int port) {
  76. this.ldapPort = port;
  77. }
  78. int ldapPort() {
  79. return this.ldapPort;
  80. }
  81. void setDS(InMemoryDirectoryServer ds) {
  82. if (this.ds == null) {
  83. this.ds = ds;
  84. this.dsSnapshot = ds.createSnapshot();
  85. };
  86. }
  87. InMemoryDirectoryServer getDS() {
  88. return ds;
  89. }
  90. void setBindTracker(BindTracker bindTracker) {
  91. this.bindTracker = bindTracker;
  92. }
  93. BindTracker getBindTracker() {
  94. return bindTracker;
  95. }
  96. void restoreSnapshot() {
  97. ds.restoreSnapshot(dsSnapshot);
  98. }
  99. }
  100. @Parameter
  101. public AuthMode authMode = AuthMode.ANONYMOUS;
  102. @Rule
  103. public TemporaryFolder folder = new TemporaryFolder();
  104. protected File usersConf;
  105. protected MemorySettings settings;
  106. /**
  107. * Run the tests with each authentication scenario once.
  108. */
  109. @Parameters(name = "{0}")
  110. public static Collection<Object[]> data() {
  111. return Arrays.asList(new Object[][] { {AuthMode.ANONYMOUS}, {AuthMode.DS_MANAGER}, {AuthMode.USR_MANAGER} });
  112. }
  113. /**
  114. * Create three different in memory DS.
  115. *
  116. * Each DS has a different configuration:
  117. * The first allows anonymous binds.
  118. * The second requires authentication for all operations. It will only allow the DIRECTORY_MANAGER account
  119. * to search for users and groups.
  120. * The third one is like the second, but it allows users to search for users and groups, and restricts the
  121. * USER_MANAGER from searching for groups.
  122. */
  123. @BeforeClass
  124. public static void ldapInit() throws Exception {
  125. InMemoryDirectoryServer ds;
  126. InMemoryDirectoryServerConfig config = createInMemoryLdapServerConfig(AuthMode.ANONYMOUS);
  127. config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("anonymous"));
  128. ds = createInMemoryLdapServer(config);
  129. AuthMode.ANONYMOUS.setDS(ds);
  130. AuthMode.ANONYMOUS.setLdapPort(ds.getListenPort("anonymous"));
  131. config = createInMemoryLdapServerConfig(AuthMode.DS_MANAGER);
  132. config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("ds_manager"));
  133. config.setAuthenticationRequiredOperationTypes(EnumSet.allOf(OperationType.class));
  134. ds = createInMemoryLdapServer(config);
  135. AuthMode.DS_MANAGER.setDS(ds);
  136. AuthMode.DS_MANAGER.setLdapPort(ds.getListenPort("ds_manager"));
  137. config = createInMemoryLdapServerConfig(AuthMode.USR_MANAGER);
  138. config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("usr_manager"));
  139. config.setAuthenticationRequiredOperationTypes(EnumSet.allOf(OperationType.class));
  140. ds = createInMemoryLdapServer(config);
  141. AuthMode.USR_MANAGER.setDS(ds);
  142. AuthMode.USR_MANAGER.setLdapPort(ds.getListenPort("usr_manager"));
  143. }
  144. @AfterClass
  145. public static void destroy() throws Exception {
  146. for (AuthMode am : AuthMode.values()) {
  147. am.getDS().shutDown(true);
  148. }
  149. }
  150. public static InMemoryDirectoryServer createInMemoryLdapServer(InMemoryDirectoryServerConfig config) throws Exception {
  151. InMemoryDirectoryServer imds = new InMemoryDirectoryServer(config);
  152. imds.importFromLDIF(true, RESOURCE_DIR + "sampledata.ldif");
  153. imds.startListening();
  154. return imds;
  155. }
  156. public static InMemoryDirectoryServerConfig createInMemoryLdapServerConfig(AuthMode authMode) throws Exception {
  157. InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=MyDomain");
  158. config.addAdditionalBindCredentials(DIRECTORY_MANAGER, "password");
  159. config.addAdditionalBindCredentials(USER_MANAGER, "passwd");
  160. config.setSchema(null);
  161. authMode.setBindTracker(new BindTracker());
  162. config.addInMemoryOperationInterceptor(authMode.getBindTracker());
  163. config.addInMemoryOperationInterceptor(new AccessInterceptor(authMode));
  164. return config;
  165. }
  166. @Before
  167. public void setupBase() throws Exception {
  168. authMode.restoreSnapshot();
  169. authMode.getBindTracker().reset();
  170. usersConf = folder.newFile("users.conf");
  171. FileUtils.copyFile(new File(RESOURCE_DIR + "users.conf"), usersConf);
  172. settings = getSettings();
  173. }
  174. protected InMemoryDirectoryServer getDS() {
  175. return authMode.getDS();
  176. }
  177. protected MemorySettings getSettings() {
  178. Map<String, Object> backingMap = new HashMap<String, Object>();
  179. backingMap.put(Keys.realm.userService, usersConf.getAbsolutePath());
  180. backingMap.put(Keys.realm.ldap.server, "ldap://localhost:" + authMode.ldapPort());
  181. switch(authMode) {
  182. case ANONYMOUS:
  183. backingMap.put(Keys.realm.ldap.username, "");
  184. backingMap.put(Keys.realm.ldap.password, "");
  185. break;
  186. case DS_MANAGER:
  187. backingMap.put(Keys.realm.ldap.username, DIRECTORY_MANAGER);
  188. backingMap.put(Keys.realm.ldap.password, "password");
  189. break;
  190. case USR_MANAGER:
  191. backingMap.put(Keys.realm.ldap.username, USER_MANAGER);
  192. backingMap.put(Keys.realm.ldap.password, "passwd");
  193. break;
  194. default:
  195. throw new RuntimeException("Unimplemented AuthMode case!");
  196. }
  197. backingMap.put(Keys.realm.ldap.maintainTeams, "true");
  198. backingMap.put(Keys.realm.ldap.accountBase, ACCOUNT_BASE);
  199. backingMap.put(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
  200. backingMap.put(Keys.realm.ldap.groupBase, GROUP_BASE);
  201. backingMap.put(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
  202. backingMap.put(Keys.realm.ldap.admins, "UserThree @Git_Admins \"@Git Admins\"");
  203. backingMap.put(Keys.realm.ldap.displayName, "displayName");
  204. backingMap.put(Keys.realm.ldap.email, "email");
  205. backingMap.put(Keys.realm.ldap.uid, "sAMAccountName");
  206. MemorySettings ms = new MemorySettings(backingMap);
  207. return ms;
  208. }
  209. /**
  210. * Operation interceptor for the in memory DS. This interceptor
  211. * tracks bind requests.
  212. *
  213. */
  214. protected static class BindTracker extends InMemoryOperationInterceptor {
  215. private Map<Integer,String> lastSuccessfulBindDNs = new HashMap<>();
  216. private String lastSuccessfulBindDN;
  217. @Override
  218. public void processSimpleBindResult(InMemoryInterceptedSimpleBindResult bind) {
  219. BindResult result = bind.getResult();
  220. if (result.getResultCode() == ResultCode.SUCCESS) {
  221. BindRequest bindRequest = bind.getRequest();
  222. lastSuccessfulBindDNs.put(bind.getMessageID(), ((SimpleBindRequest)bindRequest).getBindDN());
  223. lastSuccessfulBindDN = ((SimpleBindRequest)bindRequest).getBindDN();
  224. }
  225. }
  226. String getLastSuccessfulBindDN() {
  227. return lastSuccessfulBindDN;
  228. }
  229. String getLastSuccessfulBindDN(int messageID) {
  230. return lastSuccessfulBindDNs.get(messageID);
  231. }
  232. void reset() {
  233. lastSuccessfulBindDNs = new HashMap<>();
  234. lastSuccessfulBindDN = null;
  235. }
  236. }
  237. /**
  238. * Operation interceptor for the in memory DS. This interceptor
  239. * implements access restrictions for certain user/DN combinations.
  240. *
  241. * The USER_MANAGER is only allowed to search for users, but not for groups.
  242. * This is to test the original behaviour where the teams were searched under
  243. * the user binding.
  244. * When running in a DIRECTORY_MANAGER scenario, only the manager account
  245. * is allowed to search for users and groups, while a normal user may not do so.
  246. * This tests the scenario where a normal user cannot read teams and thus the
  247. * manager account needs to be used for all searches.
  248. *
  249. */
  250. protected static class AccessInterceptor extends InMemoryOperationInterceptor {
  251. AuthMode authMode;
  252. Map<Long,String> lastSuccessfulBindDN = new HashMap<>();
  253. Map<Long,Boolean> resultProhibited = new HashMap<>();
  254. public AccessInterceptor(AuthMode authMode) {
  255. this.authMode = authMode;
  256. }
  257. @Override
  258. public void processSimpleBindResult(InMemoryInterceptedSimpleBindResult bind) {
  259. BindResult result = bind.getResult();
  260. if (result.getResultCode() == ResultCode.SUCCESS) {
  261. BindRequest bindRequest = bind.getRequest();
  262. lastSuccessfulBindDN.put(bind.getConnectionID(), ((SimpleBindRequest)bindRequest).getBindDN());
  263. resultProhibited.remove(bind.getConnectionID());
  264. }
  265. }
  266. @Override
  267. public void processSearchRequest(InMemoryInterceptedSearchRequest request) throws LDAPException {
  268. String bindDN = getLastBindDN(request);
  269. if (USER_MANAGER.equals(bindDN)) {
  270. if (request.getRequest().getBaseDN().endsWith(GROUP_BASE)) {
  271. throw new LDAPException(ResultCode.NO_SUCH_OBJECT);
  272. }
  273. }
  274. else if(authMode == AuthMode.DS_MANAGER && !DIRECTORY_MANAGER.equals(bindDN)) {
  275. throw new LDAPException(ResultCode.NO_SUCH_OBJECT);
  276. }
  277. }
  278. @Override
  279. public void processSearchEntry(InMemoryInterceptedSearchEntry entry) {
  280. String bindDN = getLastBindDN(entry);
  281. boolean prohibited = false;
  282. if (USER_MANAGER.equals(bindDN)) {
  283. if (entry.getSearchEntry().getDN().endsWith(GROUP_BASE)) {
  284. prohibited = true;
  285. }
  286. }
  287. else if(authMode == AuthMode.DS_MANAGER && !DIRECTORY_MANAGER.equals(bindDN)) {
  288. prohibited = true;
  289. }
  290. if (prohibited) {
  291. // Found entry prohibited for bound user. Setting entry to null.
  292. entry.setSearchEntry(null);
  293. resultProhibited.put(entry.getConnectionID(), Boolean.TRUE);
  294. }
  295. }
  296. @Override
  297. public void processSearchResult(InMemoryInterceptedSearchResult result) {
  298. String bindDN = getLastBindDN(result);
  299. boolean prohibited = false;
  300. Boolean rspb = resultProhibited.get(result.getConnectionID());
  301. if (USER_MANAGER.equals(bindDN)) {
  302. if (rspb != null && rspb) {
  303. prohibited = true;
  304. }
  305. }
  306. else if(authMode == AuthMode.DS_MANAGER && !DIRECTORY_MANAGER.equals(bindDN)) {
  307. if (rspb != null && rspb) {
  308. prohibited = true;
  309. }
  310. }
  311. if (prohibited) {
  312. // Result prohibited for bound user. Returning error
  313. result.setResult(new LDAPResult(result.getMessageID(), ResultCode.INSUFFICIENT_ACCESS_RIGHTS));
  314. resultProhibited.remove(result.getConnectionID());
  315. }
  316. }
  317. private String getLastBindDN(InMemoryInterceptedResult result) {
  318. String bindDN = lastSuccessfulBindDN.get(result.getConnectionID());
  319. if (bindDN == null) {
  320. return "UNKNOWN";
  321. }
  322. return bindDN;
  323. }
  324. private String getLastBindDN(InMemoryInterceptedRequest request) {
  325. String bindDN = lastSuccessfulBindDN.get(request.getConnectionID());
  326. if (bindDN == null) {
  327. return "UNKNOWN";
  328. }
  329. return bindDN;
  330. }
  331. }
  332. }