1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.auth.shiro.realm;
18
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static java.util.Objects.requireNonNull;
21
22 import java.time.Duration;
23 import java.util.regex.Pattern;
24
25 import javax.annotation.Nullable;
26 import javax.naming.AuthenticationException;
27 import javax.naming.NamingEnumeration;
28 import javax.naming.NamingException;
29 import javax.naming.ServiceUnavailableException;
30 import javax.naming.directory.SearchControls;
31 import javax.naming.directory.SearchResult;
32 import javax.naming.ldap.LdapContext;
33
34 import org.apache.shiro.authc.AuthenticationInfo;
35 import org.apache.shiro.authc.AuthenticationToken;
36 import org.apache.shiro.authc.UsernamePasswordToken;
37 import org.apache.shiro.realm.Realm;
38 import org.apache.shiro.realm.activedirectory.ActiveDirectoryRealm;
39 import org.apache.shiro.realm.ldap.LdapContextFactory;
40 import org.apache.shiro.realm.ldap.LdapUtils;
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 public class SearchFirstActiveDirectoryRealm extends ActiveDirectoryRealm {
60
61 private static final Pattern USERNAME_PLACEHOLDER = Pattern.compile("\\{0}");
62 private static final String DEFAULT_SEARCH_FILTER = "cn={0}";
63 private static final int DEFAULT_SEARCH_TIMEOUT_MILLIS = (int) Duration.ofSeconds(10).toMillis();
64
65 @Nullable
66 private String searchFilter = DEFAULT_SEARCH_FILTER;
67 private int searchTimeoutMillis = DEFAULT_SEARCH_TIMEOUT_MILLIS;
68
69
70
71
72 @Nullable
73 protected String getSearchFilter() {
74 return searchFilter;
75 }
76
77
78
79
80 protected void setSearchFilter(String searchFilter) {
81 this.searchFilter = requireNonNull(searchFilter, "searchFilter");
82 }
83
84
85
86
87 public int getSearchTimeoutMillis() {
88 return searchTimeoutMillis;
89 }
90
91
92
93
94 protected void setSearchTimeoutMillis(int searchTimeoutMillis) {
95 checkArgument(searchTimeoutMillis >= 0,
96 "searchTimeoutMillis should be 0 or positive number");
97 this.searchTimeoutMillis = searchTimeoutMillis;
98 }
99
100
101
102
103
104 @Nullable
105 @Override
106 protected AuthenticationInfo queryForAuthenticationInfo(
107 AuthenticationToken token, LdapContextFactory ldapContextFactory) throws NamingException {
108 try {
109 return queryForAuthenticationInfo0(token, ldapContextFactory);
110 } catch (ServiceUnavailableException e) {
111
112 return queryForAuthenticationInfo0(token, ldapContextFactory);
113 }
114 }
115
116 @Nullable
117 private AuthenticationInfo queryForAuthenticationInfo0(
118 AuthenticationToken token, LdapContextFactory ldapContextFactory) throws NamingException {
119
120 final UsernamePasswordToken upToken = ensureUsernamePasswordToken(token);
121 final String userDn = findUserDn(ldapContextFactory, upToken.getUsername());
122 if (userDn == null) {
123 return null;
124 }
125
126 LdapContext ctx = null;
127 try {
128
129 ctx = ldapContextFactory.getLdapContext(userDn, upToken.getPassword());
130 } catch (AuthenticationException e) {
131
132
133
134
135 return null;
136 } finally {
137 LdapUtils.closeContext(ctx);
138 }
139 return buildAuthenticationInfo(upToken.getUsername(), upToken.getPassword());
140 }
141
142
143
144
145
146
147
148 @Nullable
149 protected String findUserDn(LdapContextFactory ldapContextFactory, String username) throws NamingException {
150 LdapContext ctx = null;
151 try {
152
153 ctx = ldapContextFactory.getSystemLdapContext();
154
155 final SearchControls ctrl = new SearchControls();
156 ctrl.setCountLimit(1);
157 ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE);
158 ctrl.setTimeLimit(searchTimeoutMillis);
159
160 final String filter =
161 searchFilter != null ? USERNAME_PLACEHOLDER.matcher(searchFilter)
162 .replaceAll(username)
163 : username;
164 final NamingEnumeration<SearchResult> result = ctx.search(searchBase, filter, ctrl);
165 try {
166 if (!result.hasMore()) {
167 return null;
168 }
169 return result.next().getNameInNamespace();
170 } finally {
171 result.close();
172 }
173 } finally {
174 LdapUtils.closeContext(ctx);
175 }
176 }
177
178 private static UsernamePasswordToken ensureUsernamePasswordToken(AuthenticationToken token) {
179 if (token instanceof UsernamePasswordToken) {
180 return (UsernamePasswordToken) token;
181 }
182
183 throw new IllegalArgumentException("Token '" + token.getClass().getName() + "' is not supported.");
184 }
185 }
186