1 package org
.argeo
.cms
.internal
.runtime
;
4 import java
.io
.IOException
;
5 import java
.net
.InetAddress
;
7 import java
.net
.URISyntaxException
;
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
;
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
;
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
.apache
.commons
.io
.FileUtils
;
34 import org
.argeo
.api
.cms
.CmsAuth
;
35 import org
.argeo
.api
.cms
.CmsConstants
;
36 import org
.argeo
.api
.cms
.CmsLog
;
37 import org
.argeo
.api
.cms
.CmsState
;
38 import org
.argeo
.cms
.CmsDeployProperty
;
39 import org
.argeo
.cms
.internal
.http
.client
.HttpCredentialProvider
;
40 import org
.argeo
.cms
.internal
.http
.client
.SpnegoAuthScheme
;
41 import org
.argeo
.osgi
.useradmin
.AggregatingUserAdmin
;
42 import org
.argeo
.osgi
.useradmin
.DirectoryUserAdmin
;
43 import org
.argeo
.osgi
.useradmin
.UserDirectory
;
44 import org
.argeo
.util
.directory
.DirectoryConf
;
45 import org
.argeo
.util
.naming
.dns
.DnsBrowser
;
46 import org
.argeo
.util
.transaction
.WorkControl
;
47 import org
.argeo
.util
.transaction
.WorkTransaction
;
48 import org
.ietf
.jgss
.GSSCredential
;
49 import org
.ietf
.jgss
.GSSException
;
50 import org
.ietf
.jgss
.GSSManager
;
51 import org
.ietf
.jgss
.GSSName
;
52 import org
.ietf
.jgss
.Oid
;
53 import org
.osgi
.service
.useradmin
.Authorization
;
54 import org
.osgi
.service
.useradmin
.Group
;
55 import org
.osgi
.service
.useradmin
.Role
;
58 * Aggregates multiple {@link UserDirectory} and integrates them with system
61 public class CmsUserAdmin
extends AggregatingUserAdmin
{
62 private final static CmsLog log
= CmsLog
.getLog(CmsUserAdmin
.class);
65 private Path nodeKeyTab
= KernelUtils
.getOsgiInstancePath(KernelConstants
.NODE_KEY_TAB_PATH
);
66 private GSSCredential acceptorCredentials
;
68 private boolean singleUser
= false;
70 private WorkControl transactionManager
;
71 private WorkTransaction userTransaction
;
73 private CmsState cmsState
;
75 public CmsUserAdmin() {
76 super(CmsConstants
.ROLES_BASEDN
, CmsConstants
.TOKENS_BASEDN
);
81 List
<Dictionary
<String
, Object
>> configs
= getUserDirectoryConfigs();
82 for (Dictionary
<String
, Object
> config
: configs
) {
83 enableUserDirectory(config
);
84 // if (userDirectory.getRealm().isPresent())
85 // loadIpaJaasConfiguration();
87 log
.debug(() -> "CMS user admin available");
91 // for (UserDirectory userDirectory : getUserDirectories()) {
92 // removeUserDirectory(userDirectory);
97 protected List
<Dictionary
<String
, Object
>> getUserDirectoryConfigs() {
98 List
<Dictionary
<String
, Object
>> res
= new ArrayList
<>();
99 File nodeBaseDir
= cmsState
.getDataPath(KernelConstants
.DIR_NODE
).toFile();
100 List
<String
> uris
= new ArrayList
<>();
103 String nodeRolesUri
= null;// getFrameworkProp(CmsConstants.ROLES_URI);
104 String baseNodeRoleDn
= CmsConstants
.ROLES_BASEDN
;
105 if (nodeRolesUri
== null) {
106 nodeRolesUri
= baseNodeRoleDn
+ ".ldif";
107 File nodeRolesFile
= new File(nodeBaseDir
, nodeRolesUri
);
108 if (!nodeRolesFile
.exists())
110 FileUtils
.copyInputStreamToFile(CmsUserAdmin
.class.getResourceAsStream(baseNodeRoleDn
+ ".ldif"),
112 } catch (IOException e
) {
113 throw new RuntimeException("Cannot copy demo resource", e
);
115 // nodeRolesUri = nodeRolesFile.toURI().toString();
117 uris
.add(nodeRolesUri
);
120 String nodeTokensUri
= null;// getFrameworkProp(CmsConstants.TOKENS_URI);
121 String baseNodeTokensDn
= CmsConstants
.TOKENS_BASEDN
;
122 if (nodeTokensUri
== null) {
123 nodeTokensUri
= baseNodeTokensDn
+ ".ldif";
124 File nodeTokensFile
= new File(nodeBaseDir
, nodeTokensUri
);
125 if (!nodeTokensFile
.exists())
127 FileUtils
.copyInputStreamToFile(CmsUserAdmin
.class.getResourceAsStream(baseNodeTokensDn
+ ".ldif"),
129 } catch (IOException e
) {
130 throw new RuntimeException("Cannot copy demo resource", e
);
132 // nodeRolesUri = nodeRolesFile.toURI().toString();
134 uris
.add(nodeTokensUri
);
137 // String userAdminUris = getFrameworkProp(CmsConstants.USERADMIN_URIS);
138 List
<String
> userAdminUris
= CmsStateImpl
.getDeployProperties(cmsState
, CmsDeployProperty
.DIRECTORY
);// getFrameworkProp(CmsConstants.USERADMIN_URIS);
139 for (String userAdminUri
: userAdminUris
) {
140 if (userAdminUri
== null)
142 // if (!userAdminUri.trim().equals(""))
143 uris
.add(userAdminUri
);
146 if (uris
.size() == 0) {
147 // TODO put this somewhere else
148 String demoBaseDn
= "dc=example,dc=com";
149 String userAdminUri
= demoBaseDn
+ ".ldif";
150 File businessRolesFile
= new File(nodeBaseDir
, userAdminUri
);
151 File systemRolesFile
= new File(nodeBaseDir
, "ou=roles,ou=node.ldif");
152 if (!businessRolesFile
.exists())
154 FileUtils
.copyInputStreamToFile(CmsUserAdmin
.class.getResourceAsStream(demoBaseDn
+ ".ldif"),
156 if (!systemRolesFile
.exists())
157 FileUtils
.copyInputStreamToFile(
158 CmsUserAdmin
.class.getResourceAsStream("example-ou=roles,ou=node.ldif"),
160 } catch (IOException e
) {
161 throw new RuntimeException("Cannot copy demo resources", e
);
163 // userAdminUris = businessRolesFile.toURI().toString();
164 log
.warn("## DEV Using dummy base DN " + demoBaseDn
);
165 // TODO downgrade security level
169 for (String uri
: uris
) {
173 if (u
.getPath() == null)
174 throw new IllegalArgumentException(
175 "URI " + uri
+ " must have a path in order to determine base DN");
176 if (u
.getScheme() == null) {
177 if (uri
.startsWith("/") || uri
.startsWith("./") || uri
.startsWith("../"))
178 u
= new File(uri
).getCanonicalFile().toURI();
179 else if (!uri
.contains("/")) {
180 // u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + uri);
183 throw new IllegalArgumentException("Cannot interpret " + uri
+ " as an uri");
184 } else if (u
.getScheme().equals(DirectoryConf
.SCHEME_FILE
)) {
185 u
= new File(u
).getCanonicalFile().toURI();
187 } catch (Exception e
) {
188 throw new RuntimeException("Cannot interpret " + uri
+ " as an uri", e
);
190 Dictionary
<String
, Object
> properties
= DirectoryConf
.uriAsProperties(u
.toString());
197 public UserDirectory
enableUserDirectory(Dictionary
<String
, ?
> properties
) {
198 String uri
= (String
) properties
.get(DirectoryConf
.uri
.name());
199 Object realm
= properties
.get(DirectoryConf
.realm
.name());
203 String baseDn
= (String
) properties
.get(DirectoryConf
.baseDn
.name());
204 u
= KernelUtils
.getOsgiInstanceUri(KernelConstants
.DIR_NODE
+ '/' + baseDn
+ ".ldif");
205 } else if (realm
!= null) {
210 } catch (URISyntaxException e
) {
211 throw new IllegalArgumentException("Badly formatted URI " + uri
, e
);
215 UserDirectory userDirectory
= new DirectoryUserAdmin(u
, properties
);
216 // if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme())
217 // || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
218 // userDirectory = new LdapUserAdmin(properties);
219 // } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
220 // userDirectory = new LdifUserAdmin(u, properties);
221 // } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
222 // userDirectory = new OsUserDirectory(u, properties);
223 // singleUser = true;
225 // throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
227 String basePath
= userDirectory
.getContext();
229 addUserDirectory(userDirectory
);
230 if (isSystemRolesBaseDn(basePath
)) {
231 addStandardSystemRoles();
233 if (log
.isDebugEnabled()) {
234 log
.debug("User directory " + userDirectory
.getContext() + (u
!= null ?
" [" + u
.getScheme() + "]" : "")
235 + " enabled." + (realm
!= null ?
" " + realm
+ " realm." : ""));
237 return userDirectory
;
240 protected void addStandardSystemRoles() {
241 // we assume UserTransaction is already available (TODO make it more robust)
243 userTransaction
.begin();
244 Role adminRole
= getRole(CmsConstants
.ROLE_ADMIN
);
245 if (adminRole
== null) {
246 adminRole
= createRole(CmsConstants
.ROLE_ADMIN
, Role
.GROUP
);
248 if (getRole(CmsConstants
.ROLE_USER_ADMIN
) == null) {
249 Group userAdminRole
= (Group
) createRole(CmsConstants
.ROLE_USER_ADMIN
, Role
.GROUP
);
250 userAdminRole
.addMember(adminRole
);
252 userTransaction
.commit();
253 } catch (Exception e
) {
255 userTransaction
.rollback();
256 } catch (Exception e1
) {
259 throw new IllegalStateException("Cannot add standard system roles", e
);
264 protected void addAbstractSystemRoles(Authorization rawAuthorization
, Set
<String
> sysRoles
) {
265 if (rawAuthorization
.getName() == null) {
266 sysRoles
.add(CmsConstants
.ROLE_ANONYMOUS
);
268 sysRoles
.add(CmsConstants
.ROLE_USER
);
273 protected void postAdd(UserDirectory userDirectory
) {
274 userDirectory
.setTransactionControl(transactionManager
);
276 Optional
<String
> realm
= userDirectory
.getRealm();
277 if (realm
.isPresent()) {
278 loadIpaJaasConfiguration();
279 if (Files
.exists(nodeKeyTab
)) {
280 String servicePrincipal
= getKerberosServicePrincipal(realm
.get());
281 if (servicePrincipal
!= null) {
282 CallbackHandler callbackHandler
= new CallbackHandler() {
284 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
285 for (Callback callback
: callbacks
)
286 if (callback
instanceof NameCallback
)
287 ((NameCallback
) callback
).setName(servicePrincipal
);
292 LoginContext nodeLc
= CmsAuth
.NODE
.newLoginContext(callbackHandler
);
294 acceptorCredentials
= logInAsAcceptor(nodeLc
.getSubject(), servicePrincipal
);
295 } catch (LoginException e
) {
296 throw new IllegalStateException("Cannot log in kernel", e
);
301 // Register client-side SPNEGO auth scheme
302 AuthPolicy
.registerAuthScheme(SpnegoAuthScheme
.NAME
, SpnegoAuthScheme
.class);
303 HttpParams params
= DefaultHttpParams
.getDefaultParams();
304 ArrayList
<String
> schemes
= new ArrayList
<>();
305 schemes
.add(SpnegoAuthScheme
.NAME
);// SPNEGO preferred
306 // schemes.add(AuthPolicy.BASIC);// incompatible with Basic
307 params
.setParameter(AuthPolicy
.AUTH_SCHEME_PRIORITY
, schemes
);
308 params
.setParameter(CredentialsProvider
.PROVIDER
, new HttpCredentialProvider());
309 params
.setParameter(HttpMethodParams
.COOKIE_POLICY
, KernelConstants
.COOKIE_POLICY_BROWSER_COMPATIBILITY
);
310 // params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
315 protected void preDestroy(UserDirectory userDirectory
) {
316 Optional
<String
> realm
= userDirectory
.getRealm();
317 if (realm
.isPresent()) {
318 if (acceptorCredentials
!= null) {
320 acceptorCredentials
.dispose();
321 } catch (GSSException e
) {
324 acceptorCredentials
= null;
329 private void loadIpaJaasConfiguration() {
330 if (CmsStateImpl
.getDeployProperty(cmsState
, CmsDeployProperty
.JAVA_LOGIN_CONFIG
) == null) {
331 String jaasConfig
= KernelConstants
.JAAS_CONFIG_IPA
;
332 URL url
= getClass().getClassLoader().getResource(jaasConfig
);
333 KernelUtils
.setJaasConfiguration(url
);
334 log
.debug("Set IPA JAAS configuration.");
338 protected String
getKerberosServicePrincipal(String realm
) {
339 if (!Files
.exists(nodeKeyTab
))
341 List
<String
> dns
= CmsStateImpl
.getDeployProperties(cmsState
, CmsDeployProperty
.DNS
);
342 String hostname
= CmsStateImpl
.getDeployProperty(cmsState
, CmsDeployProperty
.HOST
);
343 try (DnsBrowser dnsBrowser
= new DnsBrowser(dns
)) {
344 hostname
= hostname
!= null ? hostname
: InetAddress
.getLocalHost().getHostName();
345 String dnsZone
= hostname
.substring(hostname
.indexOf('.') + 1);
346 String ipv4fromDns
= dnsBrowser
.getRecord(hostname
, "A");
347 String ipv6fromDns
= dnsBrowser
.getRecord(hostname
, "AAAA");
348 if (ipv4fromDns
== null && ipv6fromDns
== null)
349 throw new IllegalStateException("hostname " + hostname
+ " is not registered in DNS");
350 // boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
351 String kerberosDomain
= dnsBrowser
.getRecord("_kerberos." + dnsZone
, "TXT");
352 if (kerberosDomain
!= null && kerberosDomain
.equals(realm
)) {
353 return KernelConstants
.DEFAULT_KERBEROS_SERVICE
+ "/" + hostname
+ "@" + kerberosDomain
;
356 } catch (Exception e
) {
357 log
.warn("Exception when determining kerberos principal", e
);
362 private GSSCredential
logInAsAcceptor(Subject subject
, String servicePrincipal
) {
363 // not static because class is not supported by Android
364 final Oid KERBEROS_OID
;
366 KERBEROS_OID
= new Oid("1.3.6.1.5.5.2");
367 } catch (GSSException e
) {
368 throw new IllegalStateException("Cannot create Kerberos OID", e
);
371 Iterator
<KerberosPrincipal
> krb5It
= subject
.getPrincipals(KerberosPrincipal
.class).iterator();
372 if (!krb5It
.hasNext())
374 KerberosPrincipal krb5Principal
= null;
375 while (krb5It
.hasNext()) {
376 KerberosPrincipal principal
= krb5It
.next();
377 if (principal
.getName().equals(servicePrincipal
))
378 krb5Principal
= principal
;
381 if (krb5Principal
== null)
384 GSSManager manager
= GSSManager
.getInstance();
386 GSSName gssName
= manager
.createName(krb5Principal
.getName(), null);
387 GSSCredential serverCredentials
= Subject
.doAs(subject
, new PrivilegedExceptionAction
<GSSCredential
>() {
390 public GSSCredential
run() throws GSSException
{
391 return manager
.createCredential(gssName
, GSSCredential
.INDEFINITE_LIFETIME
, KERBEROS_OID
,
392 GSSCredential
.ACCEPT_ONLY
);
397 if (log
.isDebugEnabled())
398 log
.debug("GSS acceptor configured for " + krb5Principal
);
399 return serverCredentials
;
400 } catch (Exception gsse
) {
401 throw new IllegalStateException("Cannot create acceptor credentials for " + krb5Principal
, gsse
);
405 public GSSCredential
getAcceptorCredentials() {
406 return acceptorCredentials
;
409 public boolean hasAcceptorCredentials() {
410 return acceptorCredentials
!= null;
413 public boolean isSingleUser() {
417 public void setTransactionManager(WorkControl transactionManager
) {
418 this.transactionManager
= transactionManager
;
421 public void setUserTransaction(WorkTransaction userTransaction
) {
422 this.userTransaction
= userTransaction
;
425 public void setCmsState(CmsState cmsState
) {
426 this.cmsState
= cmsState
;