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
.cms
.CmsDeployProperty
;
33 import org
.argeo
.osgi
.useradmin
.AggregatingUserAdmin
;
34 import org
.argeo
.osgi
.useradmin
.DirectoryUserAdmin
;
35 import org
.argeo
.osgi
.useradmin
.UserDirectory
;
36 import org
.argeo
.util
.directory
.DirectoryConf
;
37 import org
.argeo
.util
.naming
.dns
.DnsBrowser
;
38 import org
.argeo
.util
.transaction
.WorkControl
;
39 import org
.argeo
.util
.transaction
.WorkTransaction
;
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
.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_NODE
);
92 List
<String
> uris
= new ArrayList
<>();
95 String nodeRolesUri
= null;// getFrameworkProp(CmsConstants.ROLES_URI);
96 String baseNodeRoleDn
= CmsConstants
.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
);
180 Dictionary
<String
, Object
> properties
= DirectoryConf
.uriAsProperties(u
.toString());
187 public UserDirectory
enableUserDirectory(Dictionary
<String
, ?
> properties
) {
188 String uri
= (String
) properties
.get(DirectoryConf
.uri
.name());
189 Object realm
= properties
.get(DirectoryConf
.realm
.name());
193 String baseDn
= (String
) properties
.get(DirectoryConf
.baseDn
.name());
194 u
= KernelUtils
.getOsgiInstanceUri(KernelConstants
.DIR_NODE
+ '/' + baseDn
+ ".ldif");
195 } else if (realm
!= null) {
200 } catch (URISyntaxException e
) {
201 throw new IllegalArgumentException("Badly formatted URI " + uri
, e
);
205 UserDirectory userDirectory
= new DirectoryUserAdmin(u
, properties
);
206 // if (realm != null || DirectoryConf.SCHEME_LDAP.equals(u.getScheme())
207 // || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
208 // userDirectory = new LdapUserAdmin(properties);
209 // } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
210 // userDirectory = new LdifUserAdmin(u, properties);
211 // } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
212 // userDirectory = new OsUserDirectory(u, properties);
213 // singleUser = true;
215 // throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
217 String basePath
= userDirectory
.getContext();
219 addUserDirectory(userDirectory
);
220 if (isSystemRolesBaseDn(basePath
)) {
221 addStandardSystemRoles();
223 if (log
.isDebugEnabled()) {
224 log
.debug("User directory " + userDirectory
.getContext() + (u
!= null ?
" [" + u
.getScheme() + "]" : "")
225 + " enabled." + (realm
!= null ?
" " + realm
+ " realm." : ""));
227 return userDirectory
;
230 protected void addStandardSystemRoles() {
231 // we assume UserTransaction is already available (TODO make it more robust)
233 userTransaction
.begin();
234 Role adminRole
= getRole(CmsConstants
.ROLE_ADMIN
);
235 if (adminRole
== null) {
236 adminRole
= createRole(CmsConstants
.ROLE_ADMIN
, Role
.GROUP
);
238 if (getRole(CmsConstants
.ROLE_USER_ADMIN
) == null) {
239 Group userAdminRole
= (Group
) createRole(CmsConstants
.ROLE_USER_ADMIN
, Role
.GROUP
);
240 userAdminRole
.addMember(adminRole
);
242 userTransaction
.commit();
243 } catch (Exception e
) {
245 userTransaction
.rollback();
246 } catch (Exception e1
) {
249 throw new IllegalStateException("Cannot add standard system roles", e
);
254 protected void addAbstractSystemRoles(Authorization rawAuthorization
, Set
<String
> sysRoles
) {
255 if (rawAuthorization
.getName() == null) {
256 sysRoles
.add(CmsConstants
.ROLE_ANONYMOUS
);
258 sysRoles
.add(CmsConstants
.ROLE_USER
);
263 protected void postAdd(UserDirectory userDirectory
) {
264 userDirectory
.setTransactionControl(transactionManager
);
266 Optional
<String
> realm
= userDirectory
.getRealm();
267 if (realm
.isPresent()) {
268 loadIpaJaasConfiguration();
269 if (Files
.exists(nodeKeyTab
)) {
270 String servicePrincipal
= getKerberosServicePrincipal(realm
.get());
271 if (servicePrincipal
!= null) {
272 CallbackHandler callbackHandler
= new CallbackHandler() {
274 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
275 for (Callback callback
: callbacks
)
276 if (callback
instanceof NameCallback
)
277 ((NameCallback
) callback
).setName(servicePrincipal
);
282 LoginContext nodeLc
= CmsAuth
.NODE
.newLoginContext(callbackHandler
);
284 acceptorCredentials
= logInAsAcceptor(nodeLc
.getSubject(), servicePrincipal
);
285 } catch (LoginException e
) {
286 throw new IllegalStateException("Cannot log in kernel", e
);
295 protected void preDestroy(UserDirectory userDirectory
) {
296 Optional
<String
> realm
= userDirectory
.getRealm();
297 if (realm
.isPresent()) {
298 if (acceptorCredentials
!= null) {
300 acceptorCredentials
.dispose();
301 } catch (GSSException e
) {
304 acceptorCredentials
= null;
309 private void loadIpaJaasConfiguration() {
310 if (CmsStateImpl
.getDeployProperty(cmsState
, CmsDeployProperty
.JAVA_LOGIN_CONFIG
) == null) {
311 String jaasConfig
= KernelConstants
.JAAS_CONFIG_IPA
;
312 URL url
= getClass().getClassLoader().getResource(jaasConfig
);
313 KernelUtils
.setJaasConfiguration(url
);
314 log
.debug("Set IPA JAAS configuration.");
318 protected String
getKerberosServicePrincipal(String realm
) {
319 if (!Files
.exists(nodeKeyTab
))
321 List
<String
> dns
= CmsStateImpl
.getDeployProperties(cmsState
, CmsDeployProperty
.DNS
);
322 String hostname
= CmsStateImpl
.getDeployProperty(cmsState
, CmsDeployProperty
.HOST
);
323 try (DnsBrowser dnsBrowser
= new DnsBrowser(dns
)) {
324 hostname
= hostname
!= null ? hostname
: InetAddress
.getLocalHost().getHostName();
325 String dnsZone
= hostname
.substring(hostname
.indexOf('.') + 1);
326 String ipv4fromDns
= dnsBrowser
.getRecord(hostname
, "A");
327 String ipv6fromDns
= dnsBrowser
.getRecord(hostname
, "AAAA");
328 if (ipv4fromDns
== null && ipv6fromDns
== null)
329 throw new IllegalStateException("hostname " + hostname
+ " is not registered in DNS");
330 // boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
331 String kerberosDomain
= dnsBrowser
.getRecord("_kerberos." + dnsZone
, "TXT");
332 if (kerberosDomain
!= null && kerberosDomain
.equals(realm
)) {
333 return KernelConstants
.DEFAULT_KERBEROS_SERVICE
+ "/" + hostname
+ "@" + kerberosDomain
;
336 } catch (Exception e
) {
337 log
.warn("Exception when determining kerberos principal", e
);
342 private GSSCredential
logInAsAcceptor(Subject subject
, String servicePrincipal
) {
343 // not static because class is not supported by Android
344 final Oid KERBEROS_OID
;
346 KERBEROS_OID
= new Oid("1.3.6.1.5.5.2");
347 } catch (GSSException e
) {
348 throw new IllegalStateException("Cannot create Kerberos OID", e
);
351 Iterator
<KerberosPrincipal
> krb5It
= subject
.getPrincipals(KerberosPrincipal
.class).iterator();
352 if (!krb5It
.hasNext())
354 KerberosPrincipal krb5Principal
= null;
355 while (krb5It
.hasNext()) {
356 KerberosPrincipal principal
= krb5It
.next();
357 if (principal
.getName().equals(servicePrincipal
))
358 krb5Principal
= principal
;
361 if (krb5Principal
== null)
364 GSSManager manager
= GSSManager
.getInstance();
366 GSSName gssName
= manager
.createName(krb5Principal
.getName(), null);
367 GSSCredential serverCredentials
= Subject
.doAs(subject
, new PrivilegedExceptionAction
<GSSCredential
>() {
370 public GSSCredential
run() throws GSSException
{
371 return manager
.createCredential(gssName
, GSSCredential
.INDEFINITE_LIFETIME
, KERBEROS_OID
,
372 GSSCredential
.ACCEPT_ONLY
);
377 if (log
.isDebugEnabled())
378 log
.debug("GSS acceptor configured for " + krb5Principal
);
379 return serverCredentials
;
380 } catch (Exception gsse
) {
381 throw new IllegalStateException("Cannot create acceptor credentials for " + krb5Principal
, gsse
);
385 public GSSCredential
getAcceptorCredentials() {
386 return acceptorCredentials
;
389 public boolean hasAcceptorCredentials() {
390 return acceptorCredentials
!= null;
393 public boolean isSingleUser() {
397 public void setTransactionManager(WorkControl transactionManager
) {
398 this.transactionManager
= transactionManager
;
401 public void setUserTransaction(WorkTransaction userTransaction
) {
402 this.userTransaction
= userTransaction
;
405 public void setCmsState(CmsState cmsState
) {
406 this.cmsState
= cmsState
;