X-Git-Url: https://git.argeo.org/?a=blobdiff_plain;ds=sidebyside;f=org.argeo.cms%2Fsrc%2Forg%2Fargeo%2Fcms%2Fosgi%2Fuseradmin%2FDirectoryUserAdmin.java;fp=org.argeo.cms%2Fsrc%2Forg%2Fargeo%2Fcms%2Fosgi%2Fuseradmin%2FDirectoryUserAdmin.java;h=59fb05dc3b6e2e3e2160188cce194e1588b17e83;hb=54df376a9c2dd458a82eaa09bfbb718fe699dd0d;hp=0000000000000000000000000000000000000000;hpb=3c1cdc594d954520b14646102b366290bdad58c7;p=lgpl%2Fargeo-commons.git diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUserAdmin.java new file mode 100644 index 000000000..59fb05dc3 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUserAdmin.java @@ -0,0 +1,400 @@ +package org.argeo.cms.osgi.useradmin; + +import static org.argeo.api.acr.ldap.LdapAttrs.objectClass; +import static org.argeo.api.acr.ldap.LdapObjs.extensibleObject; +import static org.argeo.api.acr.ldap.LdapObjs.inetOrgPerson; +import static org.argeo.api.acr.ldap.LdapObjs.organizationalPerson; +import static org.argeo.api.acr.ldap.LdapObjs.person; +import static org.argeo.api.acr.ldap.LdapObjs.top; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import javax.naming.Context; +import javax.naming.InvalidNameException; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosTicket; + +import org.argeo.api.cms.directory.DirectoryDigestUtils; +import org.argeo.api.cms.directory.HierarchyUnit; +import org.argeo.cms.directory.ldap.AbstractLdapDirectory; +import org.argeo.cms.directory.ldap.LdapDao; +import org.argeo.cms.directory.ldap.LdapEntry; +import org.argeo.cms.directory.ldap.LdapEntryWorkingCopy; +import org.argeo.cms.directory.ldap.LdapNameUtils; +import org.argeo.cms.directory.ldap.LdifDao; +import org.argeo.cms.runtime.DirectoryConf; +import org.argeo.cms.util.CurrentSubject; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Authorization; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdmin; + +/** Base class for a {@link UserDirectory}. */ +public class DirectoryUserAdmin extends AbstractLdapDirectory implements UserAdmin, UserDirectory { + + private UserAdmin externalRoles; + + // Transaction + public DirectoryUserAdmin(URI uriArg, Dictionary props) { + this(uriArg, props, false); + } + + public DirectoryUserAdmin(URI uriArg, Dictionary props, boolean scoped) { + super(uriArg, props, scoped); + } + + public DirectoryUserAdmin(Dictionary props) { + this(null, props); + } + + /* + * ABSTRACT METHODS + */ + + protected Optional scope(User user) { + if (getDirectoryDao() instanceof LdapDao) { + return scopeLdap(user); + } else if (getDirectoryDao() instanceof LdifDao) { + return scopeLdif(user); + } else { + throw new IllegalStateException("Unsupported DAO " + getDirectoryDao().getClass()); + } + } + + protected Optional scopeLdap(User user) { + Dictionary credentials = user.getCredentials(); + String username = (String) credentials.get(SHARED_STATE_USERNAME); + if (username == null) + username = user.getName(); + Dictionary properties = cloneConfigProperties(); + properties.put(Context.SECURITY_PRINCIPAL, username.toString()); + Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); + byte[] pwd = (byte[]) pwdCred; + if (pwd != null) { + char[] password = DirectoryDigestUtils.bytesToChars(pwd); + properties.put(Context.SECURITY_CREDENTIALS, new String(password)); + } else { + properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + } + DirectoryUserAdmin scopedDirectory = new DirectoryUserAdmin(null, properties, true); + scopedDirectory.init(); + // check connection + if (!scopedDirectory.getDirectoryDao().checkConnection()) + return Optional.empty(); + return Optional.of(scopedDirectory); + } + + protected Optional scopeLdif(User user) { + Dictionary credentials = user.getCredentials(); + String username = (String) credentials.get(SHARED_STATE_USERNAME); + if (username == null) + username = user.getName(); + Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); + byte[] pwd = (byte[]) pwdCred; + if (pwd != null) { + char[] password = DirectoryDigestUtils.bytesToChars(pwd); + User directoryUser = (User) getRole(username); + if (!directoryUser.hasCredential(null, password)) + throw new IllegalStateException("Invalid credentials"); + } else { + throw new IllegalStateException("Password is required"); + } + Dictionary properties = cloneConfigProperties(); + properties.put(DirectoryConf.readOnly.name(), "true"); + DirectoryUserAdmin scopedUserAdmin = new DirectoryUserAdmin(null, properties, true); + // FIXME do it better + ((LdifDao) getDirectoryDao()).scope((LdifDao) scopedUserAdmin.getDirectoryDao()); + // no need to check authentication + scopedUserAdmin.init(); + return Optional.of(scopedUserAdmin); + } + + @Override + public String getRolePath(Role role) { + return nameToRelativePath(LdapNameUtils.toLdapName(role.getName())); + } + + @Override + public String getRoleSimpleName(Role role) { + LdapName dn = LdapNameUtils.toLdapName(role.getName()); + String name = LdapNameUtils.getLastRdnValue(dn); + return name; + } + + @Override + public Role getRoleByPath(String path) { + LdapEntry entry = doGetRole(pathToName(path)); + if (!(entry instanceof Role)) { + return null; +// throw new IllegalStateException("Path must be a UserAdmin Role."); + } else { + return (Role) entry; + } + } + + protected List getAllRoles(DirectoryUser user) { + List allRoles = new ArrayList(); + if (user != null) { + collectRoles((LdapEntry) user, allRoles); + allRoles.add(user); + } else + collectAnonymousRoles(allRoles); + return allRoles; + } + + private void collectRoles(LdapEntry user, List allRoles) { + List allEntries = new ArrayList<>(); + LdapEntry entry = user; + collectGroups(entry, allEntries); + for (LdapEntry e : allEntries) { + if (e instanceof Role) + allRoles.add((Role) e); + } + } + + private void collectAnonymousRoles(List allRoles) { + // TODO gather anonymous roles + } + + // USER ADMIN + @Override + public Role getRole(String name) { + return (Role) doGetRole(toLdapName(name)); + } + + @Override + public Role[] getRoles(String filter) throws InvalidSyntaxException { + List res = getRoles(getBaseDn(), filter, true); + return res.toArray(new Role[res.size()]); + } + + List getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException { + LdapEntryWorkingCopy wc = getWorkingCopy(); +// Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; + List searchRes = getDirectoryDao().doGetEntries(searchBase, filter, deep); + List res = new ArrayList<>(); + for (LdapEntry entry : searchRes) + res.add((DirectoryUser) entry); + if (wc != null) { + for (Iterator it = res.iterator(); it.hasNext();) { + DirectoryUser user = (DirectoryUser) it.next(); + LdapName dn = LdapNameUtils.toLdapName(user.getName()); + if (wc.getDeletedData().containsKey(dn)) + it.remove(); + } + Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; + for (LdapEntry ldapEntry : wc.getNewData().values()) { + DirectoryUser user = (DirectoryUser) ldapEntry; + if (f == null || f.match(user.getProperties())) + res.add(user); + } + // no need to check modified users, + // since doGetRoles was already based on the modified attributes + } + return res; + } + + @Override + public User getUser(String key, String value) { + // TODO check value null or empty + List collectedUsers = new ArrayList(); + if (key != null) { + doGetUser(key, value, collectedUsers); + } else { + throw new IllegalArgumentException("Key cannot be null"); + } + + if (collectedUsers.size() == 1) { + return collectedUsers.get(0); + } else if (collectedUsers.size() > 1) { + // log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" : + // "") + value); + } + return null; + } + + protected void doGetUser(String key, String value, List collectedUsers) { + String f = "(" + key + "=" + value + ")"; + List users = getDirectoryDao().doGetEntries(getBaseDn(), f, true); + for (LdapEntry entry : users) + collectedUsers.add((DirectoryUser) entry); + } + + @Override + public Authorization getAuthorization(User user) { + if (user == null) {// anonymous + return new LdifAuthorization(user, getAllRoles(null)); + } + LdapName userName = toLdapName(user.getName()); + if (isExternal(userName) && user instanceof LdapEntry) { + List allRoles = new ArrayList(); + collectRoles((LdapEntry) user, allRoles); + return new LdifAuthorization(user, allRoles); + } else { + + Subject currentSubject = CurrentSubject.current(); + if (currentSubject != null // + && getRealm().isPresent() // + && !currentSubject.getPrivateCredentials(Authorization.class).isEmpty() // + && !currentSubject.getPrivateCredentials(KerberosTicket.class).isEmpty()) // + { + // TODO not only Kerberos but also bind scope with kept password ? + Authorization auth = currentSubject.getPrivateCredentials(Authorization.class).iterator().next(); + // bind with authenticating user + DirectoryUserAdmin scopedUserAdmin = CurrentSubject.callAs(currentSubject, () -> { + return scope(new AuthenticatingUser(auth.getName(), new Hashtable<>())).orElseThrow(); + }); + return getAuthorizationFromScoped(scopedUserAdmin, user); + } + + if (user instanceof DirectoryUser) { + return new LdifAuthorization(user, getAllRoles((DirectoryUser) user)); + } else { + // bind with authenticating user + DirectoryUserAdmin scopedUserAdmin = scope(user).orElseThrow(); + return getAuthorizationFromScoped(scopedUserAdmin, user); + } + } + } + + private Authorization getAuthorizationFromScoped(DirectoryUserAdmin scopedUserAdmin, User user) { + try { + DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName()); + if (directoryUser == null) + throw new IllegalStateException("No scoped user found for " + user); + LdifAuthorization authorization = new LdifAuthorization(directoryUser, + scopedUserAdmin.getAllRoles(directoryUser)); + return authorization; + } finally { + scopedUserAdmin.destroy(); + } + } + + @Override + public Role createRole(String name, int type) { + checkEdit(); + LdapEntryWorkingCopy wc = getWorkingCopy(); + LdapName dn = toLdapName(name); + if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn)) + || wc.getNewData().containsKey(dn)) + throw new IllegalArgumentException("Already a role " + name); + BasicAttributes attrs = new BasicAttributes(true); + // attrs.put(LdifName.dn.name(), dn.toString()); + Rdn nameRdn = dn.getRdn(dn.size() - 1); + // TODO deal with multiple attr RDN + attrs.put(nameRdn.getType(), nameRdn.getValue()); + if (wc.getDeletedData().containsKey(dn)) { + wc.getDeletedData().remove(dn); + wc.getModifiedData().put(dn, attrs); + return getRole(name); + } else { + wc.getModifiedData().put(dn, attrs); + LdapEntry newRole = doCreateRole(dn, type, attrs); + wc.getNewData().put(dn, newRole); + return (Role) newRole; + } + } + + private LdapEntry doCreateRole(LdapName dn, int type, Attributes attrs) { + LdapEntry newRole; + BasicAttribute objClass = new BasicAttribute(objectClass.name()); + if (type == Role.USER) { + String userObjClass = getUserObjectClass(); + objClass.add(userObjClass); + if (inetOrgPerson.name().equals(userObjClass)) { + objClass.add(organizationalPerson.name()); + objClass.add(person.name()); + } else if (organizationalPerson.name().equals(userObjClass)) { + objClass.add(person.name()); + } + objClass.add(top.name()); + objClass.add(extensibleObject.name()); + attrs.put(objClass); + newRole = newUser(dn); + } else if (type == Role.GROUP) { + String groupObjClass = getGroupObjectClass(); + objClass.add(groupObjClass); + // objClass.add(LdifName.extensibleObject.name()); + objClass.add(top.name()); + attrs.put(objClass); + newRole = newGroup(dn); + } else + throw new IllegalArgumentException("Unsupported type " + type); + return newRole; + } + + @Override + public boolean removeRole(String name) { + return removeEntry(LdapNameUtils.toLdapName(name)); + } + + /* + * HIERARCHY + */ + @Override + public HierarchyUnit getHierarchyUnit(Role role) { + LdapName dn = LdapNameUtils.toLdapName(role.getName()); + LdapName huDn = LdapNameUtils.getParent(dn); + HierarchyUnit hierarchyUnit = getDirectoryDao().doGetHierarchyUnit(huDn); + if (hierarchyUnit == null) + throw new IllegalStateException("No hierarchy unit found for " + role); + return hierarchyUnit; + } + + @Override + public Iterable getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep) { + LdapName dn = LdapNameUtils.toLdapName(hierarchyUnit.getBase()); + try { + return getRoles(dn, filter, deep); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot filter " + filter + " " + dn, e); + } + } + + /* + * ROLES CREATION + */ + protected LdapEntry newUser(LdapName name) { + // TODO support devices, applications, etc. + return new LdifUser(this, name); + } + + protected LdapEntry newGroup(LdapName name) { + return new LdifGroup(this, name); + + } + + // GETTERS + protected UserAdmin getExternalRoles() { + return externalRoles; + } + + public void setExternalRoles(UserAdmin externalRoles) { + this.externalRoles = externalRoles; + } + + /* + * STATIC UTILITIES + */ + static LdapName toLdapName(String name) { + try { + return new LdapName(name); + } catch (InvalidNameException e) { + throw new IllegalArgumentException(name + " is not an LDAP name", e); + } + } +}