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