]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeUserAdmin.java
[maven-release-plugin] prepare release argeo-commons-2.1.70
[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
18 import javax.naming.ldap.LdapName;
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 import javax.transaction.TransactionManager;
28
29 import org.apache.commons.httpclient.auth.AuthPolicy;
30 import org.apache.commons.httpclient.auth.CredentialsProvider;
31 import org.apache.commons.httpclient.cookie.CookiePolicy;
32 import org.apache.commons.httpclient.params.DefaultHttpParams;
33 import org.apache.commons.httpclient.params.HttpMethodParams;
34 import org.apache.commons.httpclient.params.HttpParams;
35 import org.apache.commons.logging.Log;
36 import org.apache.commons.logging.LogFactory;
37 import org.argeo.cms.CmsException;
38 import org.argeo.cms.internal.http.NodeHttp;
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.node.NodeConstants;
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.UserAdminConf;
48 import org.argeo.osgi.useradmin.UserDirectory;
49 import org.ietf.jgss.GSSCredential;
50 import org.ietf.jgss.GSSException;
51 import org.ietf.jgss.GSSManager;
52 import org.ietf.jgss.GSSName;
53 import org.ietf.jgss.Oid;
54 import org.osgi.framework.BundleContext;
55 import org.osgi.framework.Constants;
56 import org.osgi.framework.FrameworkUtil;
57 import org.osgi.framework.ServiceRegistration;
58 import org.osgi.service.cm.ConfigurationException;
59 import org.osgi.service.cm.ManagedServiceFactory;
60 import org.osgi.service.useradmin.UserAdmin;
61 import org.osgi.util.tracker.ServiceTracker;
62
63 import bitronix.tm.BitronixTransactionManager;
64 import bitronix.tm.resource.ehcache.EhCacheXAResourceProducer;
65
66 /**
67 * Aggregates multiple {@link UserDirectory} and integrates them with system
68 * roles.
69 */
70 class NodeUserAdmin extends AggregatingUserAdmin implements ManagedServiceFactory, KernelConstants {
71 private final static Log log = LogFactory.getLog(NodeUserAdmin.class);
72 private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
73
74 // OSGi
75 private Map<String, LdapName> pidToBaseDn = new HashMap<>();
76 private Map<String, ServiceRegistration<UserDirectory>> pidToServiceRegs = new HashMap<>();
77 private ServiceRegistration<UserAdmin> userAdminReg;
78
79 // JTA
80 private final ServiceTracker<TransactionManager, TransactionManager> tmTracker;
81 private final String cacheName = UserDirectory.class.getName();
82
83 // GSS API
84 private Path nodeKeyTab = KernelUtils.getOsgiInstancePath(KernelConstants.NODE_KEY_TAB_PATH);
85 private GSSCredential acceptorCredentials;
86
87 public NodeUserAdmin(String systemRolesBaseDn) {
88 super(systemRolesBaseDn);
89 tmTracker = new ServiceTracker<>(bc, TransactionManager.class, null);
90 tmTracker.open();
91 }
92
93 @Override
94 public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException {
95 String uri = (String) properties.get(UserAdminConf.uri.name());
96 URI u;
97 try {
98 if (uri == null) {
99 String baseDn = (String) properties.get(UserAdminConf.baseDn.name());
100 u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + baseDn + ".ldif");
101 } else
102 u = new URI(uri);
103 } catch (URISyntaxException e) {
104 throw new CmsException("Badly formatted URI " + uri, e);
105 }
106
107 // Create
108 AbstractUserDirectory userDirectory = u.getScheme().equals("ldap") ? new LdapUserAdmin(properties)
109 : new LdifUserAdmin(u, properties);
110 Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
111 addUserDirectory(userDirectory);
112
113 // OSGi
114 LdapName baseDn = userDirectory.getBaseDn();
115 Dictionary<String, Object> regProps = new Hashtable<>();
116 regProps.put(Constants.SERVICE_PID, pid);
117 if (isSystemRolesBaseDn(baseDn))
118 regProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
119 regProps.put(UserAdminConf.baseDn.name(), baseDn);
120 ServiceRegistration<UserDirectory> reg = bc.registerService(UserDirectory.class, userDirectory, regProps);
121 pidToBaseDn.put(pid, baseDn);
122 pidToServiceRegs.put(pid, reg);
123
124 if (log.isDebugEnabled())
125 log.debug("User directory " + userDirectory.getBaseDn() + " [" + u.getScheme() + "] enabled."
126 + (realm != null ? " " + realm + " realm." : ""));
127
128 if (!isSystemRolesBaseDn(baseDn)) {
129 if (userAdminReg != null)
130 userAdminReg.unregister();
131 // register self as main user admin
132 Dictionary<String, Object> userAdminregProps = currentState();
133 userAdminregProps.put(NodeConstants.CN, NodeConstants.DEFAULT);
134 userAdminregProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
135 userAdminReg = bc.registerService(UserAdmin.class, this, userAdminregProps);
136 }
137 }
138
139 @Override
140 public void deleted(String pid) {
141 assert pidToServiceRegs.get(pid) != null;
142 assert pidToBaseDn.get(pid) != null;
143 pidToServiceRegs.remove(pid).unregister();
144 LdapName baseDn = pidToBaseDn.remove(pid);
145 removeUserDirectory(baseDn);
146 }
147
148 @Override
149 public String getName() {
150 return "Node User Admin";
151 }
152
153 protected void postAdd(AbstractUserDirectory userDirectory) {
154 // JTA
155 TransactionManager tm = tmTracker.getService();
156 if (tm == null)
157 throw new CmsException("A JTA transaction manager must be available.");
158 userDirectory.setTransactionManager(tm);
159 if (tmTracker.getService() instanceof BitronixTransactionManager)
160 EhCacheXAResourceProducer.registerXAResource(cacheName, userDirectory.getXaResource());
161
162 Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
163 if (realm != null) {
164 if (Files.exists(nodeKeyTab)) {
165 String servicePrincipal = getKerberosServicePrincipal(realm.toString());
166 if (servicePrincipal != null) {
167 CallbackHandler callbackHandler = new CallbackHandler() {
168 @Override
169 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
170 for (Callback callback : callbacks)
171 if (callback instanceof NameCallback)
172 ((NameCallback) callback).setName(servicePrincipal);
173
174 }
175 };
176 try {
177 LoginContext nodeLc = new LoginContext(NodeConstants.LOGIN_CONTEXT_NODE, callbackHandler);
178 nodeLc.login();
179 acceptorCredentials = logInAsAcceptor(nodeLc.getSubject(), servicePrincipal);
180 } catch (LoginException e) {
181 throw new CmsException("Cannot log in kernel", e);
182 }
183 }
184 }
185
186 // Register client-side SPNEGO auth scheme
187 AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class);
188 HttpParams params = DefaultHttpParams.getDefaultParams();
189 ArrayList<String> schemes = new ArrayList<>();
190 schemes.add(SpnegoAuthScheme.NAME);// SPNEGO preferred
191 // schemes.add(AuthPolicy.BASIC);// incompatible with Basic
192 params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);
193 params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider());
194 params.setParameter(HttpMethodParams.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
195 // params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
196 }
197 }
198
199 protected void preDestroy(AbstractUserDirectory userDirectory) {
200 if (tmTracker.getService() instanceof BitronixTransactionManager)
201 EhCacheXAResourceProducer.unregisterXAResource(cacheName, userDirectory.getXaResource());
202
203 Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
204 if (realm != null) {
205 if (acceptorCredentials != null) {
206 try {
207 acceptorCredentials.dispose();
208 } catch (GSSException e) {
209 // silent
210 }
211 acceptorCredentials = null;
212 }
213 }
214 }
215
216 private String getKerberosServicePrincipal(String realm) {
217 String hostname;
218 try (DnsBrowser dnsBrowser = new DnsBrowser()) {
219 InetAddress localhost = InetAddress.getLocalHost();
220 hostname = localhost.getHostName();
221 String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
222 String ipfromDns = dnsBrowser.getRecord(hostname, localhost instanceof Inet6Address ? "AAAA" : "A");
223 boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
224 String kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
225 if (consistentIp && kerberosDomain != null && kerberosDomain.equals(realm) && Files.exists(nodeKeyTab)) {
226 return NodeHttp.DEFAULT_SERVICE + "/" + hostname + "@" + kerberosDomain;
227 } else
228 return null;
229 } catch (Exception e) {
230 log.warn("Exception when determining kerberos principal", e);
231 return null;
232 }
233 }
234
235 private GSSCredential logInAsAcceptor(Subject subject, String servicePrincipal) {
236 // GSS
237 Iterator<KerberosPrincipal> krb5It = subject.getPrincipals(KerberosPrincipal.class).iterator();
238 if (!krb5It.hasNext())
239 return null;
240 KerberosPrincipal krb5Principal = null;
241 while (krb5It.hasNext()) {
242 KerberosPrincipal principal = krb5It.next();
243 if (principal.getName().equals(servicePrincipal))
244 krb5Principal = principal;
245 }
246
247 if (krb5Principal == null)
248 return null;
249
250 GSSManager manager = GSSManager.getInstance();
251 try {
252 GSSName gssName = manager.createName(krb5Principal.getName(), null);
253 GSSCredential serverCredentials = Subject.doAs(subject, new PrivilegedExceptionAction<GSSCredential>() {
254
255 @Override
256 public GSSCredential run() throws GSSException {
257 return manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, KERBEROS_OID,
258 GSSCredential.ACCEPT_ONLY);
259
260 }
261
262 });
263 if (log.isDebugEnabled())
264 log.debug("GSS acceptor configured for " + krb5Principal);
265 return serverCredentials;
266 } catch (Exception gsse) {
267 throw new CmsException("Cannot create acceptor credentials for " + krb5Principal, gsse);
268 }
269 }
270
271 public GSSCredential getAcceptorCredentials() {
272 return acceptorCredentials;
273 }
274
275 public final static Oid KERBEROS_OID;
276 static {
277 try {
278 KERBEROS_OID = new Oid("1.3.6.1.5.5.2");
279 } catch (GSSException e) {
280 throw new IllegalStateException("Cannot create Kerberos OID", e);
281 }
282 }
283
284 }