]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java
Fix static CMS initialisation.
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / runtime / CmsUserAdmin.java
1 package org.argeo.cms.internal.runtime;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.net.InetAddress;
6 import java.net.URI;
7 import java.net.URISyntaxException;
8 import java.net.URL;
9 import java.nio.file.Files;
10 import java.nio.file.Path;
11 import java.security.PrivilegedExceptionAction;
12 import java.util.ArrayList;
13 import java.util.Dictionary;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.Optional;
17 import java.util.Set;
18
19 import javax.security.auth.Subject;
20 import javax.security.auth.callback.Callback;
21 import javax.security.auth.callback.CallbackHandler;
22 import javax.security.auth.callback.NameCallback;
23 import javax.security.auth.callback.UnsupportedCallbackException;
24 import javax.security.auth.kerberos.KerberosPrincipal;
25 import javax.security.auth.login.LoginContext;
26 import javax.security.auth.login.LoginException;
27
28 import org.apache.commons.httpclient.auth.AuthPolicy;
29 import org.apache.commons.httpclient.auth.CredentialsProvider;
30 import org.apache.commons.httpclient.params.DefaultHttpParams;
31 import org.apache.commons.httpclient.params.HttpMethodParams;
32 import org.apache.commons.httpclient.params.HttpParams;
33 import org.apache.commons.io.FileUtils;
34 import org.argeo.api.cms.CmsAuth;
35 import org.argeo.api.cms.CmsConstants;
36 import org.argeo.api.cms.CmsLog;
37 import org.argeo.api.cms.CmsState;
38 import org.argeo.cms.CmsDeployProperty;
39 import org.argeo.cms.internal.http.client.HttpCredentialProvider;
40 import org.argeo.cms.internal.http.client.SpnegoAuthScheme;
41 import org.argeo.osgi.useradmin.AggregatingUserAdmin;
42 import org.argeo.osgi.useradmin.DirectoryUserAdmin;
43 import org.argeo.osgi.useradmin.UserDirectory;
44 import org.argeo.util.directory.DirectoryConf;
45 import org.argeo.util.naming.dns.DnsBrowser;
46 import org.argeo.util.transaction.WorkControl;
47 import org.argeo.util.transaction.WorkTransaction;
48 import org.ietf.jgss.GSSCredential;
49 import org.ietf.jgss.GSSException;
50 import org.ietf.jgss.GSSManager;
51 import org.ietf.jgss.GSSName;
52 import org.ietf.jgss.Oid;
53 import org.osgi.service.useradmin.Authorization;
54 import org.osgi.service.useradmin.Group;
55 import org.osgi.service.useradmin.Role;
56
57 /**
58 * Aggregates multiple {@link UserDirectory} and integrates them with system
59 * roles.
60 */
61 public class CmsUserAdmin extends AggregatingUserAdmin {
62 private final static CmsLog log = CmsLog.getLog(CmsUserAdmin.class);
63
64 // GSS API
65 private Path nodeKeyTab = KernelUtils.getOsgiInstancePath(KernelConstants.NODE_KEY_TAB_PATH);
66 private GSSCredential acceptorCredentials;
67
68 private boolean singleUser = false;
69
70 private WorkControl transactionManager;
71 private WorkTransaction userTransaction;
72
73 private CmsState cmsState;
74
75 public CmsUserAdmin() {
76 super(CmsConstants.ROLES_BASEDN, CmsConstants.TOKENS_BASEDN);
77 }
78
79 public void start() {
80 super.start();
81 List<Dictionary<String, Object>> configs = getUserDirectoryConfigs();
82 for (Dictionary<String, Object> config : configs) {
83 enableUserDirectory(config);
84 // if (userDirectory.getRealm().isPresent())
85 // loadIpaJaasConfiguration();
86 }
87 log.debug(() -> "CMS user admin available");
88 }
89
90 public void stop() {
91 // for (UserDirectory userDirectory : getUserDirectories()) {
92 // removeUserDirectory(userDirectory);
93 // }
94 super.stop();
95 }
96
97 protected List<Dictionary<String, Object>> getUserDirectoryConfigs() {
98 List<Dictionary<String, Object>> res = new ArrayList<>();
99 File nodeBaseDir = cmsState.getDataPath(KernelConstants.DIR_NODE).toFile();
100 List<String> uris = new ArrayList<>();
101
102 // node roles
103 String nodeRolesUri = null;// getFrameworkProp(CmsConstants.ROLES_URI);
104 String baseNodeRoleDn = CmsConstants.ROLES_BASEDN;
105 if (nodeRolesUri == null) {
106 nodeRolesUri = baseNodeRoleDn + ".ldif";
107 File nodeRolesFile = new File(nodeBaseDir, nodeRolesUri);
108 if (!nodeRolesFile.exists())
109 try {
110 FileUtils.copyInputStreamToFile(CmsUserAdmin.class.getResourceAsStream(baseNodeRoleDn + ".ldif"),
111 nodeRolesFile);
112 } catch (IOException e) {
113 throw new RuntimeException("Cannot copy demo resource", e);
114 }
115 // nodeRolesUri = nodeRolesFile.toURI().toString();
116 }
117 uris.add(nodeRolesUri);
118
119 // node tokens
120 String nodeTokensUri = null;// getFrameworkProp(CmsConstants.TOKENS_URI);
121 String baseNodeTokensDn = CmsConstants.TOKENS_BASEDN;
122 if (nodeTokensUri == null) {
123 nodeTokensUri = baseNodeTokensDn + ".ldif";
124 File nodeTokensFile = new File(nodeBaseDir, nodeTokensUri);
125 if (!nodeTokensFile.exists())
126 try {
127 FileUtils.copyInputStreamToFile(CmsUserAdmin.class.getResourceAsStream(baseNodeTokensDn + ".ldif"),
128 nodeTokensFile);
129 } catch (IOException e) {
130 throw new RuntimeException("Cannot copy demo resource", e);
131 }
132 // nodeRolesUri = nodeRolesFile.toURI().toString();
133 }
134 uris.add(nodeTokensUri);
135
136 // Business roles
137 // String userAdminUris = getFrameworkProp(CmsConstants.USERADMIN_URIS);
138 List<String> userAdminUris = CmsStateImpl.getDeployProperties(cmsState, CmsDeployProperty.DIRECTORY);// getFrameworkProp(CmsConstants.USERADMIN_URIS);
139 for (String userAdminUri : userAdminUris) {
140 if (userAdminUri == null)
141 continue;
142 // if (!userAdminUri.trim().equals(""))
143 uris.add(userAdminUri);
144 }
145
146 if (uris.size() == 0) {
147 // TODO put this somewhere else
148 String demoBaseDn = "dc=example,dc=com";
149 String userAdminUri = demoBaseDn + ".ldif";
150 File businessRolesFile = new File(nodeBaseDir, userAdminUri);
151 File systemRolesFile = new File(nodeBaseDir, "ou=roles,ou=node.ldif");
152 if (!businessRolesFile.exists())
153 try {
154 FileUtils.copyInputStreamToFile(CmsUserAdmin.class.getResourceAsStream(demoBaseDn + ".ldif"),
155 businessRolesFile);
156 if (!systemRolesFile.exists())
157 FileUtils.copyInputStreamToFile(
158 CmsUserAdmin.class.getResourceAsStream("example-ou=roles,ou=node.ldif"),
159 systemRolesFile);
160 } catch (IOException e) {
161 throw new RuntimeException("Cannot copy demo resources", e);
162 }
163 // userAdminUris = businessRolesFile.toURI().toString();
164 log.warn("## DEV Using dummy base DN " + demoBaseDn);
165 // TODO downgrade security level
166 }
167
168 // Interprets URIs
169 for (String uri : uris) {
170 URI u;
171 try {
172 u = new URI(uri);
173 if (u.getPath() == null)
174 throw new IllegalArgumentException(
175 "URI " + uri + " must have a path in order to determine base DN");
176 if (u.getScheme() == null) {
177 if (uri.startsWith("/") || uri.startsWith("./") || uri.startsWith("../"))
178 u = new File(uri).getCanonicalFile().toURI();
179 else if (!uri.contains("/")) {
180 // u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + uri);
181 u = new URI(uri);
182 } else
183 throw new IllegalArgumentException("Cannot interpret " + uri + " as an uri");
184 } else if (u.getScheme().equals(DirectoryConf.SCHEME_FILE)) {
185 u = new File(u).getCanonicalFile().toURI();
186 }
187 } catch (Exception e) {
188 throw new RuntimeException("Cannot interpret " + uri + " as an uri", e);
189 }
190 Dictionary<String, Object> properties = DirectoryConf.uriAsProperties(u.toString());
191 res.add(properties);
192 }
193
194 return res;
195 }
196
197 public UserDirectory enableUserDirectory(Dictionary<String, ?> properties) {
198 String uri = (String) properties.get(DirectoryConf.uri.name());
199 Object realm = properties.get(DirectoryConf.realm.name());
200 URI u;
201 try {
202 if (uri == null) {
203 String baseDn = (String) properties.get(DirectoryConf.baseDn.name());
204 u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + baseDn + ".ldif");
205 } else if (realm != null) {
206 u = null;
207 } else {
208 u = new URI(uri);
209 }
210 } catch (URISyntaxException e) {
211 throw new IllegalArgumentException("Badly formatted URI " + uri, e);
212 }
213
214 // Create
215 UserDirectory userDirectory = new DirectoryUserAdmin(u, properties);
216 // if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme())
217 // || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
218 // userDirectory = new LdapUserAdmin(properties);
219 // } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
220 // userDirectory = new LdifUserAdmin(u, properties);
221 // } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
222 // userDirectory = new OsUserDirectory(u, properties);
223 // singleUser = true;
224 // } else {
225 // throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
226 // }
227 String basePath = userDirectory.getContext();
228
229 addUserDirectory(userDirectory);
230 if (isSystemRolesBaseDn(basePath)) {
231 addStandardSystemRoles();
232 }
233 if (log.isDebugEnabled()) {
234 log.debug("User directory " + userDirectory.getContext() + (u != null ? " [" + u.getScheme() + "]" : "")
235 + " enabled." + (realm != null ? " " + realm + " realm." : ""));
236 }
237 return userDirectory;
238 }
239
240 protected void addStandardSystemRoles() {
241 // we assume UserTransaction is already available (TODO make it more robust)
242 try {
243 userTransaction.begin();
244 Role adminRole = getRole(CmsConstants.ROLE_ADMIN);
245 if (adminRole == null) {
246 adminRole = createRole(CmsConstants.ROLE_ADMIN, Role.GROUP);
247 }
248 if (getRole(CmsConstants.ROLE_USER_ADMIN) == null) {
249 Group userAdminRole = (Group) createRole(CmsConstants.ROLE_USER_ADMIN, Role.GROUP);
250 userAdminRole.addMember(adminRole);
251 }
252 userTransaction.commit();
253 } catch (Exception e) {
254 try {
255 userTransaction.rollback();
256 } catch (Exception e1) {
257 // silent
258 }
259 throw new IllegalStateException("Cannot add standard system roles", e);
260 }
261 }
262
263 @Override
264 protected void addAbstractSystemRoles(Authorization rawAuthorization, Set<String> sysRoles) {
265 if (rawAuthorization.getName() == null) {
266 sysRoles.add(CmsConstants.ROLE_ANONYMOUS);
267 } else {
268 sysRoles.add(CmsConstants.ROLE_USER);
269 }
270 }
271
272 @Override
273 protected void postAdd(UserDirectory userDirectory) {
274 userDirectory.setTransactionControl(transactionManager);
275
276 Optional<String> realm = userDirectory.getRealm();
277 if (realm.isPresent()) {
278 loadIpaJaasConfiguration();
279 if (Files.exists(nodeKeyTab)) {
280 String servicePrincipal = getKerberosServicePrincipal(realm.get());
281 if (servicePrincipal != null) {
282 CallbackHandler callbackHandler = new CallbackHandler() {
283 @Override
284 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
285 for (Callback callback : callbacks)
286 if (callback instanceof NameCallback)
287 ((NameCallback) callback).setName(servicePrincipal);
288
289 }
290 };
291 try {
292 LoginContext nodeLc = CmsAuth.NODE.newLoginContext(callbackHandler);
293 nodeLc.login();
294 acceptorCredentials = logInAsAcceptor(nodeLc.getSubject(), servicePrincipal);
295 } catch (LoginException e) {
296 throw new IllegalStateException("Cannot log in kernel", e);
297 }
298 }
299 }
300
301 // Register client-side SPNEGO auth scheme
302 AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class);
303 HttpParams params = DefaultHttpParams.getDefaultParams();
304 ArrayList<String> schemes = new ArrayList<>();
305 schemes.add(SpnegoAuthScheme.NAME);// SPNEGO preferred
306 // schemes.add(AuthPolicy.BASIC);// incompatible with Basic
307 params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);
308 params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider());
309 params.setParameter(HttpMethodParams.COOKIE_POLICY, KernelConstants.COOKIE_POLICY_BROWSER_COMPATIBILITY);
310 // params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
311 }
312 }
313
314 @Override
315 protected void preDestroy(UserDirectory userDirectory) {
316 Optional<String> realm = userDirectory.getRealm();
317 if (realm.isPresent()) {
318 if (acceptorCredentials != null) {
319 try {
320 acceptorCredentials.dispose();
321 } catch (GSSException e) {
322 // silent
323 }
324 acceptorCredentials = null;
325 }
326 }
327 }
328
329 private void loadIpaJaasConfiguration() {
330 if (CmsStateImpl.getDeployProperty(cmsState, CmsDeployProperty.JAVA_LOGIN_CONFIG) == null) {
331 String jaasConfig = KernelConstants.JAAS_CONFIG_IPA;
332 URL url = getClass().getClassLoader().getResource(jaasConfig);
333 KernelUtils.setJaasConfiguration(url);
334 log.debug("Set IPA JAAS configuration.");
335 }
336 }
337
338 protected String getKerberosServicePrincipal(String realm) {
339 if (!Files.exists(nodeKeyTab))
340 return null;
341 List<String> dns = CmsStateImpl.getDeployProperties(cmsState, CmsDeployProperty.DNS);
342 String hostname = CmsStateImpl.getDeployProperty(cmsState, CmsDeployProperty.HOST);
343 try (DnsBrowser dnsBrowser = new DnsBrowser(dns)) {
344 hostname = hostname != null ? hostname : InetAddress.getLocalHost().getHostName();
345 String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
346 String ipv4fromDns = dnsBrowser.getRecord(hostname, "A");
347 String ipv6fromDns = dnsBrowser.getRecord(hostname, "AAAA");
348 if (ipv4fromDns == null && ipv6fromDns == null)
349 throw new IllegalStateException("hostname " + hostname + " is not registered in DNS");
350 // boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
351 String kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
352 if (kerberosDomain != null && kerberosDomain.equals(realm)) {
353 return KernelConstants.DEFAULT_KERBEROS_SERVICE + "/" + hostname + "@" + kerberosDomain;
354 } else
355 return null;
356 } catch (Exception e) {
357 log.warn("Exception when determining kerberos principal", e);
358 return null;
359 }
360 }
361
362 private GSSCredential logInAsAcceptor(Subject subject, String servicePrincipal) {
363 // not static because class is not supported by Android
364 final Oid KERBEROS_OID;
365 try {
366 KERBEROS_OID = new Oid("1.3.6.1.5.5.2");
367 } catch (GSSException e) {
368 throw new IllegalStateException("Cannot create Kerberos OID", e);
369 }
370 // GSS
371 Iterator<KerberosPrincipal> krb5It = subject.getPrincipals(KerberosPrincipal.class).iterator();
372 if (!krb5It.hasNext())
373 return null;
374 KerberosPrincipal krb5Principal = null;
375 while (krb5It.hasNext()) {
376 KerberosPrincipal principal = krb5It.next();
377 if (principal.getName().equals(servicePrincipal))
378 krb5Principal = principal;
379 }
380
381 if (krb5Principal == null)
382 return null;
383
384 GSSManager manager = GSSManager.getInstance();
385 try {
386 GSSName gssName = manager.createName(krb5Principal.getName(), null);
387 GSSCredential serverCredentials = Subject.doAs(subject, new PrivilegedExceptionAction<GSSCredential>() {
388
389 @Override
390 public GSSCredential run() throws GSSException {
391 return manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, KERBEROS_OID,
392 GSSCredential.ACCEPT_ONLY);
393
394 }
395
396 });
397 if (log.isDebugEnabled())
398 log.debug("GSS acceptor configured for " + krb5Principal);
399 return serverCredentials;
400 } catch (Exception gsse) {
401 throw new IllegalStateException("Cannot create acceptor credentials for " + krb5Principal, gsse);
402 }
403 }
404
405 public GSSCredential getAcceptorCredentials() {
406 return acceptorCredentials;
407 }
408
409 public boolean hasAcceptorCredentials() {
410 return acceptorCredentials != null;
411 }
412
413 public boolean isSingleUser() {
414 return singleUser;
415 }
416
417 public void setTransactionManager(WorkControl transactionManager) {
418 this.transactionManager = transactionManager;
419 }
420
421 public void setUserTransaction(WorkTransaction userTransaction) {
422 this.userTransaction = userTransaction;
423 }
424
425 public void setCmsState(CmsState cmsState) {
426 this.cmsState = cmsState;
427 }
428
429 }