1 package org
.argeo
.cms
.internal
.runtime
;
3 import java
.io
.IOException
;
4 import java
.net
.InetAddress
;
6 import java
.net
.URISyntaxException
;
8 import java
.nio
.file
.Files
;
9 import java
.nio
.file
.Path
;
10 import java
.nio
.file
.Paths
;
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
.argeo
.api
.cms
.CmsAuth
;
29 import org
.argeo
.api
.cms
.CmsConstants
;
30 import org
.argeo
.api
.cms
.CmsLog
;
31 import org
.argeo
.api
.cms
.CmsState
;
32 import org
.argeo
.api
.cms
.directory
.UserDirectory
;
33 import org
.argeo
.api
.cms
.transaction
.WorkControl
;
34 import org
.argeo
.api
.cms
.transaction
.WorkTransaction
;
35 import org
.argeo
.cms
.CmsDeployProperty
;
36 import org
.argeo
.cms
.dns
.DnsBrowser
;
37 import org
.argeo
.cms
.osgi
.useradmin
.AggregatingUserAdmin
;
38 import org
.argeo
.cms
.osgi
.useradmin
.DirectoryUserAdmin
;
39 import org
.argeo
.cms
.runtime
.DirectoryConf
;
40 import org
.ietf
.jgss
.GSSCredential
;
41 import org
.ietf
.jgss
.GSSException
;
42 import org
.ietf
.jgss
.GSSManager
;
43 import org
.ietf
.jgss
.GSSName
;
44 import org
.ietf
.jgss
.Oid
;
45 import org
.osgi
.service
.useradmin
.Authorization
;
46 import org
.osgi
.service
.useradmin
.Group
;
47 import org
.osgi
.service
.useradmin
.Role
;
50 * Aggregates multiple {@link UserDirectory} and integrates them with system
53 public class CmsUserAdmin
extends AggregatingUserAdmin
{
54 private final static CmsLog log
= CmsLog
.getLog(CmsUserAdmin
.class);
57 private Path nodeKeyTab
= KernelUtils
.getOsgiInstancePath(KernelConstants
.NODE_KEY_TAB_PATH
);
58 private GSSCredential acceptorCredentials
;
60 private boolean singleUser
= false;
62 private WorkControl transactionManager
;
63 private WorkTransaction userTransaction
;
65 private CmsState cmsState
;
67 public CmsUserAdmin() {
68 super(CmsConstants
.SYSTEM_ROLES_BASEDN
, CmsConstants
.TOKENS_BASEDN
);
73 List
<Dictionary
<String
, Object
>> configs
= getUserDirectoryConfigs();
74 for (Dictionary
<String
, Object
> config
: configs
) {
75 enableUserDirectory(config
);
76 // if (userDirectory.getRealm().isPresent())
77 // loadIpaJaasConfiguration();
79 log
.debug(() -> "CMS user admin available");
83 // for (UserDirectory userDirectory : getUserDirectories()) {
84 // removeUserDirectory(userDirectory);
89 protected List
<Dictionary
<String
, Object
>> getUserDirectoryConfigs() {
90 List
<Dictionary
<String
, Object
>> res
= new ArrayList
<>();
91 Path nodeBase
= cmsState
.getDataPath(KernelConstants
.DIR_PRIVATE
);
92 List
<String
> uris
= new ArrayList
<>();
95 String nodeRolesUri
= null;// getFrameworkProp(CmsConstants.ROLES_URI);
96 String baseNodeRoleDn
= CmsConstants
.SYSTEM_ROLES_BASEDN
;
97 if (nodeRolesUri
== null && nodeBase
!= null) {
98 nodeRolesUri
= baseNodeRoleDn
+ ".ldif";
99 Path nodeRolesFile
= nodeBase
.resolve(nodeRolesUri
);
100 if (!Files
.exists(nodeRolesFile
))
102 Files
.copy(CmsUserAdmin
.class.getResourceAsStream(baseNodeRoleDn
+ ".ldif"), nodeRolesFile
);
103 } catch (IOException e
) {
104 throw new RuntimeException("Cannot copy demo resource", e
);
106 // nodeRolesUri = nodeRolesFile.toURI().toString();
108 if (nodeRolesUri
!= null)
109 uris
.add(nodeRolesUri
);
112 String nodeTokensUri
= null;// getFrameworkProp(CmsConstants.TOKENS_URI);
113 String baseNodeTokensDn
= CmsConstants
.TOKENS_BASEDN
;
114 if (nodeTokensUri
== null && nodeBase
!= null) {
115 nodeTokensUri
= baseNodeTokensDn
+ ".ldif";
116 Path nodeTokensFile
= nodeBase
.resolve(nodeTokensUri
);
117 if (!Files
.exists(nodeTokensFile
))
119 Files
.copy(CmsUserAdmin
.class.getResourceAsStream(baseNodeTokensDn
+ ".ldif"), nodeTokensFile
);
120 } catch (IOException e
) {
121 throw new RuntimeException("Cannot copy demo resource", e
);
123 // nodeRolesUri = nodeRolesFile.toURI().toString();
125 if (nodeTokensUri
!= null)
126 uris
.add(nodeTokensUri
);
129 // String userAdminUris = getFrameworkProp(CmsConstants.USERADMIN_URIS);
130 List
<String
> userAdminUris
= CmsStateImpl
.getDeployProperties(cmsState
, CmsDeployProperty
.DIRECTORY
);// getFrameworkProp(CmsConstants.USERADMIN_URIS);
131 for (String userAdminUri
: userAdminUris
) {
132 if (userAdminUri
== null)
134 // if (!userAdminUri.trim().equals(""))
135 uris
.add(userAdminUri
);
138 if (uris
.size() == 0 && nodeBase
!= null) {
139 // TODO put this somewhere else
140 String demoBaseDn
= "dc=example,dc=com";
141 String userAdminUri
= demoBaseDn
+ ".ldif";
142 Path businessRolesFile
= nodeBase
.resolve(userAdminUri
);
143 Path systemRolesFile
= nodeBase
.resolve("ou=roles,ou=node.ldif");
144 if (!Files
.exists(businessRolesFile
))
146 Files
.copy(CmsUserAdmin
.class.getResourceAsStream(demoBaseDn
+ ".ldif"), businessRolesFile
);
147 if (!Files
.exists(systemRolesFile
))
148 Files
.copy(CmsUserAdmin
.class.getResourceAsStream("example-ou=roles,ou=node.ldif"),
150 } catch (IOException e
) {
151 throw new RuntimeException("Cannot copy demo resources", e
);
153 // userAdminUris = businessRolesFile.toURI().toString();
154 log
.warn("## DEV Using dummy base DN " + demoBaseDn
);
155 // TODO downgrade security level
159 for (String uri
: uris
) {
163 if (u
.getPath() == null)
164 throw new IllegalArgumentException(
165 "URI " + uri
+ " must have a path in order to determine base DN");
166 if (u
.getScheme() == null) {
167 if (uri
.startsWith("/") || uri
.startsWith("./") || uri
.startsWith("../"))
168 u
= Paths
.get(uri
).toRealPath().toUri();
169 else if (!uri
.contains("/")) {
170 // u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + uri);
173 throw new IllegalArgumentException("Cannot interpret " + uri
+ " as an uri");
174 } else if (u
.getScheme().equals(DirectoryConf
.SCHEME_FILE
)) {
175 u
= Paths
.get(u
).toRealPath().toUri();
177 } catch (Exception e
) {
178 throw new RuntimeException("Cannot interpret " + uri
+ " as an uri", e
);
182 Dictionary
<String
, Object
> properties
= DirectoryConf
.uriAsProperties(u
.toString());
184 } catch (Exception e
) {
185 log
.error("Cannot load user directory " + u
, e
);
192 public UserDirectory
enableUserDirectory(Dictionary
<String
, ?
> properties
) {
193 String uri
= (String
) properties
.get(DirectoryConf
.uri
.name());
194 Object realm
= properties
.get(DirectoryConf
.realm
.name());
198 String baseDn
= (String
) properties
.get(DirectoryConf
.baseDn
.name());
199 u
= KernelUtils
.getOsgiInstanceUri(KernelConstants
.DIR_PRIVATE
+ '/' + baseDn
+ ".ldif");
200 } else if (realm
!= null) {
205 } catch (URISyntaxException e
) {
206 throw new IllegalArgumentException("Badly formatted URI " + uri
, e
);
210 UserDirectory userDirectory
= new DirectoryUserAdmin(u
, properties
);
211 // if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme())
212 // || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
213 // userDirectory = new LdapUserAdmin(properties);
214 // } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
215 // userDirectory = new LdifUserAdmin(u, properties);
216 // } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
217 // userDirectory = new OsUserDirectory(u, properties);
218 // singleUser = true;
220 // throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
222 String basePath
= userDirectory
.getBase();
224 addUserDirectory(userDirectory
);
225 if (isSystemRolesBaseDn(basePath
)) {
226 addStandardSystemRoles();
228 if (log
.isDebugEnabled()) {
229 log
.debug("User directory " + userDirectory
.getBase() + (u
!= null ?
" [" + u
.getScheme() + "]" : "")
230 + " enabled." + (realm
!= null ?
" " + realm
+ " realm." : ""));
232 return userDirectory
;
235 protected void addStandardSystemRoles() {
236 // we assume UserTransaction is already available (TODO make it more robust)
238 userTransaction
.begin();
239 Role adminRole
= getRole(CmsConstants
.ROLE_ADMIN
);
240 if (adminRole
== null) {
241 adminRole
= createRole(CmsConstants
.ROLE_ADMIN
, Role
.GROUP
);
243 if (getRole(CmsConstants
.ROLE_USER_ADMIN
) == null) {
244 Group userAdminRole
= (Group
) createRole(CmsConstants
.ROLE_USER_ADMIN
, Role
.GROUP
);
245 userAdminRole
.addMember(adminRole
);
247 userTransaction
.commit();
248 } catch (Exception e
) {
250 userTransaction
.rollback();
251 } catch (Exception e1
) {
254 throw new IllegalStateException("Cannot add standard system roles", e
);
259 protected void addAbstractSystemRoles(Authorization rawAuthorization
, Set
<String
> sysRoles
) {
260 if (rawAuthorization
.getName() == null) {
261 sysRoles
.add(CmsConstants
.ROLE_ANONYMOUS
);
263 sysRoles
.add(CmsConstants
.ROLE_USER
);
268 protected void postAdd(UserDirectory userDirectory
) {
269 userDirectory
.setTransactionControl(transactionManager
);
271 Optional
<String
> realm
= userDirectory
.getRealm();
272 if (realm
.isPresent()) {
273 loadIpaJaasConfiguration();
274 if (Files
.exists(nodeKeyTab
)) {
275 String servicePrincipal
= getKerberosServicePrincipal(realm
.get());
276 if (servicePrincipal
!= null) {
277 CallbackHandler callbackHandler
= new CallbackHandler() {
279 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
280 for (Callback callback
: callbacks
)
281 if (callback
instanceof NameCallback
)
282 ((NameCallback
) callback
).setName(servicePrincipal
);
287 LoginContext nodeLc
= CmsAuth
.NODE
.newLoginContext(callbackHandler
);
289 acceptorCredentials
= logInAsAcceptor(nodeLc
.getSubject(), servicePrincipal
);
290 } catch (LoginException e
) {
291 throw new IllegalStateException("Cannot log in kernel", e
);
300 protected void preDestroy(UserDirectory userDirectory
) {
301 Optional
<String
> realm
= userDirectory
.getRealm();
302 if (realm
.isPresent()) {
303 if (acceptorCredentials
!= null) {
305 acceptorCredentials
.dispose();
306 } catch (GSSException e
) {
309 acceptorCredentials
= null;
314 private void loadIpaJaasConfiguration() {
315 if (CmsStateImpl
.getDeployProperty(cmsState
, CmsDeployProperty
.JAVA_LOGIN_CONFIG
) == null) {
316 String jaasConfig
= KernelConstants
.JAAS_CONFIG_IPA
;
317 URL url
= getClass().getClassLoader().getResource(jaasConfig
);
318 KernelUtils
.setJaasConfiguration(url
);
319 log
.debug("Set IPA JAAS configuration.");
323 protected String
getKerberosServicePrincipal(String realm
) {
324 if (!Files
.exists(nodeKeyTab
))
326 List
<String
> dns
= CmsStateImpl
.getDeployProperties(cmsState
, CmsDeployProperty
.DNS
);
327 String hostname
= CmsStateImpl
.getDeployProperty(cmsState
, CmsDeployProperty
.HOST
);
328 try (DnsBrowser dnsBrowser
= new DnsBrowser(dns
)) {
329 hostname
= hostname
!= null ? hostname
: InetAddress
.getLocalHost().getHostName();
330 String dnsZone
= hostname
.substring(hostname
.indexOf('.') + 1);
331 String ipv4fromDns
= dnsBrowser
.getRecord(hostname
, "A");
332 String ipv6fromDns
= dnsBrowser
.getRecord(hostname
, "AAAA");
333 if (ipv4fromDns
== null && ipv6fromDns
== null)
334 throw new IllegalStateException("hostname " + hostname
+ " is not registered in DNS");
335 // boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
336 String kerberosDomain
= dnsBrowser
.getRecord("_kerberos." + dnsZone
, "TXT");
337 if (kerberosDomain
!= null && kerberosDomain
.equals(realm
)) {
338 return KernelConstants
.DEFAULT_KERBEROS_SERVICE
+ "/" + hostname
+ "@" + kerberosDomain
;
341 } catch (Exception e
) {
342 log
.warn("Exception when determining kerberos principal", e
);
347 private GSSCredential
logInAsAcceptor(Subject subject
, String servicePrincipal
) {
348 // not static because class is not supported by Android
349 final Oid KERBEROS_OID
;
351 KERBEROS_OID
= new Oid("1.3.6.1.5.5.2");
352 } catch (GSSException e
) {
353 throw new IllegalStateException("Cannot create Kerberos OID", e
);
356 Iterator
<KerberosPrincipal
> krb5It
= subject
.getPrincipals(KerberosPrincipal
.class).iterator();
357 if (!krb5It
.hasNext())
359 KerberosPrincipal krb5Principal
= null;
360 while (krb5It
.hasNext()) {
361 KerberosPrincipal principal
= krb5It
.next();
362 if (principal
.getName().equals(servicePrincipal
))
363 krb5Principal
= principal
;
366 if (krb5Principal
== null)
369 GSSManager manager
= GSSManager
.getInstance();
371 GSSName gssName
= manager
.createName(krb5Principal
.getName(), null);
372 GSSCredential serverCredentials
= Subject
.doAs(subject
, new PrivilegedExceptionAction
<GSSCredential
>() {
375 public GSSCredential
run() throws GSSException
{
376 return manager
.createCredential(gssName
, GSSCredential
.INDEFINITE_LIFETIME
, KERBEROS_OID
,
377 GSSCredential
.ACCEPT_ONLY
);
382 if (log
.isDebugEnabled())
383 log
.debug("GSS acceptor configured for " + krb5Principal
);
384 return serverCredentials
;
385 } catch (Exception gsse
) {
386 throw new IllegalStateException("Cannot create acceptor credentials for " + krb5Principal
, gsse
);
390 public GSSCredential
getAcceptorCredentials() {
391 return acceptorCredentials
;
394 public boolean hasAcceptorCredentials() {
395 return acceptorCredentials
!= null;
398 public boolean isSingleUser() {
402 public void setTransactionManager(WorkControl transactionManager
) {
403 this.transactionManager
= transactionManager
;
406 public void setUserTransaction(WorkTransaction userTransaction
) {
407 this.userTransaction
= userTransaction
;
410 public void setCmsState(CmsState cmsState
) {
411 this.cmsState
= cmsState
;