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