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