]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java
Improve ACR attribute typing.
[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.api.cms.directory.UserDirectory;
33 import org.argeo.api.cms.transaction.WorkControl;
34 import org.argeo.api.cms.transaction.WorkTransaction;
35 import org.argeo.cms.CmsDeployProperty;
36 import org.argeo.cms.dns.DnsBrowser;
37 import org.argeo.cms.osgi.useradmin.AggregatingUserAdmin;
38 import org.argeo.cms.osgi.useradmin.DirectoryUserAdmin;
39 import org.argeo.cms.runtime.DirectoryConf;
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.SYSTEM_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_PRIVATE);
92 List<String> uris = new ArrayList<>();
93
94 // node roles
95 String nodeRolesUri = null;// getFrameworkProp(CmsConstants.ROLES_URI);
96 String baseNodeRoleDn = CmsConstants.SYSTEM_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
181 try {
182 Dictionary<String, Object> properties = DirectoryConf.uriAsProperties(u.toString());
183 res.add(properties);
184 } catch (Exception e) {
185 log.error("Cannot load user directory " + u, e);
186 }
187 }
188
189 return res;
190 }
191
192 public UserDirectory enableUserDirectory(Dictionary<String, ?> properties) {
193 String uri = (String) properties.get(DirectoryConf.uri.name());
194 Object realm = properties.get(DirectoryConf.realm.name());
195 URI u;
196 try {
197 if (uri == null) {
198 String baseDn = (String) properties.get(DirectoryConf.baseDn.name());
199 u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_PRIVATE + '/' + baseDn + ".ldif");
200 } else if (realm != null) {
201 u = null;
202 } else {
203 u = new URI(uri);
204 }
205 } catch (URISyntaxException e) {
206 throw new IllegalArgumentException("Badly formatted URI " + uri, e);
207 }
208
209 // Create
210 UserDirectory userDirectory = new DirectoryUserAdmin(u, properties);
211 // if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme())
212 // || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
213 // userDirectory = new LdapUserAdmin(properties);
214 // } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
215 // userDirectory = new LdifUserAdmin(u, properties);
216 // } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
217 // userDirectory = new OsUserDirectory(u, properties);
218 // singleUser = true;
219 // } else {
220 // throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
221 // }
222 String basePath = userDirectory.getBase();
223
224 addUserDirectory(userDirectory);
225 if (isSystemRolesBaseDn(basePath)) {
226 addStandardSystemRoles();
227 }
228 if (log.isDebugEnabled()) {
229 log.debug("User directory " + userDirectory.getBase() + (u != null ? " [" + u.getScheme() + "]" : "")
230 + " enabled." + (realm != null ? " " + realm + " realm." : ""));
231 }
232 return userDirectory;
233 }
234
235 protected void addStandardSystemRoles() {
236 // we assume UserTransaction is already available (TODO make it more robust)
237 try {
238 userTransaction.begin();
239 Role adminRole = getRole(CmsConstants.ROLE_ADMIN);
240 if (adminRole == null) {
241 adminRole = createRole(CmsConstants.ROLE_ADMIN, Role.GROUP);
242 }
243 if (getRole(CmsConstants.ROLE_USER_ADMIN) == null) {
244 Group userAdminRole = (Group) createRole(CmsConstants.ROLE_USER_ADMIN, Role.GROUP);
245 userAdminRole.addMember(adminRole);
246 }
247 userTransaction.commit();
248 } catch (Exception e) {
249 try {
250 userTransaction.rollback();
251 } catch (Exception e1) {
252 // silent
253 }
254 throw new IllegalStateException("Cannot add standard system roles", e);
255 }
256 }
257
258 @Override
259 protected void addAbstractSystemRoles(Authorization rawAuthorization, Set<String> sysRoles) {
260 if (rawAuthorization.getName() == null) {
261 sysRoles.add(CmsConstants.ROLE_ANONYMOUS);
262 } else {
263 sysRoles.add(CmsConstants.ROLE_USER);
264 }
265 }
266
267 @Override
268 protected void postAdd(UserDirectory userDirectory) {
269 userDirectory.setTransactionControl(transactionManager);
270
271 Optional<String> realm = userDirectory.getRealm();
272 if (realm.isPresent()) {
273 loadIpaJaasConfiguration();
274 if (Files.exists(nodeKeyTab)) {
275 String servicePrincipal = getKerberosServicePrincipal(realm.get());
276 if (servicePrincipal != null) {
277 CallbackHandler callbackHandler = new CallbackHandler() {
278 @Override
279 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
280 for (Callback callback : callbacks)
281 if (callback instanceof NameCallback)
282 ((NameCallback) callback).setName(servicePrincipal);
283
284 }
285 };
286 try {
287 LoginContext nodeLc = CmsAuth.NODE.newLoginContext(callbackHandler);
288 nodeLc.login();
289 acceptorCredentials = logInAsAcceptor(nodeLc.getSubject(), servicePrincipal);
290 } catch (LoginException e) {
291 throw new IllegalStateException("Cannot log in kernel", e);
292 }
293 }
294 }
295
296 }
297 }
298
299 @Override
300 protected void preDestroy(UserDirectory userDirectory) {
301 Optional<String> realm = userDirectory.getRealm();
302 if (realm.isPresent()) {
303 if (acceptorCredentials != null) {
304 try {
305 acceptorCredentials.dispose();
306 } catch (GSSException e) {
307 // silent
308 }
309 acceptorCredentials = null;
310 }
311 }
312 }
313
314 private void loadIpaJaasConfiguration() {
315 if (CmsStateImpl.getDeployProperty(cmsState, CmsDeployProperty.JAVA_LOGIN_CONFIG) == null) {
316 String jaasConfig = KernelConstants.JAAS_CONFIG_IPA;
317 URL url = getClass().getClassLoader().getResource(jaasConfig);
318 KernelUtils.setJaasConfiguration(url);
319 log.debug("Set IPA JAAS configuration.");
320 }
321 }
322
323 protected String getKerberosServicePrincipal(String realm) {
324 if (!Files.exists(nodeKeyTab))
325 return null;
326 List<String> dns = CmsStateImpl.getDeployProperties(cmsState, CmsDeployProperty.DNS);
327 String hostname = CmsStateImpl.getDeployProperty(cmsState, CmsDeployProperty.HOST);
328 try (DnsBrowser dnsBrowser = new DnsBrowser(dns)) {
329 hostname = hostname != null ? hostname : InetAddress.getLocalHost().getHostName();
330 String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
331 String ipv4fromDns = dnsBrowser.getRecord(hostname, "A");
332 String ipv6fromDns = dnsBrowser.getRecord(hostname, "AAAA");
333 if (ipv4fromDns == null && ipv6fromDns == null)
334 throw new IllegalStateException("hostname " + hostname + " is not registered in DNS");
335 // boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
336 String kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
337 if (kerberosDomain != null && kerberosDomain.equals(realm)) {
338 return KernelConstants.DEFAULT_KERBEROS_SERVICE + "/" + hostname + "@" + kerberosDomain;
339 } else
340 return null;
341 } catch (Exception e) {
342 log.warn("Exception when determining kerberos principal", e);
343 return null;
344 }
345 }
346
347 private GSSCredential logInAsAcceptor(Subject subject, String servicePrincipal) {
348 // not static because class is not supported by Android
349 final Oid KERBEROS_OID;
350 try {
351 KERBEROS_OID = new Oid("1.3.6.1.5.5.2");
352 } catch (GSSException e) {
353 throw new IllegalStateException("Cannot create Kerberos OID", e);
354 }
355 // GSS
356 Iterator<KerberosPrincipal> krb5It = subject.getPrincipals(KerberosPrincipal.class).iterator();
357 if (!krb5It.hasNext())
358 return null;
359 KerberosPrincipal krb5Principal = null;
360 while (krb5It.hasNext()) {
361 KerberosPrincipal principal = krb5It.next();
362 if (principal.getName().equals(servicePrincipal))
363 krb5Principal = principal;
364 }
365
366 if (krb5Principal == null)
367 return null;
368
369 GSSManager manager = GSSManager.getInstance();
370 try {
371 GSSName gssName = manager.createName(krb5Principal.getName(), null);
372 GSSCredential serverCredentials = Subject.doAs(subject, new PrivilegedExceptionAction<GSSCredential>() {
373
374 @Override
375 public GSSCredential run() throws GSSException {
376 return manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, KERBEROS_OID,
377 GSSCredential.ACCEPT_ONLY);
378
379 }
380
381 });
382 if (log.isDebugEnabled())
383 log.debug("GSS acceptor configured for " + krb5Principal);
384 return serverCredentials;
385 } catch (Exception gsse) {
386 throw new IllegalStateException("Cannot create acceptor credentials for " + krb5Principal, gsse);
387 }
388 }
389
390 public GSSCredential getAcceptorCredentials() {
391 return acceptorCredentials;
392 }
393
394 public boolean hasAcceptorCredentials() {
395 return acceptorCredentials != null;
396 }
397
398 public boolean isSingleUser() {
399 return singleUser;
400 }
401
402 public void setTransactionManager(WorkControl transactionManager) {
403 this.transactionManager = transactionManager;
404 }
405
406 public void setUserTransaction(WorkTransaction userTransaction) {
407 this.userTransaction = userTransaction;
408 }
409
410 public void setCmsState(CmsState cmsState) {
411 this.cmsState = cmsState;
412 }
413
414 }