]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java
e73d96f65ec7ab80c5abe7f14d13faa05c8028d5
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / runtime / CmsUserAdmin.java
1 package org.argeo.cms.internal.runtime;
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.net.URL;
9 import java.nio.file.Files;
10 import java.nio.file.Path;
11 import java.security.PrivilegedExceptionAction;
12 import java.util.ArrayList;
13 import java.util.Dictionary;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.Optional;
17 import java.util.Set;
18
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
28 import org.apache.commons.httpclient.auth.AuthPolicy;
29 import org.apache.commons.httpclient.auth.CredentialsProvider;
30 import org.apache.commons.httpclient.params.DefaultHttpParams;
31 import org.apache.commons.httpclient.params.HttpMethodParams;
32 import org.apache.commons.httpclient.params.HttpParams;
33 import org.argeo.api.cms.CmsAuth;
34 import org.argeo.api.cms.CmsConstants;
35 import org.argeo.api.cms.CmsLog;
36 import org.argeo.api.cms.CmsState;
37 import org.argeo.cms.internal.http.client.HttpCredentialProvider;
38 import org.argeo.cms.internal.http.client.SpnegoAuthScheme;
39 import org.argeo.osgi.useradmin.AggregatingUserAdmin;
40 import org.argeo.osgi.useradmin.DirectoryUserAdmin;
41 import org.argeo.osgi.useradmin.UserDirectory;
42 import org.argeo.util.directory.DirectoryConf;
43 import org.argeo.util.naming.dns.DnsBrowser;
44 import org.argeo.util.transaction.WorkControl;
45 import org.argeo.util.transaction.WorkTransaction;
46 import org.ietf.jgss.GSSCredential;
47 import org.ietf.jgss.GSSException;
48 import org.ietf.jgss.GSSManager;
49 import org.ietf.jgss.GSSName;
50 import org.ietf.jgss.Oid;
51 import org.osgi.service.useradmin.Authorization;
52 import org.osgi.service.useradmin.Group;
53 import org.osgi.service.useradmin.Role;
54
55 /**
56 * Aggregates multiple {@link UserDirectory} and integrates them with system
57 * roles.
58 */
59 public class CmsUserAdmin extends AggregatingUserAdmin {
60 private final static CmsLog log = CmsLog.getLog(CmsUserAdmin.class);
61
62 // GSS API
63 private Path nodeKeyTab = KernelUtils.getOsgiInstancePath(KernelConstants.NODE_KEY_TAB_PATH);
64 private GSSCredential acceptorCredentials;
65
66 private boolean singleUser = false;
67
68 private WorkControl transactionManager;
69 private WorkTransaction userTransaction;
70
71 private CmsState cmsState;
72
73 public CmsUserAdmin() {
74 super(CmsConstants.ROLES_BASEDN, CmsConstants.TOKENS_BASEDN);
75 }
76
77 public void start() {
78 super.start();
79 List<Dictionary<String, Object>> configs = InitUtils.getUserDirectoryConfigs();
80 for (Dictionary<String, Object> config : configs) {
81 UserDirectory userDirectory = enableUserDirectory(config);
82 if (userDirectory.getRealm().isPresent())
83 loadIpaJaasConfiguration();
84 }
85 log.debug(() -> "CMS user admin available");
86 }
87
88 public void stop() {
89 // for (UserDirectory userDirectory : getUserDirectories()) {
90 // removeUserDirectory(userDirectory);
91 // }
92 super.stop();
93 }
94
95 public UserDirectory enableUserDirectory(Dictionary<String, ?> properties) {
96 String uri = (String) properties.get(DirectoryConf.uri.name());
97 Object realm = properties.get(DirectoryConf.realm.name());
98 URI u;
99 try {
100 if (uri == null) {
101 String baseDn = (String) properties.get(DirectoryConf.baseDn.name());
102 u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + baseDn + ".ldif");
103 } else if (realm != null) {
104 u = null;
105 } else {
106 u = new URI(uri);
107 }
108 } catch (URISyntaxException e) {
109 throw new IllegalArgumentException("Badly formatted URI " + uri, e);
110 }
111
112 // Create
113 UserDirectory userDirectory = new DirectoryUserAdmin(u, properties);
114 // if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme())
115 // || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
116 // userDirectory = new LdapUserAdmin(properties);
117 // } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
118 // userDirectory = new LdifUserAdmin(u, properties);
119 // } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
120 // userDirectory = new OsUserDirectory(u, properties);
121 // singleUser = true;
122 // } else {
123 // throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
124 // }
125 String basePath = userDirectory.getContext();
126
127 addUserDirectory(userDirectory);
128 if (isSystemRolesBaseDn(basePath)) {
129 addStandardSystemRoles();
130 }
131 if (log.isDebugEnabled()) {
132 log.debug("User directory " + userDirectory.getContext() + (u != null ? " [" + u.getScheme() + "]" : "")
133 + " enabled." + (realm != null ? " " + realm + " realm." : ""));
134 }
135 return userDirectory;
136 }
137
138 protected void addStandardSystemRoles() {
139 // we assume UserTransaction is already available (TODO make it more robust)
140 try {
141 userTransaction.begin();
142 Role adminRole = getRole(CmsConstants.ROLE_ADMIN);
143 if (adminRole == null) {
144 adminRole = createRole(CmsConstants.ROLE_ADMIN, Role.GROUP);
145 }
146 if (getRole(CmsConstants.ROLE_USER_ADMIN) == null) {
147 Group userAdminRole = (Group) createRole(CmsConstants.ROLE_USER_ADMIN, Role.GROUP);
148 userAdminRole.addMember(adminRole);
149 }
150 userTransaction.commit();
151 } catch (Exception e) {
152 try {
153 userTransaction.rollback();
154 } catch (Exception e1) {
155 // silent
156 }
157 throw new IllegalStateException("Cannot add standard system roles", e);
158 }
159 }
160
161 @Override
162 protected void addAbstractSystemRoles(Authorization rawAuthorization, Set<String> sysRoles) {
163 if (rawAuthorization.getName() == null) {
164 sysRoles.add(CmsConstants.ROLE_ANONYMOUS);
165 } else {
166 sysRoles.add(CmsConstants.ROLE_USER);
167 }
168 }
169
170 @Override
171 protected void postAdd(UserDirectory userDirectory) {
172 userDirectory.setTransactionControl(transactionManager);
173
174 Optional<String> realm = userDirectory.getRealm();
175 if (realm.isPresent()) {
176 if (Files.exists(nodeKeyTab)) {
177 String servicePrincipal = getKerberosServicePrincipal(realm.get());
178 if (servicePrincipal != null) {
179 CallbackHandler callbackHandler = new CallbackHandler() {
180 @Override
181 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
182 for (Callback callback : callbacks)
183 if (callback instanceof NameCallback)
184 ((NameCallback) callback).setName(servicePrincipal);
185
186 }
187 };
188 try {
189 LoginContext nodeLc = new LoginContext(CmsAuth.LOGIN_CONTEXT_NODE, callbackHandler);
190 nodeLc.login();
191 acceptorCredentials = logInAsAcceptor(nodeLc.getSubject(), servicePrincipal);
192 } catch (LoginException e) {
193 throw new IllegalStateException("Cannot log in kernel", e);
194 }
195 }
196 }
197
198 // Register client-side SPNEGO auth scheme
199 AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class);
200 HttpParams params = DefaultHttpParams.getDefaultParams();
201 ArrayList<String> schemes = new ArrayList<>();
202 schemes.add(SpnegoAuthScheme.NAME);// SPNEGO preferred
203 // schemes.add(AuthPolicy.BASIC);// incompatible with Basic
204 params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);
205 params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider());
206 params.setParameter(HttpMethodParams.COOKIE_POLICY, KernelConstants.COOKIE_POLICY_BROWSER_COMPATIBILITY);
207 // params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
208 }
209 }
210
211 @Override
212 protected void preDestroy(UserDirectory userDirectory) {
213 Optional<String> realm = userDirectory.getRealm();
214 if (realm.isPresent()) {
215 if (acceptorCredentials != null) {
216 try {
217 acceptorCredentials.dispose();
218 } catch (GSSException e) {
219 // silent
220 }
221 acceptorCredentials = null;
222 }
223 }
224 }
225
226 private void loadIpaJaasConfiguration() {
227 if (System.getProperty(KernelConstants.JAAS_CONFIG_PROP) == null) {
228 String jaasConfig = KernelConstants.JAAS_CONFIG_IPA;
229 URL url = getClass().getClassLoader().getResource(jaasConfig);
230 KernelUtils.setJaasConfiguration(url);
231 log.debug("Set IPA JAAS configuration.");
232 }
233 }
234
235 private String getKerberosServicePrincipal(String realm) {
236 String hostname;
237 try (DnsBrowser dnsBrowser = new DnsBrowser()) {
238 InetAddress localhost = InetAddress.getLocalHost();
239 hostname = localhost.getHostName();
240 String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
241 String ipfromDns = dnsBrowser.getRecord(hostname, localhost instanceof Inet6Address ? "AAAA" : "A");
242 boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
243 String kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
244 if (consistentIp && kerberosDomain != null && kerberosDomain.equals(realm) && Files.exists(nodeKeyTab)) {
245 return KernelConstants.DEFAULT_KERBEROS_SERVICE + "/" + hostname + "@" + kerberosDomain;
246 } else
247 return null;
248 } catch (Exception e) {
249 log.warn("Exception when determining kerberos principal", e);
250 return null;
251 }
252 }
253
254 private GSSCredential logInAsAcceptor(Subject subject, String servicePrincipal) {
255 // not static because class is not supported by Android
256 final Oid KERBEROS_OID;
257 try {
258 KERBEROS_OID = new Oid("1.3.6.1.5.5.2");
259 } catch (GSSException e) {
260 throw new IllegalStateException("Cannot create Kerberos OID", e);
261 }
262 // GSS
263 Iterator<KerberosPrincipal> krb5It = subject.getPrincipals(KerberosPrincipal.class).iterator();
264 if (!krb5It.hasNext())
265 return null;
266 KerberosPrincipal krb5Principal = null;
267 while (krb5It.hasNext()) {
268 KerberosPrincipal principal = krb5It.next();
269 if (principal.getName().equals(servicePrincipal))
270 krb5Principal = principal;
271 }
272
273 if (krb5Principal == null)
274 return null;
275
276 GSSManager manager = GSSManager.getInstance();
277 try {
278 GSSName gssName = manager.createName(krb5Principal.getName(), null);
279 GSSCredential serverCredentials = Subject.doAs(subject, new PrivilegedExceptionAction<GSSCredential>() {
280
281 @Override
282 public GSSCredential run() throws GSSException {
283 return manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, KERBEROS_OID,
284 GSSCredential.ACCEPT_ONLY);
285
286 }
287
288 });
289 if (log.isDebugEnabled())
290 log.debug("GSS acceptor configured for " + krb5Principal);
291 return serverCredentials;
292 } catch (Exception gsse) {
293 throw new IllegalStateException("Cannot create acceptor credentials for " + krb5Principal, gsse);
294 }
295 }
296
297 public GSSCredential getAcceptorCredentials() {
298 return acceptorCredentials;
299 }
300
301 public boolean hasAcceptorCredentials() {
302 return acceptorCredentials != null;
303 }
304
305 public boolean isSingleUser() {
306 return singleUser;
307 }
308
309 public void setTransactionManager(WorkControl transactionManager) {
310 this.transactionManager = transactionManager;
311 }
312
313 public void setUserTransaction(WorkTransaction userTransaction) {
314 this.userTransaction = userTransaction;
315 }
316
317 public void setCmsState(CmsState cmsState) {
318 this.cmsState = cmsState;
319 }
320
321 }