]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java
Make client certificate authorization more robust
[lgpl/argeo-commons.git] / org.argeo.enterprise / src / org / argeo / osgi / useradmin / UserAdminConf.java
1 package org.argeo.osgi.useradmin;
2
3 import java.io.IOException;
4 import java.net.InetAddress;
5 import java.net.URI;
6 import java.net.URISyntaxException;
7 import java.net.UnknownHostException;
8 import java.util.Dictionary;
9 import java.util.Enumeration;
10 import java.util.Hashtable;
11 import java.util.List;
12 import java.util.Map;
13
14 import javax.naming.Context;
15 import javax.naming.NamingException;
16 import javax.naming.ldap.LdapName;
17
18 import org.apache.commons.logging.Log;
19 import org.apache.commons.logging.LogFactory;
20 import org.argeo.naming.DnsBrowser;
21 import org.argeo.naming.NamingUtils;
22 import org.osgi.framework.Constants;
23
24 /** Properties used to configure user admins. */
25 public enum UserAdminConf {
26 /** Base DN (cannot be configured externally) */
27 baseDn("dc=example,dc=com"),
28
29 /** URI of the underlying resource (cannot be configured externally) */
30 uri("ldap://localhost:10389"),
31
32 /** User objectClass */
33 userObjectClass("inetOrgPerson"),
34
35 /** Relative base DN for users */
36 userBase("ou=People"),
37
38 /** Groups objectClass */
39 groupObjectClass("groupOfNames"),
40
41 /** Relative base DN for users */
42 groupBase("ou=Groups"),
43
44 /** Read-only source */
45 readOnly(null),
46
47 /** Disabled source */
48 disabled(null),
49
50 /** Authentication realm */
51 realm(null);
52
53 public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config";
54 private final static Log log = LogFactory.getLog(UserAdminConf.class);
55
56 public final static String SCHEME_LDAP = "ldap";
57 public final static String SCHEME_FILE = "file";
58 public final static String SCHEME_OS = "os";
59 public final static String SCHEME_IPA = "ipa";
60
61 /** The default value. */
62 private Object def;
63
64 UserAdminConf(Object def) {
65 this.def = def;
66 }
67
68 public Object getDefault() {
69 return def;
70 }
71
72 /**
73 * For use as Java property.
74 *
75 * @deprecated use {@link #name()} instead
76 */
77 @Deprecated
78 public String property() {
79 return name();
80 }
81
82 public String getValue(Dictionary<String, ?> properties) {
83 Object res = getRawValue(properties);
84 if (res == null)
85 return null;
86 return res.toString();
87 }
88
89 @SuppressWarnings("unchecked")
90 public <T> T getRawValue(Dictionary<String, ?> properties) {
91 Object res = properties.get(name());
92 if (res == null)
93 res = getDefault();
94 return (T) res;
95 }
96
97 /** @deprecated use {@link #valueOf(String)} instead */
98 @Deprecated
99 public static UserAdminConf local(String property) {
100 return UserAdminConf.valueOf(property);
101 }
102
103 /** Hides host and credentials. */
104 public static URI propertiesAsUri(Dictionary<String, ?> properties) {
105 StringBuilder query = new StringBuilder();
106
107 boolean first = true;
108 for (Enumeration<String> keys = properties.keys(); keys.hasMoreElements();) {
109 String key = keys.nextElement();
110 // TODO clarify which keys are relevant (list only the enum?)
111 if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn")
112 && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name())
113 && !key.equals(uri.name())) {
114 if (first)
115 first = false;
116 else
117 query.append('&');
118 query.append(valueOf(key).name());
119 query.append('=').append(properties.get(key).toString());
120 }
121 }
122
123 String bDn = (String) properties.get(baseDn.name());
124 try {
125 return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null,
126 null);
127 } catch (URISyntaxException e) {
128 throw new UserDirectoryException("Cannot create URI from properties", e);
129 }
130 }
131
132 public static Dictionary<String, Object> uriAsProperties(String uriStr) {
133 try {
134 Hashtable<String, Object> res = new Hashtable<String, Object>();
135 URI u = new URI(uriStr);
136 String scheme = u.getScheme();
137 if (scheme != null && scheme.equals(SCHEME_IPA)) {
138 u = convertIpaConfig(u);
139 scheme = u.getScheme();
140 }
141 String path = u.getPath();
142 // base DN
143 String bDn = path.substring(path.lastIndexOf('/') + 1, path.length());
144 if (bDn.equals("") && SCHEME_OS.equals(scheme)) {
145 bDn = getBaseDnFromHostname();
146 }
147
148 if (bDn.endsWith(".ldif"))
149 bDn = bDn.substring(0, bDn.length() - ".ldif".length());
150
151 // Normalize base DN as LDAP name
152 bDn = new LdapName(bDn).toString();
153
154 String principal = null;
155 String credentials = null;
156 if (scheme != null)
157 if (scheme.equals(SCHEME_LDAP) || scheme.equals("ldaps")) {
158 // TODO additional checks
159 if (u.getUserInfo() != null) {
160 String[] userInfo = u.getUserInfo().split(":");
161 principal = userInfo.length > 0 ? userInfo[0] : null;
162 credentials = userInfo.length > 1 ? userInfo[1] : null;
163 }
164 } else if (scheme.equals(SCHEME_FILE)) {
165 } else if (scheme.equals(SCHEME_IPA)) {
166 } else if (scheme.equals(SCHEME_OS)) {
167 } else
168 throw new UserDirectoryException("Unsupported scheme " + scheme);
169 Map<String, List<String>> query = NamingUtils.queryToMap(u);
170 for (String key : query.keySet()) {
171 UserAdminConf ldapProp = UserAdminConf.valueOf(key);
172 List<String> values = query.get(key);
173 if (values.size() == 1) {
174 res.put(ldapProp.name(), values.get(0));
175 } else {
176 throw new UserDirectoryException("Only single values are supported");
177 }
178 }
179 res.put(baseDn.name(), bDn);
180 if (SCHEME_OS.equals(scheme))
181 res.put(readOnly.name(), "true");
182 if (principal != null)
183 res.put(Context.SECURITY_PRINCIPAL, principal);
184 if (credentials != null)
185 res.put(Context.SECURITY_CREDENTIALS, credentials);
186 if (scheme != null) {// relative URIs are dealt with externally
187 if (SCHEME_OS.equals(scheme)) {
188 res.put(uri.name(), SCHEME_OS + ":///");
189 } else {
190 URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(),
191 scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null);
192 res.put(uri.name(), bareUri.toString());
193 }
194 }
195 return res;
196 } catch (Exception e) {
197 throw new UserDirectoryException("Cannot convert " + uri + " to properties", e);
198 }
199 }
200
201 private static URI convertIpaConfig(URI uri) {
202 String path = uri.getPath();
203 String kerberosRealm;
204 if (path == null || path.length() <= 1) {
205 kerberosRealm = kerberosDomainFromDns();
206 } else {
207 kerberosRealm = path.substring(1);
208 }
209
210 if (kerberosRealm == null)
211 throw new UserDirectoryException("No Kerberos domain available for " + uri);
212 try (DnsBrowser dnsBrowser = new DnsBrowser()) {
213 String ldapHostsStr = uri.getHost();
214 if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) {
215 List<String> ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase());
216 if (ldapHosts == null || ldapHosts.size() == 0) {
217 throw new UserDirectoryException("Cannot configure LDAP for IPA " + uri);
218 } else {
219 ldapHostsStr = ldapHosts.get(0);
220 }
221 }
222 URI convertedUri = new URI(
223 SCHEME_LDAP + "://" + ldapHostsStr + "/" + IpaUtils.domainToUserDirectoryConfigPath(kerberosRealm));
224 if (log.isDebugEnabled())
225 log.debug("Converted " + uri + " to " + convertedUri);
226 return convertedUri;
227 } catch (NamingException | IOException | URISyntaxException e) {
228 throw new UserDirectoryException("cannot convert IPA uri " + uri, e);
229 }
230 }
231
232 private static String kerberosDomainFromDns() {
233 String kerberosDomain;
234 try (DnsBrowser dnsBrowser = new DnsBrowser()) {
235 InetAddress localhost = InetAddress.getLocalHost();
236 String hostname = localhost.getHostName();
237 String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
238 kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
239 return kerberosDomain;
240 } catch (Exception e) {
241 throw new UserDirectoryException("Cannot determine Kerberos domain from DNS", e);
242 }
243
244 }
245
246 private static String getBaseDnFromHostname() {
247 String hostname;
248 try {
249 hostname = InetAddress.getLocalHost().getHostName();
250 } catch (UnknownHostException e) {
251 log.warn("Using localhost as hostname", e);
252 hostname = "localhost.localdomain";
253 }
254 int dotIdx = hostname.indexOf('.');
255 if (dotIdx >= 0) {
256 String domain = hostname.substring(dotIdx + 1, hostname.length());
257 String bDn = ("." + domain).replaceAll("\\.", ",dc=");
258 bDn = bDn.substring(1, bDn.length());
259 return bDn;
260 } else {
261 return "dc=" + hostname;
262 }
263 }
264
265 /**
266 * Hash the base DN in order to have a deterministic string to be used as a cn
267 * for the underlying user directory.
268 */
269 public static String baseDnHash(Dictionary<String, Object> properties) {
270 String bDn = (String) properties.get(baseDn.name());
271 if (bDn == null)
272 throw new UserDirectoryException("No baseDn in " + properties);
273 return DigestUtils.sha1str(bDn);
274 }
275 }