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