]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeUserAdmin.java
Introduce CMS Servlet.
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / kernel / NodeUserAdmin.java
1 package org.argeo.cms.internal.kernel;
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.apache.commons.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36 import org.argeo.api.NodeConstants;
37 import org.argeo.cms.CmsUserManager;
38 import org.argeo.cms.internal.auth.CmsUserManagerImpl;
39 import org.argeo.cms.internal.http.client.HttpCredentialProvider;
40 import org.argeo.cms.internal.http.client.SpnegoAuthScheme;
41 import org.argeo.naming.DnsBrowser;
42 import org.argeo.osgi.transaction.WorkControl;
43 import org.argeo.osgi.transaction.WorkTransaction;
44 import org.argeo.osgi.useradmin.AbstractUserDirectory;
45 import org.argeo.osgi.useradmin.AggregatingUserAdmin;
46 import org.argeo.osgi.useradmin.LdapUserAdmin;
47 import org.argeo.osgi.useradmin.LdifUserAdmin;
48 import org.argeo.osgi.useradmin.OsUserDirectory;
49 import org.argeo.osgi.useradmin.UserAdminConf;
50 import org.argeo.osgi.useradmin.UserDirectory;
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.BundleContext;
57 import org.osgi.framework.Constants;
58 import org.osgi.framework.ServiceReference;
59 import org.osgi.service.cm.ConfigurationException;
60 import org.osgi.service.cm.ManagedServiceFactory;
61 import org.osgi.service.useradmin.Authorization;
62 import org.osgi.service.useradmin.UserAdmin;
63 import org.osgi.util.tracker.ServiceTracker;
64
65 /**
66 * Aggregates multiple {@link UserDirectory} and integrates them with system
67 * roles.
68 */
69 class NodeUserAdmin extends AggregatingUserAdmin implements ManagedServiceFactory, KernelConstants {
70 private final static Log log = LogFactory.getLog(NodeUserAdmin.class);
71 // private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
72
73 // OSGi
74 private Map<String, LdapName> pidToBaseDn = new HashMap<>();
75 // private Map<String, ServiceRegistration<UserDirectory>> pidToServiceRegs = new HashMap<>();
76 // private ServiceRegistration<UserAdmin> userAdminReg;
77
78 // JTA
79 private final ServiceTracker<WorkControl, WorkControl> tmTracker;
80 // private final String cacheName = UserDirectory.class.getName();
81
82 // GSS API
83 private Path nodeKeyTab = KernelUtils.getOsgiInstancePath(KernelConstants.NODE_KEY_TAB_PATH);
84 private GSSCredential acceptorCredentials;
85
86 private boolean singleUser = false;
87 // private boolean systemRolesAvailable = false;
88
89 CmsUserManagerImpl userManager;
90
91 public NodeUserAdmin(String systemRolesBaseDn, String tokensBaseDn) {
92 super(systemRolesBaseDn, tokensBaseDn);
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 @Override
115 public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException {
116 String uri = (String) properties.get(UserAdminConf.uri.name());
117 Object realm = properties.get(UserAdminConf.realm.name());
118 URI u;
119 try {
120 if (uri == null) {
121 String baseDn = (String) properties.get(UserAdminConf.baseDn.name());
122 u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + baseDn + ".ldif");
123 } else if (realm != null) {
124 u = null;
125 } else {
126 u = new URI(uri);
127 }
128 } catch (URISyntaxException e) {
129 throw new IllegalArgumentException("Badly formatted URI " + uri, e);
130 }
131
132 // Create
133 AbstractUserDirectory userDirectory;
134 if (realm != null || UserAdminConf.SCHEME_LDAP.equals(u.getScheme())
135 || UserAdminConf.SCHEME_LDAPS.equals(u.getScheme())) {
136 userDirectory = new LdapUserAdmin(properties);
137 } else if (UserAdminConf.SCHEME_FILE.equals(u.getScheme())) {
138 userDirectory = new LdifUserAdmin(u, properties);
139 } else if (UserAdminConf.SCHEME_OS.equals(u.getScheme())) {
140 userDirectory = new OsUserDirectory(u, properties);
141 singleUser = true;
142 } else {
143 throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
144 }
145 addUserDirectory(userDirectory);
146
147 // OSGi
148 LdapName baseDn = userDirectory.getBaseDn();
149 Hashtable<String, Object> regProps = new Hashtable<>();
150 regProps.put(Constants.SERVICE_PID, pid);
151 if (isSystemRolesBaseDn(baseDn))
152 regProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
153 regProps.put(UserAdminConf.baseDn.name(), baseDn);
154 // ServiceRegistration<UserDirectory> reg =
155 // bc.registerService(UserDirectory.class, userDirectory, regProps);
156 Activator.registerService(UserDirectory.class, userDirectory, regProps);
157 userManager.addUserDirectory(userDirectory, regProps);
158 pidToBaseDn.put(pid, baseDn);
159 // pidToServiceRegs.put(pid, reg);
160
161 if (log.isDebugEnabled()) {
162 log.debug("User directory " + userDirectory.getBaseDn() + (u != null ? " [" + u.getScheme() + "]" : "")
163 + " enabled." + (realm != null ? " " + realm + " realm." : ""));
164 }
165
166 if (isSystemRolesBaseDn(baseDn)) {
167 // publishes only when system roles are available
168 Dictionary<String, Object> userAdminregProps = new Hashtable<>();
169 userAdminregProps.put(NodeConstants.CN, NodeConstants.DEFAULT);
170 userAdminregProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
171 Activator.registerService(UserAdmin.class, this, userAdminregProps);
172 }
173
174 // if (isSystemRolesBaseDn(baseDn))
175 // systemRolesAvailable = true;
176 //
177 // // start publishing only when system roles are available
178 // if (systemRolesAvailable) {
179 // // The list of baseDns is published as properties
180 // // TODO clients should rather reference USerDirectory services
181 // if (userAdminReg != null)
182 // userAdminReg.unregister();
183 // // register self as main user admin
184 // Dictionary<String, Object> userAdminregProps = currentState();
185 // userAdminregProps.put(NodeConstants.CN, NodeConstants.DEFAULT);
186 // userAdminregProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
187 // userAdminReg = bc.registerService(UserAdmin.class, this, userAdminregProps);
188 // }
189 }
190
191 @Override
192 public void deleted(String pid) {
193 // assert pidToServiceRegs.get(pid) != null;
194 assert pidToBaseDn.get(pid) != null;
195 // pidToServiceRegs.remove(pid).unregister();
196 LdapName baseDn = pidToBaseDn.remove(pid);
197 removeUserDirectory(baseDn);
198 }
199
200 @Override
201 public String getName() {
202 return "Node User Admin";
203 }
204
205 @Override
206 protected void addAbstractSystemRoles(Authorization rawAuthorization, Set<String> sysRoles) {
207 if (rawAuthorization.getName() == null) {
208 sysRoles.add(NodeConstants.ROLE_ANONYMOUS);
209 } else {
210 sysRoles.add(NodeConstants.ROLE_USER);
211 }
212 }
213
214 protected void postAdd(AbstractUserDirectory userDirectory) {
215 // JTA
216 WorkControl tm = tmTracker != null ? tmTracker.getService() : null;
217 if (tm == null)
218 throw new IllegalStateException("A JTA transaction manager must be available.");
219 userDirectory.setTransactionControl(tm);
220 // if (tmTracker.getService() instanceof BitronixTransactionManager)
221 // EhCacheXAResourceProducer.registerXAResource(cacheName, userDirectory.getXaResource());
222
223 Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
224 if (realm != null) {
225 if (Files.exists(nodeKeyTab)) {
226 String servicePrincipal = getKerberosServicePrincipal(realm.toString());
227 if (servicePrincipal != null) {
228 CallbackHandler callbackHandler = new CallbackHandler() {
229 @Override
230 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
231 for (Callback callback : callbacks)
232 if (callback instanceof NameCallback)
233 ((NameCallback) callback).setName(servicePrincipal);
234
235 }
236 };
237 try {
238 LoginContext nodeLc = new LoginContext(NodeConstants.LOGIN_CONTEXT_NODE, callbackHandler);
239 nodeLc.login();
240 acceptorCredentials = logInAsAcceptor(nodeLc.getSubject(), servicePrincipal);
241 } catch (LoginException e) {
242 throw new IllegalStateException("Cannot log in kernel", e);
243 }
244 }
245 }
246
247 // Register client-side SPNEGO auth scheme
248 AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class);
249 HttpParams params = DefaultHttpParams.getDefaultParams();
250 ArrayList<String> schemes = new ArrayList<>();
251 schemes.add(SpnegoAuthScheme.NAME);// SPNEGO preferred
252 // schemes.add(AuthPolicy.BASIC);// incompatible with Basic
253 params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);
254 params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider());
255 params.setParameter(HttpMethodParams.COOKIE_POLICY, KernelConstants.COOKIE_POLICY_BROWSER_COMPATIBILITY);
256 // params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
257 }
258 }
259
260 protected void preDestroy(AbstractUserDirectory userDirectory) {
261 // if (tmTracker.getService() instanceof BitronixTransactionManager)
262 // EhCacheXAResourceProducer.unregisterXAResource(cacheName, userDirectory.getXaResource());
263
264 Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
265 if (realm != null) {
266 if (acceptorCredentials != null) {
267 try {
268 acceptorCredentials.dispose();
269 } catch (GSSException e) {
270 // silent
271 }
272 acceptorCredentials = null;
273 }
274 }
275 }
276
277 private String getKerberosServicePrincipal(String realm) {
278 String hostname;
279 try (DnsBrowser dnsBrowser = new DnsBrowser()) {
280 InetAddress localhost = InetAddress.getLocalHost();
281 hostname = localhost.getHostName();
282 String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
283 String ipfromDns = dnsBrowser.getRecord(hostname, localhost instanceof Inet6Address ? "AAAA" : "A");
284 boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
285 String kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
286 if (consistentIp && kerberosDomain != null && kerberosDomain.equals(realm) && Files.exists(nodeKeyTab)) {
287 return KernelConstants.DEFAULT_KERBEROS_SERVICE + "/" + hostname + "@" + kerberosDomain;
288 } else
289 return null;
290 } catch (Exception e) {
291 log.warn("Exception when determining kerberos principal", e);
292 return null;
293 }
294 }
295
296 private GSSCredential logInAsAcceptor(Subject subject, String servicePrincipal) {
297 // GSS
298 Iterator<KerberosPrincipal> krb5It = subject.getPrincipals(KerberosPrincipal.class).iterator();
299 if (!krb5It.hasNext())
300 return null;
301 KerberosPrincipal krb5Principal = null;
302 while (krb5It.hasNext()) {
303 KerberosPrincipal principal = krb5It.next();
304 if (principal.getName().equals(servicePrincipal))
305 krb5Principal = principal;
306 }
307
308 if (krb5Principal == null)
309 return null;
310
311 GSSManager manager = GSSManager.getInstance();
312 try {
313 GSSName gssName = manager.createName(krb5Principal.getName(), null);
314 GSSCredential serverCredentials = Subject.doAs(subject, new PrivilegedExceptionAction<GSSCredential>() {
315
316 @Override
317 public GSSCredential run() throws GSSException {
318 return manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, KERBEROS_OID,
319 GSSCredential.ACCEPT_ONLY);
320
321 }
322
323 });
324 if (log.isDebugEnabled())
325 log.debug("GSS acceptor configured for " + krb5Principal);
326 return serverCredentials;
327 } catch (Exception gsse) {
328 throw new IllegalStateException("Cannot create acceptor credentials for " + krb5Principal, gsse);
329 }
330 }
331
332 public GSSCredential getAcceptorCredentials() {
333 return acceptorCredentials;
334 }
335
336 public boolean isSingleUser() {
337 return singleUser;
338 }
339
340 public final static Oid KERBEROS_OID;
341 static {
342 try {
343 KERBEROS_OID = new Oid("1.3.6.1.5.5.2");
344 } catch (GSSException e) {
345 throw new IllegalStateException("Cannot create Kerberos OID", e);
346 }
347 }
348
349 }