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