]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/osgi/NodeUserAdmin.java
Introduce CMS UX
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / osgi / NodeUserAdmin.java
1 package org.argeo.cms.internal.osgi;
2
3 import java.io.IOException;
4 import java.net.Inet6Address;
5 import java.net.InetAddress;
6 import java.net.URI;
7 import java.net.URISyntaxException;
8 import java.nio.file.Files;
9 import java.nio.file.Path;
10 import java.security.PrivilegedExceptionAction;
11 import java.util.ArrayList;
12 import java.util.Dictionary;
13 import java.util.HashMap;
14 import java.util.Hashtable;
15 import java.util.Iterator;
16 import java.util.Map;
17 import java.util.Set;
18
19 import javax.naming.ldap.LdapName;
20 import javax.security.auth.Subject;
21 import javax.security.auth.callback.Callback;
22 import javax.security.auth.callback.CallbackHandler;
23 import javax.security.auth.callback.NameCallback;
24 import javax.security.auth.callback.UnsupportedCallbackException;
25 import javax.security.auth.kerberos.KerberosPrincipal;
26 import javax.security.auth.login.LoginContext;
27 import javax.security.auth.login.LoginException;
28
29 import org.apache.commons.httpclient.auth.AuthPolicy;
30 import org.apache.commons.httpclient.auth.CredentialsProvider;
31 import org.apache.commons.httpclient.params.DefaultHttpParams;
32 import org.apache.commons.httpclient.params.HttpMethodParams;
33 import org.apache.commons.httpclient.params.HttpParams;
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.cms.internal.http.client.HttpCredentialProvider;
38 import org.argeo.cms.internal.http.client.SpnegoAuthScheme;
39 import org.argeo.cms.internal.runtime.KernelConstants;
40 import org.argeo.cms.internal.runtime.KernelUtils;
41 import org.argeo.osgi.transaction.WorkControl;
42 import org.argeo.osgi.transaction.WorkTransaction;
43 import org.argeo.osgi.useradmin.AbstractUserDirectory;
44 import org.argeo.osgi.useradmin.AggregatingUserAdmin;
45 import org.argeo.osgi.useradmin.LdapUserAdmin;
46 import org.argeo.osgi.useradmin.LdifUserAdmin;
47 import org.argeo.osgi.useradmin.OsUserDirectory;
48 import org.argeo.osgi.useradmin.UserAdminConf;
49 import org.argeo.osgi.useradmin.UserDirectory;
50 import org.argeo.util.naming.DnsBrowser;
51 import org.ietf.jgss.GSSCredential;
52 import org.ietf.jgss.GSSException;
53 import org.ietf.jgss.GSSManager;
54 import org.ietf.jgss.GSSName;
55 import org.ietf.jgss.Oid;
56 import org.osgi.framework.Constants;
57 import org.osgi.service.cm.ConfigurationException;
58 import org.osgi.service.cm.ManagedServiceFactory;
59 import org.osgi.service.useradmin.Authorization;
60 import org.osgi.service.useradmin.Group;
61 import org.osgi.service.useradmin.Role;
62 import org.osgi.service.useradmin.UserAdmin;
63
64 /**
65 * Aggregates multiple {@link UserDirectory} and integrates them with system
66 * roles.
67 */
68 public class NodeUserAdmin extends AggregatingUserAdmin implements ManagedServiceFactory, KernelConstants {
69 private final static CmsLog log = CmsLog.getLog(NodeUserAdmin.class);
70
71 // OSGi
72 private Map<String, LdapName> pidToBaseDn = new HashMap<>();
73 // private Map<String, ServiceRegistration<UserDirectory>> pidToServiceRegs = new HashMap<>();
74 // private ServiceRegistration<UserAdmin> userAdminReg;
75
76 // JTA
77 // private final ServiceTracker<WorkControl, WorkControl> tmTracker;
78 // private final String cacheName = UserDirectory.class.getName();
79
80 // GSS API
81 private Path nodeKeyTab = KernelUtils.getOsgiInstancePath(KernelConstants.NODE_KEY_TAB_PATH);
82 private GSSCredential acceptorCredentials;
83
84 private boolean singleUser = false;
85 // private boolean systemRolesAvailable = false;
86
87 // CmsUserManagerImpl userManager;
88 private WorkControl transactionManager;
89 private WorkTransaction userTransaction;
90
91 public NodeUserAdmin() {
92 super(CmsConstants.ROLES_BASEDN, CmsConstants.TOKENS_BASEDN);
93 // BundleContext bc = Activator.getBundleContext();
94 // if (bc != null) {
95 // tmTracker = new ServiceTracker<>(bc, WorkControl.class, null) {
96 //
97 // @Override
98 // public WorkControl addingService(ServiceReference<WorkControl> reference) {
99 // WorkControl workControl = super.addingService(reference);
100 // userManager = new CmsUserManagerImpl();
101 // userManager.setUserAdmin(NodeUserAdmin.this);
102 // // FIXME make it more robust
103 // userManager.setUserTransaction((WorkTransaction) workControl);
104 // bc.registerService(CmsUserManager.class, userManager, null);
105 // return workControl;
106 // }
107 // };
108 // tmTracker.open();
109 // } else {
110 // tmTracker = null;
111 // }
112 }
113
114 public void start() {
115 }
116
117 public void stop() {
118 }
119
120 @Override
121 public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException {
122 String uri = (String) properties.get(UserAdminConf.uri.name());
123 Object realm = properties.get(UserAdminConf.realm.name());
124 URI u;
125 try {
126 if (uri == null) {
127 String baseDn = (String) properties.get(UserAdminConf.baseDn.name());
128 u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + baseDn + ".ldif");
129 } else if (realm != null) {
130 u = null;
131 } else {
132 u = new URI(uri);
133 }
134 } catch (URISyntaxException e) {
135 throw new IllegalArgumentException("Badly formatted URI " + uri, e);
136 }
137
138 // Create
139 AbstractUserDirectory userDirectory;
140 if (realm != null || UserAdminConf.SCHEME_LDAP.equals(u.getScheme())
141 || UserAdminConf.SCHEME_LDAPS.equals(u.getScheme())) {
142 userDirectory = new LdapUserAdmin(properties);
143 } else if (UserAdminConf.SCHEME_FILE.equals(u.getScheme())) {
144 userDirectory = new LdifUserAdmin(u, properties);
145 } else if (UserAdminConf.SCHEME_OS.equals(u.getScheme())) {
146 userDirectory = new OsUserDirectory(u, properties);
147 singleUser = true;
148 } else {
149 throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
150 }
151 LdapName baseDn = userDirectory.getBaseDn();
152
153 // FIXME make updates more robust
154 if (pidToBaseDn.containsValue(baseDn)) {
155 if (log.isDebugEnabled())
156 log.debug("Ignoring user directory update of " + baseDn);
157 return;
158 }
159
160 addUserDirectory(userDirectory);
161
162 // OSGi
163 Hashtable<String, Object> regProps = new Hashtable<>();
164 regProps.put(Constants.SERVICE_PID, pid);
165 if (isSystemRolesBaseDn(baseDn))
166 regProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
167 regProps.put(UserAdminConf.baseDn.name(), baseDn);
168 // ServiceRegistration<UserDirectory> reg =
169 // bc.registerService(UserDirectory.class, userDirectory, regProps);
170 CmsActivator.getBundleContext().registerService(UserDirectory.class, userDirectory, regProps);
171 // userManager.addUserDirectory(userDirectory, regProps);
172 pidToBaseDn.put(pid, baseDn);
173 // pidToServiceRegs.put(pid, reg);
174
175 if (log.isDebugEnabled()) {
176 log.debug("User directory " + userDirectory.getBaseDn() + (u != null ? " [" + u.getScheme() + "]" : "")
177 + " enabled." + (realm != null ? " " + realm + " realm." : ""));
178 }
179
180 if (isSystemRolesBaseDn(baseDn)) {
181 addStandardSystemRoles();
182
183 // publishes itself as user admin only when system roles are available
184 Dictionary<String, Object> userAdminregProps = new Hashtable<>();
185 userAdminregProps.put(CmsConstants.CN, CmsConstants.DEFAULT);
186 userAdminregProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
187 CmsActivator.getBundleContext().registerService(UserAdmin.class, this, userAdminregProps);
188 }
189
190 // if (isSystemRolesBaseDn(baseDn))
191 // systemRolesAvailable = true;
192 //
193 // // start publishing only when system roles are available
194 // if (systemRolesAvailable) {
195 // // The list of baseDns is published as properties
196 // // TODO clients should rather reference USerDirectory services
197 // if (userAdminReg != null)
198 // userAdminReg.unregister();
199 // // register self as main user admin
200 // Dictionary<String, Object> userAdminregProps = currentState();
201 // userAdminregProps.put(NodeConstants.CN, NodeConstants.DEFAULT);
202 // userAdminregProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
203 // userAdminReg = bc.registerService(UserAdmin.class, this, userAdminregProps);
204 // }
205 }
206
207 private void addStandardSystemRoles() {
208 // we assume UserTransaction is already available (TODO make it more robust)
209 try {
210 userTransaction.begin();
211 Role adminRole = getRole(CmsConstants.ROLE_ADMIN);
212 if (adminRole == null) {
213 adminRole = createRole(CmsConstants.ROLE_ADMIN, Role.GROUP);
214 }
215 if (getRole(CmsConstants.ROLE_USER_ADMIN) == null) {
216 Group userAdminRole = (Group) createRole(CmsConstants.ROLE_USER_ADMIN, Role.GROUP);
217 userAdminRole.addMember(adminRole);
218 }
219 userTransaction.commit();
220 } catch (Exception e) {
221 try {
222 userTransaction.rollback();
223 } catch (Exception e1) {
224 // silent
225 }
226 throw new IllegalStateException("Cannot add standard system roles", e);
227 }
228 }
229
230 @Override
231 public void deleted(String pid) {
232 // assert pidToServiceRegs.get(pid) != null;
233 assert pidToBaseDn.get(pid) != null;
234 // pidToServiceRegs.remove(pid).unregister();
235 LdapName baseDn = pidToBaseDn.remove(pid);
236 removeUserDirectory(baseDn);
237 }
238
239 @Override
240 public String getName() {
241 return "Node User Admin";
242 }
243
244 @Override
245 protected void addAbstractSystemRoles(Authorization rawAuthorization, Set<String> sysRoles) {
246 if (rawAuthorization.getName() == null) {
247 sysRoles.add(CmsConstants.ROLE_ANONYMOUS);
248 } else {
249 sysRoles.add(CmsConstants.ROLE_USER);
250 }
251 }
252
253 protected void postAdd(AbstractUserDirectory userDirectory) {
254 // JTA
255 // WorkControl tm = tmTracker != null ? tmTracker.getService() : null;
256 // if (tm == null)
257 // throw new IllegalStateException("A JTA transaction manager must be available.");
258 userDirectory.setTransactionControl(transactionManager);
259 // if (tmTracker.getService() instanceof BitronixTransactionManager)
260 // EhCacheXAResourceProducer.registerXAResource(cacheName, userDirectory.getXaResource());
261
262 Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
263 if (realm != null) {
264 if (Files.exists(nodeKeyTab)) {
265 String servicePrincipal = getKerberosServicePrincipal(realm.toString());
266 if (servicePrincipal != null) {
267 CallbackHandler callbackHandler = new CallbackHandler() {
268 @Override
269 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
270 for (Callback callback : callbacks)
271 if (callback instanceof NameCallback)
272 ((NameCallback) callback).setName(servicePrincipal);
273
274 }
275 };
276 try {
277 LoginContext nodeLc = new LoginContext(CmsAuth.LOGIN_CONTEXT_NODE, callbackHandler);
278 nodeLc.login();
279 acceptorCredentials = logInAsAcceptor(nodeLc.getSubject(), servicePrincipal);
280 } catch (LoginException e) {
281 throw new IllegalStateException("Cannot log in kernel", e);
282 }
283 }
284 }
285
286 // Register client-side SPNEGO auth scheme
287 AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class);
288 HttpParams params = DefaultHttpParams.getDefaultParams();
289 ArrayList<String> schemes = new ArrayList<>();
290 schemes.add(SpnegoAuthScheme.NAME);// SPNEGO preferred
291 // schemes.add(AuthPolicy.BASIC);// incompatible with Basic
292 params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);
293 params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider());
294 params.setParameter(HttpMethodParams.COOKIE_POLICY, KernelConstants.COOKIE_POLICY_BROWSER_COMPATIBILITY);
295 // params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
296 }
297 }
298
299 protected void preDestroy(AbstractUserDirectory userDirectory) {
300 // if (tmTracker.getService() instanceof BitronixTransactionManager)
301 // EhCacheXAResourceProducer.unregisterXAResource(cacheName, userDirectory.getXaResource());
302
303 Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
304 if (realm != null) {
305 if (acceptorCredentials != null) {
306 try {
307 acceptorCredentials.dispose();
308 } catch (GSSException e) {
309 // silent
310 }
311 acceptorCredentials = null;
312 }
313 }
314 }
315
316 private String getKerberosServicePrincipal(String realm) {
317 String hostname;
318 try (DnsBrowser dnsBrowser = new DnsBrowser()) {
319 InetAddress localhost = InetAddress.getLocalHost();
320 hostname = localhost.getHostName();
321 String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
322 String ipfromDns = dnsBrowser.getRecord(hostname, localhost instanceof Inet6Address ? "AAAA" : "A");
323 boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
324 String kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
325 if (consistentIp && kerberosDomain != null && kerberosDomain.equals(realm) && Files.exists(nodeKeyTab)) {
326 return KernelConstants.DEFAULT_KERBEROS_SERVICE + "/" + hostname + "@" + kerberosDomain;
327 } else
328 return null;
329 } catch (Exception e) {
330 log.warn("Exception when determining kerberos principal", e);
331 return null;
332 }
333 }
334
335 private GSSCredential logInAsAcceptor(Subject subject, String servicePrincipal) {
336 // GSS
337 Iterator<KerberosPrincipal> krb5It = subject.getPrincipals(KerberosPrincipal.class).iterator();
338 if (!krb5It.hasNext())
339 return null;
340 KerberosPrincipal krb5Principal = null;
341 while (krb5It.hasNext()) {
342 KerberosPrincipal principal = krb5It.next();
343 if (principal.getName().equals(servicePrincipal))
344 krb5Principal = principal;
345 }
346
347 if (krb5Principal == null)
348 return null;
349
350 GSSManager manager = GSSManager.getInstance();
351 try {
352 GSSName gssName = manager.createName(krb5Principal.getName(), null);
353 GSSCredential serverCredentials = Subject.doAs(subject, new PrivilegedExceptionAction<GSSCredential>() {
354
355 @Override
356 public GSSCredential run() throws GSSException {
357 return manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, KERBEROS_OID,
358 GSSCredential.ACCEPT_ONLY);
359
360 }
361
362 });
363 if (log.isDebugEnabled())
364 log.debug("GSS acceptor configured for " + krb5Principal);
365 return serverCredentials;
366 } catch (Exception gsse) {
367 throw new IllegalStateException("Cannot create acceptor credentials for " + krb5Principal, gsse);
368 }
369 }
370
371 public GSSCredential getAcceptorCredentials() {
372 return acceptorCredentials;
373 }
374
375 public boolean hasAcceptorCredentials() {
376 return acceptorCredentials != null;
377 }
378
379 public boolean isSingleUser() {
380 return singleUser;
381 }
382
383 public void setTransactionManager(WorkControl transactionManager) {
384 this.transactionManager = transactionManager;
385 }
386
387 public void setUserTransaction(WorkTransaction userTransaction) {
388 this.userTransaction = userTransaction;
389 }
390
391 /*
392 * STATIC
393 */
394
395 public final static Oid KERBEROS_OID;
396 static {
397 try {
398 KERBEROS_OID = new Oid("1.3.6.1.5.5.2");
399 } catch (GSSException e) {
400 throw new IllegalStateException("Cannot create Kerberos OID", e);
401 }
402 }
403 }