X-Git-Url: http://git.argeo.org/?a=blobdiff_plain;f=org.argeo.security.core%2Fsrc%2Forg%2Fargeo%2Fosgi%2Fuseradmin%2FAbstractUserDirectory.java;h=f871cd25e8af8c8c97e96e06ab9f62ff2136bcaa;hb=081e25f3b95f41cf1d400f5dc926387e9f92f8aa;hp=77f68463fe4452101d4fe1f4af66e00050cf3d52;hpb=9a9418f4c0df975756de3093df71d757c72a386d;p=lgpl%2Fargeo-commons.git diff --git a/org.argeo.security.core/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java b/org.argeo.security.core/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java index 77f68463f..f871cd25e 100644 --- a/org.argeo.security.core/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java +++ b/org.argeo.security.core/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java @@ -1,68 +1,111 @@ package org.argeo.osgi.useradmin; +import static org.argeo.osgi.useradmin.LdifName.gidNumber; +import static org.argeo.osgi.useradmin.LdifName.homeDirectory; +import static org.argeo.osgi.useradmin.LdifName.inetOrgPerson; +import static org.argeo.osgi.useradmin.LdifName.objectClass; +import static org.argeo.osgi.useradmin.LdifName.organizationalPerson; +import static org.argeo.osgi.useradmin.LdifName.person; +import static org.argeo.osgi.useradmin.LdifName.posixAccount; +import static org.argeo.osgi.useradmin.LdifName.top; +import static org.argeo.osgi.useradmin.LdifName.uid; +import static org.argeo.osgi.useradmin.LdifName.uidNumber; + +import java.io.File; import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; import java.util.Iterator; import java.util.List; -import java.util.Map; import javax.naming.InvalidNameException; +import javax.naming.NamingException; 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.transaction.SystemException; import javax.transaction.Transaction; import javax.transaction.TransactionManager; -import javax.transaction.TransactionSynchronizationRegistry; -import javax.transaction.xa.XAException; -import javax.transaction.xa.XAResource; -import javax.transaction.xa.Xid; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.argeo.ArgeoException; 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.Group; import org.osgi.service.useradmin.Role; import org.osgi.service.useradmin.User; import org.osgi.service.useradmin.UserAdmin; -public abstract class AbstractUserDirectory implements UserAdmin { +/** Base class for a {@link UserDirectory}. */ +abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { private final static Log log = LogFactory .getLog(AbstractUserDirectory.class); - private boolean isReadOnly; - private URI uri; + + private final Hashtable properties; + private final String baseDn; + private final String userObjectClass; + private final String groupObjectClass; + + private final boolean readOnly; + private final URI uri; private UserAdmin externalRoles; private List indexedUserProperties = Arrays.asList(new String[] { - "uid", "mail", "cn" }); + LdifName.uid.name(), LdifName.mail.name(), LdifName.cn.name() }); private String memberAttributeId = "member"; private List credentialAttributeIds = Arrays - .asList(new String[] { "userpassword" }); - - // private TransactionSynchronizationRegistry syncRegistry; - // private Object editingTransactionKey = null; + .asList(new String[] { LdifName.userPassword.name() }); private TransactionManager transactionManager; - private ThreadLocal workingCopy = new ThreadLocal(); - private Xid editingTransactionXid = null; + // private TransactionSynchronizationRegistry transactionRegistry; + // private Xid editingTransactionXid = null; + private WcXaResource xaResource = new WcXaResource(this); + + // POSIX + private String homeDirectoryBase = "/home"; + + AbstractUserDirectory(Dictionary props) { + properties = new Hashtable(); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + properties.put(key, props.get(key)); + } - public AbstractUserDirectory() { - } + String uriStr = UserAdminConf.uri.getValue(properties); + if (uriStr == null) + uri = null; + else + try { + uri = new URI(uriStr); + } catch (URISyntaxException e) { + throw new UserDirectoryException("Badly formatted URI " + + uriStr, e); + } + + baseDn = UserAdminConf.baseDn.getValue(properties).toString(); + String readOnlyStr = UserAdminConf.readOnly.getValue(properties); + if (readOnlyStr == null) { + readOnly = readOnlyDefault(uri); + properties.put(UserAdminConf.readOnly.property(), + Boolean.toString(readOnly)); + } else + readOnly = new Boolean(readOnlyStr); - public AbstractUserDirectory(URI uri, boolean isReadOnly) { - this.uri = uri; - this.isReadOnly = isReadOnly; + userObjectClass = UserAdminConf.userObjectClass.getValue(properties); + groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties); } - /** Returns the {@link Group}s this user is a direct member of. */ - protected abstract List getDirectGroups(User user); + /** Returns the groups this user is a direct member of. */ + protected abstract List getDirectGroups(LdapName dn); protected abstract Boolean daoHasRole(LdapName dn); @@ -70,9 +113,6 @@ public abstract class AbstractUserDirectory implements UserAdmin { protected abstract List doGetRoles(Filter f); - protected abstract void doGetUser(String key, String value, - List collectedUsers); - public void init() { } @@ -82,23 +122,21 @@ public abstract class AbstractUserDirectory implements UserAdmin { } boolean isEditing() { - if (editingTransactionXid == null) - return false; - return workingCopy.get() != null; - // Object currentTrKey = syncRegistry.getTransactionKey(); - // if (currentTrKey == null) + // if (editingTransactionXid == null) // return false; - // return editingTransactionKey.equals(currentTrKey); + // return workingCopy.get() != null; + return xaResource.wc() != null; } - protected WorkingCopy getWorkingCopy() { - WorkingCopy wc = workingCopy.get(); + protected UserDirectoryWorkingCopy getWorkingCopy() { + // UserDirectoryWorkingCopy wc = workingCopy.get(); + UserDirectoryWorkingCopy wc = xaResource.wc(); if (wc == null) return null; - if (wc.xid == null) { - workingCopy.set(null); - return null; - } + // if (wc.getXid() == null) { + // workingCopy.set(null); + // return null; + // } return wc; } @@ -112,27 +150,30 @@ public abstract class AbstractUserDirectory implements UserAdmin { if (transaction == null) throw new UserDirectoryException( "A transaction needs to be active in order to edit"); - if (editingTransactionXid == null) { - WorkingCopy wc = new WorkingCopy(); + if (xaResource.wc() == null) { + // UserDirectoryWorkingCopy wc = new UserDirectoryWorkingCopy(this); try { - transaction.enlistResource(wc); - editingTransactionXid = wc.getXid(); - workingCopy.set(wc); + transaction.enlistResource(xaResource); + // editingTransactionXid = wc.getXid(); + // workingCopy.set(wc); } catch (Exception e) { - throw new UserDirectoryException("Cannot enlist " + wc, e); + throw new UserDirectoryException("Cannot enlist " + xaResource, + e); } } else { - if (workingCopy.get() == null) - throw new UserDirectoryException("Transaction " - + editingTransactionXid + " already editing"); - else if (!editingTransactionXid.equals(workingCopy.get().getXid())) - throw new UserDirectoryException("Working copy Xid " - + workingCopy.get().getXid() + " inconsistent with" - + editingTransactionXid); + // UserDirectoryWorkingCopy wc = xaResource.wc(); + // if (wc == null) + // throw new UserDirectoryException("Transaction " + // + editingTransactionXid + " already editing"); + // else if + // (!editingTransactionXid.equals(workingCopy.get().getXid())) + // throw new UserDirectoryException("Working copy Xid " + // + workingCopy.get().getXid() + " inconsistent with" + // + editingTransactionXid); } } - List getAllRoles(User user) { + List getAllRoles(DirectoryUser user) { List allRoles = new ArrayList(); if (user != null) { collectRoles(user, allRoles); @@ -142,9 +183,10 @@ public abstract class AbstractUserDirectory implements UserAdmin { return allRoles; } - private void collectRoles(User user, List allRoles) { - for (Group group : getDirectGroups(user)) { + private void collectRoles(DirectoryUser user, List allRoles) { + for (LdapName groupDn : getDirectGroups(user.getDn())) { // TODO check for loops + DirectoryUser group = doGetRole(groupDn); allRoles.add(group); collectRoles(group, allRoles); } @@ -157,21 +199,25 @@ public abstract class AbstractUserDirectory implements UserAdmin { // USER ADMIN @Override public Role getRole(String name) { - LdapName key = toDn(name); - WorkingCopy wc = getWorkingCopy(); - DirectoryUser user = daoGetRole(key); + return doGetRole(toDn(name)); + } + + protected DirectoryUser doGetRole(LdapName dn) { + UserDirectoryWorkingCopy wc = getWorkingCopy(); + DirectoryUser user = daoGetRole(dn); if (wc != null) { - if (user == null && wc.getNewUsers().containsKey(key)) - user = wc.getNewUsers().get(key); - else if (wc.getDeletedUsers().containsKey(key)) + if (user == null && wc.getNewUsers().containsKey(dn)) + user = wc.getNewUsers().get(dn); + else if (wc.getDeletedUsers().containsKey(dn)) user = null; } return user; } + @SuppressWarnings("unchecked") @Override public Role[] getRoles(String filter) throws InvalidSyntaxException { - WorkingCopy wc = getWorkingCopy(); + UserDirectoryWorkingCopy wc = getWorkingCopy(); Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; List res = doGetRoles(f); if (wc != null) { @@ -214,9 +260,25 @@ public abstract class AbstractUserDirectory implements UserAdmin { } 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) { + try { + Filter f = FrameworkUtil + .createFilter("(" + key + "=" + value + ")"); + List users = doGetRoles(f); + collectedUsers.addAll(users); + } catch (InvalidSyntaxException e) { + throw new UserDirectoryException("Cannot get user with " + key + + "=" + value, e); + } + } + @Override public Authorization getAuthorization(User user) { return new LdifAuthorization((DirectoryUser) user, @@ -226,12 +288,12 @@ public abstract class AbstractUserDirectory implements UserAdmin { @Override public Role createRole(String name, int type) { checkEdit(); - WorkingCopy wc = getWorkingCopy(); + UserDirectoryWorkingCopy wc = getWorkingCopy(); LdapName dn = toDn(name); if ((daoHasRole(dn) && !wc.getDeletedUsers().containsKey(dn)) || wc.getNewUsers().containsKey(dn)) throw new UserDirectoryException("Already a role " + name); - BasicAttributes attrs = new BasicAttributes(); + BasicAttributes attrs = new BasicAttributes(true); attrs.put("dn", dn.toString()); Rdn nameRdn = dn.getRdn(dn.size() - 1); // TODO deal with multiple attr RDN @@ -249,12 +311,47 @@ public abstract class AbstractUserDirectory implements UserAdmin { protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) { LdifUser newRole; + BasicAttribute objClass = new BasicAttribute(objectClass.name()); if (type == Role.USER) { + String userObjClass = newUserObjectClass(dn); + objClass.add(userObjClass); + if (posixAccount.name().equals(userObjClass)) { + objClass.add(inetOrgPerson.name()); + objClass.add(organizationalPerson.name()); + objClass.add(person.name()); + + String username; + try { + username = dn.getRdn(dn.size() - 1).toAttributes() + .get(uid.name()).get().toString(); + } catch (NamingException e) { + throw new UserDirectoryException( + "Cannot extract username from " + dn, e); + } + // TODO look for uid in attributes too? + attrs.put(uidNumber.name(), new Long(max(uidNumber.name()) + 1)); + attrs.put(homeDirectory.name(), generateHomeDirectory(username)); + // TODO create user private group + // NB: on RHEL, the 'users' group has gid 100 + attrs.put(gidNumber.name(), 100); + // attrs.put(LdifName.loginShell.name(),"/sbin/nologin"); + } else 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); + attrs.put(objClass); newRole = new LdifUser(this, dn, attrs); - // users.put(dn, newRole); } else if (type == Role.GROUP) { + String groupObjClass = getGroupObjectClass(); + objClass.add(groupObjClass); + objClass.add(LdifName.extensibleObject.name()); + attrs.put(gidNumber.name(), new Long(max(gidNumber.name()) + 1)); + objClass.add(top); + attrs.put(objClass); newRole = new LdifGroup(this, dn, attrs); - // groups.put(dn, (LdifGroup) newRole); } else throw new UserDirectoryException("Unsupported type " + type); return newRole; @@ -263,33 +360,95 @@ public abstract class AbstractUserDirectory implements UserAdmin { @Override public boolean removeRole(String name) { checkEdit(); - WorkingCopy wc = getWorkingCopy(); + UserDirectoryWorkingCopy wc = getWorkingCopy(); LdapName dn = toDn(name); - if (!daoHasRole(dn) && !wc.getNewUsers().containsKey(dn)) - return false; - DirectoryUser user = (DirectoryUser) getRole(name); - wc.getDeletedUsers().put(dn, user); - // FIXME clarify directgroups - for (DirectoryGroup group : getDirectGroups(user)) { + boolean actuallyDeleted; + if (daoHasRole(dn) || wc.getNewUsers().containsKey(dn)) { + DirectoryUser user = (DirectoryUser) getRole(name); + wc.getDeletedUsers().put(dn, user); + actuallyDeleted = true; + } else {// just removing from groups (e.g. system roles) + actuallyDeleted = false; + } + for (LdapName groupDn : getDirectGroups(dn)) { + DirectoryUser group = doGetRole(groupDn); group.getAttributes().get(getMemberAttributeId()) .remove(dn.toString()); } - return true; + return actuallyDeleted; + } + + // POSIX + /** Generate path for a new user home */ + protected String generateHomeDirectory(String username) { + String base = homeDirectoryBase; + int atIndex = username.indexOf('@'); + if (atIndex > 0) { + String domain = username.substring(0, atIndex); + String name = username.substring(atIndex + 1); + return base + '/' + firstCharsToPath(domain, 2) + '/' + domain + + '/' + firstCharsToPath(name, 2) + '/' + name; + } else if (atIndex == 0 || atIndex == (username.length() - 1)) { + throw new ArgeoException("Unsupported username " + username); + } else { + return base + '/' + firstCharsToPath(username, 2) + '/' + username; + } + } + + protected long max(String attr) { + long max; + try { + List users = doGetRoles(FrameworkUtil + .createFilter("(" + attr + "=*)")); + max = 1000; + for (DirectoryUser user : users) { + long uid = Long.parseLong(user.getAttributes().get(attr).get() + .toString()); + if (uid > max) + max = uid; + } + } catch (Exception e) { + throw new UserDirectoryException("Cannot get max of " + attr, e); + } + return max; + } + + /** + * Creates depth from a string (typically a username) by adding levels based + * on its first characters: "aBcD",2 => a/aB + */ + public static String firstCharsToPath(String str, Integer nbrOfChars) { + if (str.length() < nbrOfChars) + throw new ArgeoException("String " + str + + " length must be greater or equal than " + nbrOfChars); + StringBuffer path = new StringBuffer(""); + StringBuffer curr = new StringBuffer(""); + for (int i = 0; i < nbrOfChars; i++) { + curr.append(str.charAt(i)); + path.append(curr); + if (i < nbrOfChars - 1) + path.append('/'); + } + return path.toString(); } // TRANSACTION - protected void prepare(WorkingCopy wc) { + protected void prepare(UserDirectoryWorkingCopy wc) { } - protected void commit(WorkingCopy wc) { + protected void commit(UserDirectoryWorkingCopy wc) { } - protected void rollback(WorkingCopy wc) { + protected void rollback(UserDirectoryWorkingCopy wc) { } + // void clearEditingTransactionXid() { + // editingTransactionXid = null; + // } + // UTILITIES protected LdapName toDn(String name) { try { @@ -313,10 +472,6 @@ public abstract class AbstractUserDirectory implements UserAdmin { return uri; } - protected void setUri(URI uri) { - this.uri = uri; - } - protected List getIndexedUserProperties() { return indexedUserProperties; } @@ -325,180 +480,62 @@ public abstract class AbstractUserDirectory implements UserAdmin { this.indexedUserProperties = indexedUserProperties; } - protected void setReadOnly(boolean isReadOnly) { - this.isReadOnly = isReadOnly; + private static boolean readOnlyDefault(URI uri) { + if (uri == null) + return true; + if (uri.getScheme().equals("file")) { + File file = new File(uri); + if (file.exists()) + return !file.canWrite(); + else + return !file.getParentFile().canWrite(); + } + return true; } public boolean isReadOnly() { - return isReadOnly; + return readOnly; } UserAdmin getExternalRoles() { return externalRoles; } - public void setExternalRoles(UserAdmin externalRoles) { - this.externalRoles = externalRoles; + public String getBaseDn() { + return baseDn; } - public void setSyncRegistry(TransactionSynchronizationRegistry syncRegistry) { - // this.syncRegistry = syncRegistry; + /** dn can be null, in that case a default should be returned. */ + protected String getUserObjectClass() { + return userObjectClass; } - public void setTransactionManager(TransactionManager transactionManager) { - this.transactionManager = transactionManager; + protected String newUserObjectClass(LdapName dn) { + if (dn != null + && dn.getRdn(dn.size() - 1).toAttributes().get(uid.name()) != null) + return posixAccount.name(); + else + return getUserObjectClass(); } - // - // XA RESOURCE - // - protected class WorkingCopy implements XAResource { - private Xid xid; - private int transactionTimeout = 0; - - private Map newUsers = new HashMap(); - private Map modifiedUsers = new HashMap(); - private Map deletedUsers = new HashMap(); - - @Override - public void start(Xid xid, int flags) throws XAException { - if (editingTransactionXid != null) - throw new UserDirectoryException("Transaction " - + editingTransactionXid + " already editing"); - this.xid = xid; - } - - @Override - public void end(Xid xid, int flags) throws XAException { - checkXid(xid); - - // clean collections - newUsers.clear(); - newUsers = null; - modifiedUsers.clear(); - modifiedUsers = null; - deletedUsers.clear(); - deletedUsers = null; - - // clean IDs - this.xid = null; - editingTransactionXid = null; - } - - @Override - public int prepare(Xid xid) throws XAException { - checkXid(xid); - if (noModifications()) - return XA_RDONLY; - try { - AbstractUserDirectory.this.prepare(this); - } catch (Exception e) { - log.error("Cannot prepare " + xid, e); - throw new XAException(XAException.XA_RBOTHER); - } - return XA_OK; - } - - @Override - public void commit(Xid xid, boolean onePhase) throws XAException { - checkXid(xid); - if (noModifications()) - return; - try { - if (onePhase) - AbstractUserDirectory.this.prepare(this); - AbstractUserDirectory.this.commit(this); - } catch (Exception e) { - log.error("Cannot commit " + xid, e); - throw new XAException(XAException.XA_RBOTHER); - } - } - - @Override - public void rollback(Xid xid) throws XAException { - checkXid(xid); - try { - AbstractUserDirectory.this.rollback(this); - } catch (Exception e) { - log.error("Cannot rollback " + xid, e); - throw new XAException(XAException.XA_HEURMIX); - } - } - - @Override - public void forget(Xid xid) throws XAException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isSameRM(XAResource xares) throws XAException { - return xares == this; - } - - @Override - public Xid[] recover(int flag) throws XAException { - throw new UnsupportedOperationException(); - } - - @Override - public int getTransactionTimeout() throws XAException { - return transactionTimeout; - } - - @Override - public boolean setTransactionTimeout(int seconds) throws XAException { - transactionTimeout = seconds; - return true; - } - - private Xid getXid() { - return xid; - } - - private void checkXid(Xid xid) throws XAException { - if (this.xid == null) - throw new XAException(XAException.XAER_OUTSIDE); - if (!this.xid.equals(xid)) - throw new XAException(XAException.XAER_NOTA); - } - - @Override - protected void finalize() throws Throwable { - if (editingTransactionXid != null) - log.warn("Editing transaction still referenced but no working copy " - + editingTransactionXid); - editingTransactionXid = null; - } - - public boolean noModifications() { - return newUsers.size() == 0 && modifiedUsers.size() == 0 - && deletedUsers.size() == 0; - } - - public Attributes getAttributes(LdapName dn) { - if (modifiedUsers.containsKey(dn)) - return modifiedUsers.get(dn); - return null; - } - - public void startEditing(DirectoryUser user) { - LdapName dn = user.getDn(); - if (modifiedUsers.containsKey(dn)) - throw new UserDirectoryException("Already editing " + dn); - modifiedUsers.put(dn, (Attributes) user.getAttributes().clone()); - } + protected String getGroupObjectClass() { + return groupObjectClass; + } - public Map getNewUsers() { - return newUsers; - } + public Dictionary getProperties() { + return properties; + } - public Map getDeletedUsers() { - return deletedUsers; - } + public void setExternalRoles(UserAdmin externalRoles) { + this.externalRoles = externalRoles; + } - public Map getModifiedUsers() { - return modifiedUsers; - } + public void setTransactionManager(TransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + public WcXaResource getXaResource() { + return xaResource; } + }