From: Mathieu Date: Sat, 5 Nov 2022 06:52:27 +0000 (+0100) Subject: Massive package refactoring X-Git-Tag: v2.3.11~26 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=54df376a9c2dd458a82eaa09bfbb718fe699dd0d Massive package refactoring --- diff --git a/Makefile b/Makefile index 3d58eb0c0..330bc4fca 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,8 @@ A2_CATEGORY = org.argeo.cms BUNDLES = \ org.argeo.init \ -org.argeo.util \ org.argeo.api.uuid \ +org.argeo.api.register \ org.argeo.api.acr \ org.argeo.api.cli \ org.argeo.api.cms \ diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ArgeoNamespace.java b/org.argeo.api.acr/src/org/argeo/api/acr/ArgeoNamespace.java new file mode 100644 index 000000000..98131d13e --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ArgeoNamespace.java @@ -0,0 +1,14 @@ +package org.argeo.api.acr; + +/** Namespaces declared by Argeo. */ +public enum ArgeoNamespace { + ; + + public final static String CR_NAMESPACE_URI = "http://www.argeo.org/ns/cr"; + public final static String CR_DEFAULT_PREFIX = "cr"; + public final static String LDAP_NAMESPACE_URI = "http://www.argeo.org/ns/ldap"; + public final static String LDAP_DEFAULT_PREFIX = "ldap"; + public final static String ROLE_NAMESPACE_URI = "http://www.argeo.org/ns/role"; + public final static String ROLE_DEFAULT_PREFIX = "role"; + +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java b/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java index 55ad079ec..3e12fb1c8 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java @@ -28,7 +28,7 @@ public enum CrAttributeType { // we do not support short and float, like recent additions to Java // (e.g. optional primitives) DATE_TIME(Instant.class, W3C_XML_SCHEMA_NS_URI, "dateTime", new InstantFormatter()), // - UUID(UUID.class, CrName.CR_NAMESPACE_URI, "uuid", new UuidFormatter()), // + UUID(UUID.class, ArgeoNamespace.CR_NAMESPACE_URI, "uuid", new UuidFormatter()), // ANY_URI(URI.class, W3C_XML_SCHEMA_NS_URI, "anyUri", new UriFormatter()), // STRING(String.class, W3C_XML_SCHEMA_NS_URI, "string", new StringFormatter()), // ; diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java b/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java index f61833507..ead47377b 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java @@ -34,14 +34,7 @@ public enum CrName implements QNamed { // ; - public final static String CR_NAMESPACE_URI = "http://www.argeo.org/ns/cr"; - public final static String CR_DEFAULT_PREFIX = "cr"; - - public final static String LDAP_NAMESPACE_URI = "http://www.argeo.org/ns/ldap"; - public final static String LDAP_DEFAULT_PREFIX = "ldap"; - - public final static String ROLE_NAMESPACE_URI = "http://www.argeo.org/ns/role"; - public final static String ROLE_DEFAULT_PREFIX = "role"; + // private final ContentName value; @@ -55,12 +48,12 @@ public enum CrName implements QNamed { @Override public String getNamespace() { - return CR_NAMESPACE_URI; + return ArgeoNamespace.CR_NAMESPACE_URI; } @Override public String getDefaultPrefix() { - return CR_DEFAULT_PREFIX; + return ArgeoNamespace.CR_DEFAULT_PREFIX; } } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java b/org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java index 0941597d7..1c55156ee 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java @@ -61,9 +61,9 @@ public class RuntimeNamespaceContext implements NamespaceContext { register(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, XSD_INSTANCE_DEFAULT_PREFIX); // Argeo specific - register(CrName.CR_NAMESPACE_URI, CrName.CR_DEFAULT_PREFIX); - register(CrName.LDAP_NAMESPACE_URI, CrName.LDAP_DEFAULT_PREFIX); - register(CrName.ROLE_NAMESPACE_URI, CrName.ROLE_DEFAULT_PREFIX); + register(ArgeoNamespace.CR_NAMESPACE_URI, ArgeoNamespace.CR_DEFAULT_PREFIX); + register(ArgeoNamespace.LDAP_NAMESPACE_URI, ArgeoNamespace.LDAP_DEFAULT_PREFIX); + register(ArgeoNamespace.ROLE_NAMESPACE_URI, ArgeoNamespace.ROLE_DEFAULT_PREFIX); } public static NamespaceContext getNamespaceContext() { diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/Distinguished.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/Distinguished.java new file mode 100644 index 000000000..52556cf22 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/Distinguished.java @@ -0,0 +1,36 @@ +package org.argeo.api.acr.ldap; + +import java.util.EnumSet; +import java.util.Set; +import java.util.TreeSet; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +/** + * An object that can be identified with an X.500 distinguished name. + * + * @see "https://tools.ietf.org/html/rfc1779" + */ +public interface Distinguished { + /** The related distinguished name. */ + String dn(); + + /** The related distinguished name as an {@link LdapName}. */ + default LdapName distinguishedName() { + try { + return new LdapName(dn()); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Distinguished name " + dn() + " is not properly formatted.", e); + } + } + + /** List all DNs of an enumeration as strings. */ + static Set enumToDns(EnumSet enumSet) { + Set res = new TreeSet<>(); + for (Enum enm : enumSet) { + res.add(((Distinguished) enm).dn()); + } + return res; + } +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapAttrs.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapAttrs.java new file mode 100644 index 000000000..b5af7526f --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapAttrs.java @@ -0,0 +1,369 @@ +package org.argeo.api.acr.ldap; + +import static org.argeo.api.acr.ArgeoNamespace.LDAP_DEFAULT_PREFIX; +import static org.argeo.api.acr.ArgeoNamespace.LDAP_NAMESPACE_URI; + +import java.util.function.Supplier; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.QNamed; +import org.argeo.api.acr.RuntimeNamespaceContext; + +/** + * Standard LDAP attributes as per:
+ * - Standard LDAP
+ * - Kerberos + * LDAP (partial) + */ +public enum LdapAttrs implements QNamed, SpecifiedName, Supplier { + /** */ + uid("0.9.2342.19200300.100.1.1", "RFC 4519"), + /** */ + mail("0.9.2342.19200300.100.1.3", "RFC 4524"), + /** */ + info("0.9.2342.19200300.100.1.4", "RFC 4524"), + /** */ + drink("0.9.2342.19200300.100.1.5", "RFC 4524"), + /** */ + roomNumber("0.9.2342.19200300.100.1.6", "RFC 4524"), + /** */ + photo("0.9.2342.19200300.100.1.7", "RFC 2798"), + /** */ + userClass("0.9.2342.19200300.100.1.8", "RFC 4524"), + /** */ + host("0.9.2342.19200300.100.1.9", "RFC 4524"), + /** */ + manager("0.9.2342.19200300.100.1.10", "RFC 4524"), + /** */ + documentIdentifier("0.9.2342.19200300.100.1.11", "RFC 4524"), + /** */ + documentTitle("0.9.2342.19200300.100.1.12", "RFC 4524"), + /** */ + documentVersion("0.9.2342.19200300.100.1.13", "RFC 4524"), + /** */ + documentAuthor("0.9.2342.19200300.100.1.14", "RFC 4524"), + /** */ + documentLocation("0.9.2342.19200300.100.1.15", "RFC 4524"), + /** */ + homePhone("0.9.2342.19200300.100.1.20", "RFC 4524"), + /** */ + secretary("0.9.2342.19200300.100.1.21", "RFC 4524"), + /** */ + dc("0.9.2342.19200300.100.1.25", "RFC 4519"), + /** */ + associatedDomain("0.9.2342.19200300.100.1.37", "RFC 4524"), + /** */ + associatedName("0.9.2342.19200300.100.1.38", "RFC 4524"), + /** */ + homePostalAddress("0.9.2342.19200300.100.1.39", "RFC 4524"), + /** */ + personalTitle("0.9.2342.19200300.100.1.40", "RFC 4524"), + /** */ + mobile("0.9.2342.19200300.100.1.41", "RFC 4524"), + /** */ + pager("0.9.2342.19200300.100.1.42", "RFC 4524"), + /** */ + co("0.9.2342.19200300.100.1.43", "RFC 4524"), + /** */ + uniqueIdentifier("0.9.2342.19200300.100.1.44", "RFC 4524"), + /** */ + organizationalStatus("0.9.2342.19200300.100.1.45", "RFC 4524"), + /** */ + buildingName("0.9.2342.19200300.100.1.48", "RFC 4524"), + /** */ + audio("0.9.2342.19200300.100.1.55", "RFC 2798"), + /** */ + documentPublisher("0.9.2342.19200300.100.1.56", "RFC 4524"), + /** */ + jpegPhoto("0.9.2342.19200300.100.1.60", "RFC 2798"), + /** */ + vendorName("1.3.6.1.1.4", "RFC 3045"), + /** */ + vendorVersion("1.3.6.1.1.5", "RFC 3045"), + /** */ + entryUUID("1.3.6.1.1.16.4", "RFC 4530"), + /** */ + entryDN("1.3.6.1.1.20", "RFC 5020"), + /** */ + labeledURI("1.3.6.1.4.1.250.1.57", "RFC 2798"), + /** */ + numSubordinates("1.3.6.1.4.1.453.16.2.103", "draft-ietf-boreham-numsubordinates"), + /** */ + namingContexts("1.3.6.1.4.1.1466.101.120.5", "RFC 4512"), + /** */ + altServer("1.3.6.1.4.1.1466.101.120.6", "RFC 4512"), + /** */ + supportedExtension("1.3.6.1.4.1.1466.101.120.7", "RFC 4512"), + /** */ + supportedControl("1.3.6.1.4.1.1466.101.120.13", "RFC 4512"), + /** */ + supportedSASLMechanisms("1.3.6.1.4.1.1466.101.120.14", "RFC 4512"), + /** */ + supportedLDAPVersion("1.3.6.1.4.1.1466.101.120.15", "RFC 4512"), + /** */ + ldapSyntaxes("1.3.6.1.4.1.1466.101.120.16", "RFC 4512"), + /** */ + supportedAuthPasswordSchemes("1.3.6.1.4.1.4203.1.3.3", "RFC 3112"), + /** */ + authPassword("1.3.6.1.4.1.4203.1.3.4", "RFC 3112"), + /** */ + supportedFeatures("1.3.6.1.4.1.4203.1.3.5", "RFC 4512"), + /** */ + inheritable("1.3.6.1.4.1.7628.5.4.1", "draft-ietf-ldup-subentry"), + /** */ + blockInheritance("1.3.6.1.4.1.7628.5.4.2", "draft-ietf-ldup-subentry"), + /** */ + objectClass("2.5.4.0", "RFC 4512"), + /** */ + aliasedObjectName("2.5.4.1", "RFC 4512"), + /** */ + cn("2.5.4.3", "RFC 4519"), + /** */ + sn("2.5.4.4", "RFC 4519"), + /** */ + serialNumber("2.5.4.5", "RFC 4519"), + /** */ + c("2.5.4.6", "RFC 4519"), + /** */ + l("2.5.4.7", "RFC 4519"), + /** */ + st("2.5.4.8", "RFC 4519"), + /** */ + street("2.5.4.9", "RFC 4519"), + /** */ + o("2.5.4.10", "RFC 4519"), + /** */ + ou("2.5.4.11", "RFC 4519"), + /** */ + title("2.5.4.12", "RFC 4519"), + /** */ + description("2.5.4.13", "RFC 4519"), + /** */ + searchGuide("2.5.4.14", "RFC 4519"), + /** */ + businessCategory("2.5.4.15", "RFC 4519"), + /** */ + postalAddress("2.5.4.16", "RFC 4519"), + /** */ + postalCode("2.5.4.17", "RFC 4519"), + /** */ + postOfficeBox("2.5.4.18", "RFC 4519"), + /** */ + physicalDeliveryOfficeName("2.5.4.19", "RFC 4519"), + /** */ + telephoneNumber("2.5.4.20", "RFC 4519"), + /** */ + telexNumber("2.5.4.21", "RFC 4519"), + /** */ + teletexTerminalIdentifier("2.5.4.22", "RFC 4519"), + /** */ + facsimileTelephoneNumber("2.5.4.23", "RFC 4519"), + /** */ + x121Address("2.5.4.24", "RFC 4519"), + /** */ + internationalISDNNumber("2.5.4.25", "RFC 4519"), + /** */ + registeredAddress("2.5.4.26", "RFC 4519"), + /** */ + destinationIndicator("2.5.4.27", "RFC 4519"), + /** */ + preferredDeliveryMethod("2.5.4.28", "RFC 4519"), + /** */ + member("2.5.4.31", "RFC 4519"), + /** */ + owner("2.5.4.32", "RFC 4519"), + /** */ + roleOccupant("2.5.4.33", "RFC 4519"), + /** */ + seeAlso("2.5.4.34", "RFC 4519"), + /** */ + userPassword("2.5.4.35", "RFC 4519"), + /** */ + userCertificate("2.5.4.36", "RFC 4523"), + /** */ + cACertificate("2.5.4.37", "RFC 4523"), + /** */ + authorityRevocationList("2.5.4.38", "RFC 4523"), + /** */ + certificateRevocationList("2.5.4.39", "RFC 4523"), + /** */ + crossCertificatePair("2.5.4.40", "RFC 4523"), + /** */ + name("2.5.4.41", "RFC 4519"), + /** */ + givenName("2.5.4.42", "RFC 4519"), + /** */ + initials("2.5.4.43", "RFC 4519"), + /** */ + generationQualifier("2.5.4.44", "RFC 4519"), + /** */ + x500UniqueIdentifier("2.5.4.45", "RFC 4519"), + /** */ + dnQualifier("2.5.4.46", "RFC 4519"), + /** */ + enhancedSearchGuide("2.5.4.47", "RFC 4519"), + /** */ + distinguishedName("2.5.4.49", "RFC 4519"), + /** */ + uniqueMember("2.5.4.50", "RFC 4519"), + /** */ + houseIdentifier("2.5.4.51", "RFC 4519"), + /** */ + supportedAlgorithms("2.5.4.52", "RFC 4523"), + /** */ + deltaRevocationList("2.5.4.53", "RFC 4523"), + /** */ + createTimestamp("2.5.18.1", "RFC 4512"), + /** */ + modifyTimestamp("2.5.18.2", "RFC 4512"), + /** */ + creatorsName("2.5.18.3", "RFC 4512"), + /** */ + modifiersName("2.5.18.4", "RFC 4512"), + /** */ + subschemaSubentry("2.5.18.10", "RFC 4512"), + /** */ + dITStructureRules("2.5.21.1", "RFC 4512"), + /** */ + dITContentRules("2.5.21.2", "RFC 4512"), + /** */ + matchingRules("2.5.21.4", "RFC 4512"), + /** */ + attributeTypes("2.5.21.5", "RFC 4512"), + /** */ + objectClasses("2.5.21.6", "RFC 4512"), + /** */ + nameForms("2.5.21.7", "RFC 4512"), + /** */ + matchingRuleUse("2.5.21.8", "RFC 4512"), + /** */ + structuralObjectClass("2.5.21.9", "RFC 4512"), + /** */ + governingStructureRule("2.5.21.10", "RFC 4512"), + /** */ + carLicense("2.16.840.1.113730.3.1.1", "RFC 2798"), + /** */ + departmentNumber("2.16.840.1.113730.3.1.2", "RFC 2798"), + /** */ + employeeNumber("2.16.840.1.113730.3.1.3", "RFC 2798"), + /** */ + employeeType("2.16.840.1.113730.3.1.4", "RFC 2798"), + /** */ + changeNumber("2.16.840.1.113730.3.1.5", "draft-good-ldap-changelog"), + /** */ + targetDN("2.16.840.1.113730.3.1.6", "draft-good-ldap-changelog"), + /** */ + changeType("2.16.840.1.113730.3.1.7", "draft-good-ldap-changelog"), + /** */ + changes("2.16.840.1.113730.3.1.8", "draft-good-ldap-changelog"), + /** */ + newRDN("2.16.840.1.113730.3.1.9", "draft-good-ldap-changelog"), + /** */ + deleteOldRDN("2.16.840.1.113730.3.1.10", "draft-good-ldap-changelog"), + /** */ + newSuperior("2.16.840.1.113730.3.1.11", "draft-good-ldap-changelog"), + /** */ + ref("2.16.840.1.113730.3.1.34", "RFC 3296"), + /** */ + changelog("2.16.840.1.113730.3.1.35", "draft-good-ldap-changelog"), + /** */ + preferredLanguage("2.16.840.1.113730.3.1.39", "RFC 2798"), + /** */ + userSMIMECertificate("2.16.840.1.113730.3.1.40", "RFC 2798"), + /** */ + userPKCS12("2.16.840.1.113730.3.1.216", "RFC 2798"), + /** */ + displayName("2.16.840.1.113730.3.1.241", "RFC 2798"), + + // Sun memberOf + memberOf("1.2.840.113556.1.2.102", "389 DS memberOf"), + + // KERBEROS (partial) + krbPrincipalName("2.16.840.1.113719.1.301.6.8.1", "Novell Kerberos Schema Definitions"), + + // RFC 2985 and RFC 3039 (partial) + dateOfBirth("1.3.6.1.5.5.7.9.1", "RFC 2985"), + /** */ + placeOfBirth("1.3.6.1.5.5.7.9.2", "RFC 2985"), + /** */ + gender("1.3.6.1.5.5.7.9.3", "RFC 2985"), + /** */ + countryOfCitizenship("1.3.6.1.5.5.7.9.4", "RFC 2985"), + /** */ + countryOfResidence("1.3.6.1.5.5.7.9.5", "RFC 2985"), + + // RFC 2307bis (partial) + /** */ + uidNumber("1.3.6.1.1.1.1.0", "RFC 2307bis"), + /** */ + gidNumber("1.3.6.1.1.1.1.1", "RFC 2307bis"), + /** */ + homeDirectory("1.3.6.1.1.1.1.3", "RFC 2307bis"), + /** */ + loginShell("1.3.6.1.1.1.1.4", "RFC 2307bis"), + /** */ + memberUid("1.3.6.1.1.1.1.12", "RFC 2307bis"), + + // + ; + + public final static String DN = "dn"; + + private final String oid, spec; + private final QName value; + + LdapAttrs(String oid, String spec) { + this.oid = oid; + this.spec = spec; + this.value = new ContentName(LDAP_NAMESPACE_URI, name()); + } + + public QName qName() { + return value; + } + + @Override + public String getID() { + return oid; + } + + @Override + public String getSpec() { + return spec; + } + + @Deprecated + public String property() { + return get(); + } + + @Deprecated + public String qualified() { + return get(); + } + + @Override + public String get() { + return RuntimeNamespaceContext.getNamespaceContext().getPrefix(LDAP_NAMESPACE_URI) + ":" + name(); + } + + @Override + public final String toString() { + // must return the name + return name(); + } + + @Override + public String getNamespace() { + return LDAP_NAMESPACE_URI; + } + + @Override + public String getDefaultPrefix() { + return LDAP_DEFAULT_PREFIX; + } + +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapObjs.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapObjs.java new file mode 100644 index 000000000..45c8a5428 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapObjs.java @@ -0,0 +1,156 @@ +package org.argeo.api.acr.ldap; + +import static org.argeo.api.acr.ArgeoNamespace.LDAP_DEFAULT_PREFIX; +import static org.argeo.api.acr.ArgeoNamespace.LDAP_NAMESPACE_URI; + +import java.util.function.Supplier; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.ArgeoNamespace; +import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.QNamed; +import org.argeo.api.acr.RuntimeNamespaceContext; + +/** + * Standard LDAP object classes as per + * https://www.ldap.com/ldap- + * oid-reference + */ +public enum LdapObjs implements QNamed, SpecifiedName, Supplier { + account("0.9.2342.19200300.100.4.5", "RFC 4524"), + /** */ + document("0.9.2342.19200300.100.4.6", "RFC 4524"), + /** */ + room("0.9.2342.19200300.100.4.7", "RFC 4524"), + /** */ + documentSeries("0.9.2342.19200300.100.4.9", "RFC 4524"), + /** */ + domain("0.9.2342.19200300.100.4.13", "RFC 4524"), + /** */ + rFC822localPart("0.9.2342.19200300.100.4.14", "RFC 4524"), + /** */ + domainRelatedObject("0.9.2342.19200300.100.4.17", "RFC 4524"), + /** */ + friendlyCountry("0.9.2342.19200300.100.4.18", "RFC 4524"), + /** */ + simpleSecurityObject("0.9.2342.19200300.100.4.19", "RFC 4524"), + /** */ + uidObject("1.3.6.1.1.3.1", "RFC 4519"), + /** */ + extensibleObject("1.3.6.1.4.1.1466.101.120.111", "RFC 4512"), + /** */ + dcObject("1.3.6.1.4.1.1466.344", "RFC 4519"), + /** */ + authPasswordObject("1.3.6.1.4.1.4203.1.4.7", "RFC 3112"), + /** */ + namedObject("1.3.6.1.4.1.5322.13.1.1", "draft-howard-namedobject"), + /** */ + inheritableLDAPSubEntry("1.3.6.1.4.1.7628.5.6.1.1", "draft-ietf-ldup-subentry"), + /** */ + top("2.5.6.0", "RFC 4512"), + /** */ + alias("2.5.6.1", "RFC 4512"), + /** */ + country("2.5.6.2", "RFC 4519"), + /** */ + locality("2.5.6.3", "RFC 4519"), + /** */ + organization("2.5.6.4", "RFC 4519"), + /** */ + organizationalUnit("2.5.6.5", "RFC 4519"), + /** */ + person("2.5.6.6", "RFC 4519"), + /** */ + organizationalPerson("2.5.6.7", "RFC 4519"), + /** */ + organizationalRole("2.5.6.8", "RFC 4519"), + /** */ + groupOfNames("2.5.6.9", "RFC 4519"), + /** */ + residentialPerson("2.5.6.10", "RFC 4519"), + /** */ + applicationProcess("2.5.6.11", "RFC 4519"), + /** */ + device("2.5.6.14", "RFC 4519"), + /** */ + strongAuthenticationUser("2.5.6.15", "RFC 4523"), + /** */ + certificationAuthority("2.5.6.16", "RFC 4523"), + // /** Should be certificationAuthority-V2 */ + // certificationAuthority_V2("2.5.6.16.2", "RFC 4523") { + // }, + /** */ + groupOfUniqueNames("2.5.6.17", "RFC 4519"), + /** */ + userSecurityInformation("2.5.6.18", "RFC 4523"), + /** */ + cRLDistributionPoint("2.5.6.19", "RFC 4523"), + /** */ + pkiUser("2.5.6.21", "RFC 4523"), + /** */ + pkiCA("2.5.6.22", "RFC 4523"), + /** */ + deltaCRL("2.5.6.23", "RFC 4523"), + /** */ + subschema("2.5.20.1", "RFC 4512"), + /** */ + ldapSubEntry("2.16.840.1.113719.2.142.6.1.1", "draft-ietf-ldup-subentry"), + /** */ + changeLogEntry("2.16.840.1.113730.3.2.1", "draft-good-ldap-changelog"), + /** */ + inetOrgPerson("2.16.840.1.113730.3.2.2", "RFC 2798"), + /** */ + referral("2.16.840.1.113730.3.2.6", "RFC 3296"), + + // RFC 2307bis (partial) + /** */ + posixAccount("1.3.6.1.1.1.2.0", "RFC 2307bis"), + /** */ + posixGroup("1.3.6.1.1.1.2.2", "RFC 2307bis"), + + // + ; + + private final String oid, spec; + private final QName value; + + private LdapObjs(String oid, String spec) { + this.oid = oid; + this.spec = spec; + this.value = new ContentName(ArgeoNamespace.LDAP_NAMESPACE_URI, name()); + } + + public QName qName() { + return value; + } + + public String getOid() { + return oid; + } + + public String getSpec() { + return spec; + } + + @Deprecated + public String property() { + return get(); + } + + @Override + public String get() { + return RuntimeNamespaceContext.getNamespaceContext().getPrefix(LDAP_NAMESPACE_URI) + ":" + name(); + } + + @Override + public String getNamespace() { + return LDAP_NAMESPACE_URI; + } + + @Override + public String getDefaultPrefix() { + return LDAP_DEFAULT_PREFIX; + } + +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NamingUtils.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NamingUtils.java new file mode 100644 index 000000000..9f7ec61d8 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NamingUtils.java @@ -0,0 +1,106 @@ +package org.argeo.api.acr.ldap; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class NamingUtils { + /** As per https://tools.ietf.org/html/rfc4517#section-3.3.13 */ + private final static DateTimeFormatter utcLdapDate = DateTimeFormatter.ofPattern("uuuuMMddHHmmssX") + .withZone(ZoneOffset.UTC); + + /** @return null if not parseable */ + public static Instant ldapDateToInstant(String ldapDate) { + try { + return OffsetDateTime.parse(ldapDate, utcLdapDate).toInstant(); + } catch (DateTimeParseException e) { + return null; + } + } + + /** @return null if not parseable */ + public static ZonedDateTime ldapDateToZonedDateTime(String ldapDate) { + try { + return OffsetDateTime.parse(ldapDate, utcLdapDate).toZonedDateTime(); + } catch (DateTimeParseException e) { + return null; + } + } + + public static Calendar ldapDateToCalendar(String ldapDate) { + OffsetDateTime instant = OffsetDateTime.parse(ldapDate, utcLdapDate); + GregorianCalendar calendar = new GregorianCalendar(); + calendar.set(Calendar.DAY_OF_MONTH, instant.get(ChronoField.DAY_OF_MONTH)); + calendar.set(Calendar.MONTH, instant.get(ChronoField.MONTH_OF_YEAR)); + calendar.set(Calendar.YEAR, instant.get(ChronoField.YEAR)); + return calendar; + } + + public static String instantToLdapDate(ZonedDateTime instant) { + return utcLdapDate.format(instant.withZoneSameInstant(ZoneOffset.UTC)); + } + + public static String getQueryValue(Map> query, String key) { + if (!query.containsKey(key)) + return null; + List val = query.get(key); + if (val.size() == 1) + return val.get(0); + else + throw new IllegalArgumentException("There are " + val.size() + " value(s) for " + key); + } + + public static Map> queryToMap(URI uri) { + return queryToMap(uri.getQuery()); + } + + private static Map> queryToMap(String queryPart) { + try { + final Map> query_pairs = new LinkedHashMap>(); + if (queryPart == null) + return query_pairs; + final String[] pairs = queryPart.split("&"); + for (String pair : pairs) { + final int idx = pair.indexOf("="); + final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name()) + : pair; + if (!query_pairs.containsKey(key)) { + query_pairs.put(key, new LinkedList()); + } + final String value = idx > 0 && pair.length() > idx + 1 + ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name()) + : null; + query_pairs.get(key).add(value); + } + return query_pairs; + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Cannot convert " + queryPart + " to map", e); + } + } + + private NamingUtils() { + + } + + public static void main(String args[]) { + ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC); + String str = utcLdapDate.format(now); + System.out.println(str); + utcLdapDate.parse(str); + utcLdapDate.parse("19520512000000Z"); + } +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NodeOID.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NodeOID.java new file mode 100644 index 000000000..a68b6cb8b --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NodeOID.java @@ -0,0 +1,17 @@ +package org.argeo.api.acr.ldap; + +interface NodeOID { + String BASE = "1.3.6.1.4.1" + ".48308" + ".1"; + + // uuidgen --md5 --namespace @oid --name 1.3.6.1.4.1.48308 + String BASE_UUID_V3 = "6869e86b-96b7-3d49-b6ab-ffffc5ad95fb"; + + // uuidgen --sha1 --namespace @oid --name 1.3.6.1.4.1.48308 + String BASE_UUID_V5 = "58873947-460c-59a6-a7b4-28a97def5f27"; + + // ATTRIBUTE TYPES + String ATTRIBUTE_TYPES = BASE + ".4"; + + // OBJECT CLASSES + String OBJECT_CLASSES = BASE + ".6"; +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/SpecifiedName.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/SpecifiedName.java new file mode 100644 index 000000000..5de8ab253 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/SpecifiedName.java @@ -0,0 +1,20 @@ +package org.argeo.api.acr.ldap; + +/** + * A name which has been specified and for which an id has been defined + * (typically an OID). + */ +public interface SpecifiedName { + /** The name */ + String name(); + + /** An RFC or the URLof some specification */ + default String getSpec() { + return null; + } + + /** Typically an OID */ + default String getID() { + return getClass().getName() + "." + name(); + } +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/ArrayTabularRow.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/ArrayTabularRow.java new file mode 100644 index 000000000..c0efe87ff --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/ArrayTabularRow.java @@ -0,0 +1,25 @@ +package org.argeo.api.acr.tabular; + +import java.util.List; + +/** Minimal tabular row wrapping an {@link Object} array */ +public class ArrayTabularRow implements TabularRow { + private final Object[] arr; + + public ArrayTabularRow(List objs) { + this.arr = objs.toArray(); + } + + public Object get(Integer col) { + return arr[col]; + } + + public int size() { + return arr.length; + } + + public Object[] toArray() { + return arr; + } + +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularColumn.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularColumn.java new file mode 100644 index 000000000..5b9bf239a --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularColumn.java @@ -0,0 +1,41 @@ +package org.argeo.api.acr.tabular; + +/** The column in a tabular content */ +public class TabularColumn { + private String name; + /** + * JCR types, see + * http://www.day.com/maven/javax.jcr/javadocs/jcr-2.0/index.html + * ?javax/jcr/PropertyType.html + */ + private Integer type; + + /** column with default type */ + public TabularColumn(String name) { + super(); + this.name = name; + } + + public TabularColumn(String name, Integer type) { + super(); + this.name = name; + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getType() { + return type; + } + + public void setType(Integer type) { + this.type = type; + } + +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularContent.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularContent.java new file mode 100644 index 000000000..ae6eb4e2a --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularContent.java @@ -0,0 +1,14 @@ +package org.argeo.api.acr.tabular; + +import java.util.List; + +/** + * Content organized as a table, possibly with headers. Only JCR types are + * supported even though there is not direct dependency on JCR. + */ +public interface TabularContent { + /** The headers of this table or null is none available. */ + public List getColumns(); + + public TabularRowIterator read(); +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRow.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRow.java new file mode 100644 index 000000000..5302cc04f --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRow.java @@ -0,0 +1,13 @@ +package org.argeo.api.acr.tabular; + +/** A row of tabular data */ +public interface TabularRow { + /** The value at this column index */ + public Object get(Integer col); + + /** The raw objects (direct references) */ + public Object[] toArray(); + + /** Number of columns */ + public int size(); +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRowIterator.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRowIterator.java new file mode 100644 index 000000000..768c593ad --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRowIterator.java @@ -0,0 +1,12 @@ +package org.argeo.api.acr.tabular; + +import java.util.Iterator; + +/** Navigation of rows */ +public interface TabularRowIterator extends Iterator { + /** + * Current row number, has to be incremented by each call to next() ; starts at 0, will + * therefore be 1 for the first row returned. + */ + public Long getCurrentRowNumber(); +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularWriter.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularWriter.java new file mode 100644 index 000000000..f1d555f6c --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularWriter.java @@ -0,0 +1,11 @@ +package org.argeo.api.acr.tabular; + + +/** Write to a tabular content */ +public interface TabularWriter { + /** Append a new row of data */ + public void appendRow(Object[] row); + + /** Finish persisting data and release resources */ + public void close(); +} diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/package-info.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/package-info.java new file mode 100644 index 000000000..06acbc587 --- /dev/null +++ b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/package-info.java @@ -0,0 +1,2 @@ +/** Tabular format API. */ +package org.argeo.api.acr.tabular; \ No newline at end of file diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/directory/Directory.java b/org.argeo.api.cms/src/org/argeo/api/cms/directory/Directory.java new file mode 100644 index 000000000..7ed61ebc6 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/directory/Directory.java @@ -0,0 +1,32 @@ +package org.argeo.api.cms.directory; + +import java.util.Optional; + +import org.argeo.api.cms.transaction.WorkControl; + +/** An information directory (typicylly LDAP). */ +public interface Directory extends HierarchyUnit { + String getName(); + + /** Whether this directory is read only. */ + boolean isReadOnly(); + + /** Whether this directory is disabled. */ + boolean isDisabled(); + + /** The realm (typically Kerberos) of this directory. */ + Optional getRealm(); + + /** Sets the transaction control used by this directory when editing. */ + void setTransactionControl(WorkControl transactionControl); + + /* + * HIERARCHY + */ + + /** The hierarchy unit at this path. */ + HierarchyUnit getHierarchyUnit(String path); + + /** Create a new hierarchy unit. */ + HierarchyUnit createHierarchyUnit(String path); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/directory/DirectoryDigestUtils.java b/org.argeo.api.cms/src/org/argeo/api/cms/directory/DirectoryDigestUtils.java new file mode 100644 index 000000000..dabcfe8ee --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/directory/DirectoryDigestUtils.java @@ -0,0 +1,114 @@ +package org.argeo.api.cms.directory; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Arrays; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +/** Utilities around digests, mostly those related to passwords. */ +public class DirectoryDigestUtils { + public final static String PASSWORD_SCHEME_SHA = "SHA"; + public final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256"; + + public static byte[] sha1(byte[] bytes) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + digest.update(bytes); + byte[] checksum = digest.digest(); + return checksum; + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Cannot SHA1 digest", e); + } + } + + public static byte[] toPasswordScheme(String passwordScheme, char[] password, byte[] salt, Integer iterations, + Integer keyLength) { + try { + if (PASSWORD_SCHEME_SHA.equals(passwordScheme)) { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + byte[] bytes = charsToBytes(password); + digest.update(bytes); + return digest.digest(); + } else if (PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { + KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength); + + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + final int ITERATION_LENGTH = 4; + byte[] key = f.generateSecret(spec).getEncoded(); + byte[] result = new byte[ITERATION_LENGTH + salt.length + key.length]; + byte iterationsArr[] = new BigInteger(iterations.toString()).toByteArray(); + if (iterationsArr.length < ITERATION_LENGTH) { + Arrays.fill(result, 0, ITERATION_LENGTH - iterationsArr.length, (byte) 0); + System.arraycopy(iterationsArr, 0, result, ITERATION_LENGTH - iterationsArr.length, + iterationsArr.length); + } else { + System.arraycopy(iterationsArr, 0, result, 0, ITERATION_LENGTH); + } + System.arraycopy(salt, 0, result, ITERATION_LENGTH, salt.length); + System.arraycopy(key, 0, result, ITERATION_LENGTH + salt.length, key.length); + return result; + } else { + throw new UnsupportedOperationException("Unkown password scheme " + passwordScheme); + } + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalStateException("Cannot digest", e); + } + } + + public static char[] bytesToChars(Object obj) { + if (obj instanceof char[]) + return (char[]) obj; + if (!(obj instanceof byte[])) + throw new IllegalArgumentException(obj.getClass() + " is not a byte array"); + ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj); + CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer); + char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit()); + // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data + // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data + // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data + return res; + } + + public static byte[] charsToBytes(char[] chars) { + CharBuffer charBuffer = CharBuffer.wrap(chars); + ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); + byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); + // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data + // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data + return bytes; + } + + public static String sha1str(String str) { + byte[] hash = sha1(str.getBytes(StandardCharsets.UTF_8)); + return encodeHexString(hash); + } + + final private static char[] hexArray = "0123456789abcdef".toCharArray(); + + /** + * From + * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to + * -a-hex-string-in-java + */ + public static String encodeHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + /** singleton */ + private DirectoryDigestUtils() { + } +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/directory/HierarchyUnit.java b/org.argeo.api.cms/src/org/argeo/api/cms/directory/HierarchyUnit.java new file mode 100644 index 000000000..04593d94e --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/directory/HierarchyUnit.java @@ -0,0 +1,42 @@ +package org.argeo.api.cms.directory; + +import java.util.Dictionary; +import java.util.Locale; + +/** A unit within the high-level organisational structure of a directory. */ +public interface HierarchyUnit { + /** Name to use in paths. */ + String getHierarchyUnitName(); + + /** Name to use in UI. */ + String getHierarchyUnitLabel(Locale locale); + + /** + * The parent {@link HierarchyUnit}, or null if a + * {@link Directory}. + */ + HierarchyUnit getParent(); + + /** Direct children {@link HierarchyUnit}s. */ + Iterable getDirectHierarchyUnits(boolean functionalOnly); + + /** + * Whether this is an arbitrary named and placed {@link HierarchyUnit}. + * + * @return true if functional, false is technical + * (e.g. People, Groups, etc.) + */ + boolean isFunctional(); + + /** + * The base of this organisational unit within the hierarchy. This would + * typically be an LDAP base DN. + */ + String getBase(); + + /** The related {@link Directory}. */ + Directory getDirectory(); + + /** Its metadata (typically LDAP attributes). */ + Dictionary getProperties(); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/keyring/CryptoKeyring.java b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/CryptoKeyring.java new file mode 100644 index 000000000..454f8b4a9 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/CryptoKeyring.java @@ -0,0 +1,10 @@ +package org.argeo.api.cms.keyring; + +/** + * Marker interface for an advanced keyring based on cryptography. + */ +public interface CryptoKeyring extends Keyring { + public void changePassword(char[] oldPassword, char[] newPassword); + + public void unlock(char[] password); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/keyring/Keyring.java b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/Keyring.java new file mode 100644 index 000000000..efc9455eb --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/Keyring.java @@ -0,0 +1,26 @@ +package org.argeo.api.cms.keyring; + +import java.io.InputStream; + +/** + * Access to private (typically encrypted) data. The keyring is responsible for + * retrieving the necessary credentials. Experimental. This API may + * change. + */ +public interface Keyring { + /** + * Returns the confidential information as chars. Must ask for it if it is + * not stored. + */ + public char[] getAsChars(String path); + + /** + * Returns the confidential information as a stream. Must ask for it if it + * is not stored. + */ + public InputStream getAsStream(String path); + + public void set(String path, char[] arr); + + public void set(String path, InputStream in); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/keyring/PBEKeySpecCallback.java b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/PBEKeySpecCallback.java new file mode 100644 index 000000000..6a7ce19fb --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/PBEKeySpecCallback.java @@ -0,0 +1,63 @@ +package org.argeo.api.cms.keyring; + +import javax.crypto.spec.PBEKeySpec; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.PasswordCallback; + +/** + * All information required to set up a {@link PBEKeySpec} bar the password + * itself (use a {@link PasswordCallback}) + */ +public class PBEKeySpecCallback implements Callback { + private String secretKeyFactory; + private byte[] salt; + private Integer iterationCount; + /** Can be null for some algorithms */ + private Integer keyLength; + /** Can be null, will trigger secret key encryption if not */ + private String secretKeyEncryption; + + private String encryptedPasswordHashCipher; + private byte[] encryptedPasswordHash; + + public void set(String secretKeyFactory, byte[] salt, + Integer iterationCount, Integer keyLength, + String secretKeyEncryption) { + this.secretKeyFactory = secretKeyFactory; + this.salt = salt; + this.iterationCount = iterationCount; + this.keyLength = keyLength; + this.secretKeyEncryption = secretKeyEncryption; +// this.encryptedPasswordHashCipher = encryptedPasswordHashCipher; +// this.encryptedPasswordHash = encryptedPasswordHash; + } + + public String getSecretKeyFactory() { + return secretKeyFactory; + } + + public byte[] getSalt() { + return salt; + } + + public Integer getIterationCount() { + return iterationCount; + } + + public Integer getKeyLength() { + return keyLength; + } + + public String getSecretKeyEncryption() { + return secretKeyEncryption; + } + + public String getEncryptedPasswordHashCipher() { + return encryptedPasswordHashCipher; + } + + public byte[] getEncryptedPasswordHash() { + return encryptedPasswordHash; + } + +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/keyring/package-info.java b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/package-info.java new file mode 100644 index 000000000..7f61e09b0 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS reusable security components. */ +package org.argeo.api.cms.keyring; \ No newline at end of file diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/AbstractWorkingCopy.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/AbstractWorkingCopy.java new file mode 100644 index 000000000..928acad2c --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/AbstractWorkingCopy.java @@ -0,0 +1,48 @@ +package org.argeo.api.cms.transaction; + +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractWorkingCopy implements WorkingCopy { + private Map newData = new HashMap(); + private Map modifiedData = new HashMap(); + private Map deletedData = new HashMap(); + + protected abstract ID getId(DATA data); + + protected abstract ATTR cloneAttributes(DATA data); + + public void cleanUp() { + // clean collections + newData.clear(); + newData = null; + modifiedData.clear(); + modifiedData = null; + deletedData.clear(); + deletedData = null; + } + + public boolean noModifications() { + return newData.size() == 0 && modifiedData.size() == 0 && deletedData.size() == 0; + } + + public void startEditing(DATA user) { + ID id = getId(user); + if (modifiedData.containsKey(id)) + throw new IllegalStateException("Already editing " + id); + modifiedData.put(id, cloneAttributes(user)); + } + + public Map getNewData() { + return newData; + } + + public Map getDeletedData() { + return deletedData; + } + + public Map getModifiedData() { + return modifiedData; + } + +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/JtaStatusAdapter.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/JtaStatusAdapter.java new file mode 100644 index 000000000..2ba6c0dae --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/JtaStatusAdapter.java @@ -0,0 +1,61 @@ +package org.argeo.api.cms.transaction; + +/** JTA transaction status. */ +public class JtaStatusAdapter implements TransactionStatusAdapter { + private static final Integer STATUS_ACTIVE = 0; + private static final Integer STATUS_COMMITTED = 3; + private static final Integer STATUS_COMMITTING = 8; + private static final Integer STATUS_MARKED_ROLLBACK = 1; + private static final Integer STATUS_NO_TRANSACTION = 6; + private static final Integer STATUS_PREPARED = 2; + private static final Integer STATUS_PREPARING = 7; + private static final Integer STATUS_ROLLEDBACK = 4; + private static final Integer STATUS_ROLLING_BACK = 9; +// private static final Integer STATUS_UNKNOWN = 5; + + @Override + public Integer getActiveStatus() { + return STATUS_ACTIVE; + } + + @Override + public Integer getPreparingStatus() { + return STATUS_PREPARING; + } + + @Override + public Integer getMarkedRollbackStatus() { + return STATUS_MARKED_ROLLBACK; + } + + @Override + public Integer getPreparedStatus() { + return STATUS_PREPARED; + } + + @Override + public Integer getCommittingStatus() { + return STATUS_COMMITTING; + } + + @Override + public Integer getCommittedStatus() { + return STATUS_COMMITTED; + } + + @Override + public Integer getRollingBackStatus() { + return STATUS_ROLLING_BACK; + } + + @Override + public Integer getRolledBackStatus() { + return STATUS_ROLLEDBACK; + } + + @Override + public Integer getNoTransactionStatus() { + return STATUS_NO_TRANSACTION; + } + +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleRollbackException.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleRollbackException.java new file mode 100644 index 000000000..39ed9b9a0 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleRollbackException.java @@ -0,0 +1,15 @@ +package org.argeo.api.cms.transaction; + +/** Internal unchecked rollback exception. */ +class SimpleRollbackException extends RuntimeException { + private static final long serialVersionUID = 8055601819719780566L; + + public SimpleRollbackException() { + super(); + } + + public SimpleRollbackException(Throwable cause) { + super(cause); + } + +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransaction.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransaction.java new file mode 100644 index 000000000..f2bb9078c --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransaction.java @@ -0,0 +1,160 @@ +package org.argeo.api.cms.transaction; + +import java.util.ArrayList; +import java.util.List; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +/** Simple implementation of an XA transaction. */ +class SimpleTransaction +//implements Transaction, Status +{ + private final Xid xid; + private T status; + private final List xaResources = new ArrayList(); + + private final SimpleTransactionManager transactionManager; + private TransactionStatusAdapter tsa; + + public SimpleTransaction(SimpleTransactionManager transactionManager, TransactionStatusAdapter tsa) { + this.tsa = tsa; + this.status = tsa.getActiveStatus(); + this.xid = new UuidXid(); + this.transactionManager = transactionManager; + } + + public synchronized void commit() +// throws RollbackException, HeuristicMixedException, HeuristicRollbackException, +// SecurityException, IllegalStateException, SystemException + { + status = tsa.getPreparingStatus(); + for (XAResource xaRes : xaResources) { + if (status.equals(tsa.getMarkedRollbackStatus())) + break; + try { + xaRes.prepare(xid); + } catch (XAException e) { + status = tsa.getMarkedRollbackStatus(); + error("Cannot prepare " + xaRes + " for " + xid, e); + } + } + if (status.equals(tsa.getMarkedRollbackStatus())) { + rollback(); + throw new SimpleRollbackException(); + } + status = tsa.getPreparedStatus(); + + status = tsa.getCommittingStatus(); + for (XAResource xaRes : xaResources) { + if (status.equals(tsa.getMarkedRollbackStatus())) + break; + try { + xaRes.commit(xid, false); + } catch (XAException e) { + status = tsa.getMarkedRollbackStatus(); + error("Cannot prepare " + xaRes + " for " + xid, e); + } + } + if (status.equals(tsa.getMarkedRollbackStatus())) { + rollback(); + throw new SimpleRollbackException(); + } + + // complete + status = tsa.getCommittedStatus(); + clearResources(XAResource.TMSUCCESS); + transactionManager.unregister(xid); + } + + public synchronized void rollback() +// throws IllegalStateException, SystemException + { + status = tsa.getRollingBackStatus(); + for (XAResource xaRes : xaResources) { + try { + xaRes.rollback(xid); + } catch (XAException e) { + error("Cannot rollback " + xaRes + " for " + xid, e); + } + } + + // complete + status = tsa.getRolledBackStatus(); + clearResources(XAResource.TMFAIL); + transactionManager.unregister(xid); + } + + public synchronized boolean enlistResource(XAResource xaRes) +// throws RollbackException, IllegalStateException, SystemException + { + if (xaResources.add(xaRes)) { + try { + xaRes.start(getXid(), XAResource.TMNOFLAGS); + return true; + } catch (XAException e) { + error("Cannot enlist " + xaRes, e); + return false; + } + } else + return false; + } + + public synchronized boolean delistResource(XAResource xaRes, int flag) +// throws IllegalStateException, SystemException + { + if (xaResources.remove(xaRes)) { + try { + xaRes.end(getXid(), flag); + } catch (XAException e) { + error("Cannot delist " + xaRes, e); + return false; + } + return true; + } else + return false; + } + + protected void clearResources(int flag) { + for (XAResource xaRes : xaResources) + try { + xaRes.end(getXid(), flag); + } catch (XAException e) { + error("Cannot end " + xaRes, e); + } + xaResources.clear(); + } + + protected void error(Object obj, Exception e) { + System.err.println(obj); + e.printStackTrace(); + } + + public synchronized T getStatus() +// throws SystemException + { + return status; + } + +// public void registerSynchronization(Synchronization sync) +// throws RollbackException, IllegalStateException, SystemException { +// throw new UnsupportedOperationException(); +// } + + public void setRollbackOnly() +// throws IllegalStateException, SystemException + { + status = tsa.getMarkedRollbackStatus(); + } + + @Override + public int hashCode() { + return xid.hashCode(); + } + + Xid getXid() { + return xid; + } + +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransactionManager.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransactionManager.java new file mode 100644 index 000000000..ee99ccb19 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransactionManager.java @@ -0,0 +1,214 @@ +package org.argeo.api.cms.transaction; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; + +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +/** + * Simple implementation of an XA transaction manager. + */ +public class SimpleTransactionManager +// implements TransactionManager, UserTransaction + implements WorkControl, WorkTransaction { + private ThreadLocal> current = new ThreadLocal>(); + + private Map> knownTransactions = Collections + .synchronizedMap(new HashMap>()); + private TransactionStatusAdapter tsa = new JtaStatusAdapter(); +// private SyncRegistry syncRegistry = new SyncRegistry(); + + /* + * WORK IMPLEMENTATION + */ + @Override + public T required(Callable work) { + T res; + begin(); + try { + res = work.call(); + commit(); + } catch (Exception e) { + rollback(); + throw new SimpleRollbackException(e); + } + return res; + } + + @Override + public WorkContext getWorkContext() { + return new WorkContext() { + + @Override + public void registerXAResource(XAResource resource, String recoveryId) { + getTransaction().enlistResource(resource); + } + }; + } + + /* + * WORK TRANSACTION IMPLEMENTATION + */ + + @Override + public boolean isNoTransactionStatus() { + return tsa.getNoTransactionStatus().equals(getStatus()); + } + + /* + * JTA IMPLEMENTATION + */ + + public void begin() +// throws NotSupportedException, SystemException + { + if (getCurrent() != null) + throw new UnsupportedOperationException("Nested transactions are not supported"); + SimpleTransaction transaction = new SimpleTransaction(this, tsa); + knownTransactions.put(transaction.getXid(), transaction); + current.set(transaction); + } + + public void commit() +// throws RollbackException, HeuristicMixedException, HeuristicRollbackException, +// SecurityException, IllegalStateException, SystemException + { + if (getCurrent() == null) + throw new IllegalStateException("No transaction registered with the current thread."); + getCurrent().commit(); + } + + public int getStatus() +// throws SystemException + { + if (getCurrent() == null) + return tsa.getNoTransactionStatus(); + return getTransaction().getStatus(); + } + + public SimpleTransaction getTransaction() +// throws SystemException + { + return getCurrent(); + } + + protected SimpleTransaction getCurrent() +// throws SystemException + { + SimpleTransaction transaction = current.get(); + if (transaction == null) + return null; + Integer status = transaction.getStatus(); + if (status.equals(tsa.getCommittedStatus()) || status.equals(tsa.getRolledBackStatus())) { + current.remove(); + return null; + } + return transaction; + } + + void unregister(Xid xid) { + knownTransactions.remove(xid); + } + + public void resume(SimpleTransaction tobj) +// throws InvalidTransactionException, IllegalStateException, SystemException + { + if (getCurrent() != null) + throw new IllegalStateException("Transaction " + current.get() + " already registered"); + current.set(tobj); + } + + public void rollback() +// throws IllegalStateException, SecurityException, SystemException + { + if (getCurrent() == null) + throw new IllegalStateException("No transaction registered with the current thread."); + getCurrent().rollback(); + } + + public void setRollbackOnly() +// throws IllegalStateException, SystemException + { + if (getCurrent() == null) + throw new IllegalStateException("No transaction registered with the current thread."); + getCurrent().setRollbackOnly(); + } + + public void setTransactionTimeout(int seconds) +// throws SystemException + { + throw new UnsupportedOperationException(); + } + + public SimpleTransaction suspend() +// throws SystemException + { + SimpleTransaction transaction = getCurrent(); + current.remove(); + return transaction; + } + +// public TransactionSynchronizationRegistry getTsr() { +// return syncRegistry; +// } +// +// private class SyncRegistry implements TransactionSynchronizationRegistry { +// @Override +// public Object getTransactionKey() { +// try { +// SimpleTransaction transaction = getCurrent(); +// if (transaction == null) +// return null; +// return getCurrent().getXid(); +// } catch (SystemException e) { +// throw new IllegalStateException("Cannot get transaction key", e); +// } +// } +// +// @Override +// public void putResource(Object key, Object value) { +// throw new UnsupportedOperationException(); +// } +// +// @Override +// public Object getResource(Object key) { +// throw new UnsupportedOperationException(); +// } +// +// @Override +// public void registerInterposedSynchronization(Synchronization sync) { +// throw new UnsupportedOperationException(); +// } +// +// @Override +// public int getTransactionStatus() { +// try { +// return getStatus(); +// } catch (SystemException e) { +// throw new IllegalStateException("Cannot get status", e); +// } +// } +// +// @Override +// public boolean getRollbackOnly() { +// try { +// return getStatus() == Status.STATUS_MARKED_ROLLBACK; +// } catch (SystemException e) { +// throw new IllegalStateException("Cannot get status", e); +// } +// } +// +// @Override +// public void setRollbackOnly() { +// try { +// getCurrent().setRollbackOnly(); +// } catch (Exception e) { +// throw new IllegalStateException("Cannot set rollback only", e); +// } +// } +// +// } +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/TransactionStatusAdapter.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/TransactionStatusAdapter.java new file mode 100644 index 000000000..ab4effd55 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/TransactionStatusAdapter.java @@ -0,0 +1,22 @@ +package org.argeo.api.cms.transaction; + +/** Abstract the various approaches to represent transaction status. */ +public interface TransactionStatusAdapter { + T getActiveStatus(); + + T getPreparingStatus(); + + T getMarkedRollbackStatus(); + + T getPreparedStatus(); + + T getCommittingStatus(); + + T getCommittedStatus(); + + T getRollingBackStatus(); + + T getRolledBackStatus(); + + T getNoTransactionStatus(); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/UuidXid.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/UuidXid.java new file mode 100644 index 000000000..83358a5ae --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/UuidXid.java @@ -0,0 +1,132 @@ +package org.argeo.api.cms.transaction; + +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.UUID; + +import javax.transaction.xa.Xid; + +/** + * Implementation of {@link Xid} based on {@link UUID}, using max significant + * bits as global transaction id, and least significant bits as branch + * qualifier. + */ +public class UuidXid implements Xid, Serializable { + private static final long serialVersionUID = -5380531989917886819L; + public final static int FORMAT = (int) serialVersionUID; + + private final static int BYTES_PER_LONG = Long.SIZE / Byte.SIZE; + + private final int format; + private final byte[] globalTransactionId; + private final byte[] branchQualifier; + private final String uuid; + private final int hashCode; + + public UuidXid() { + this(UUID.randomUUID()); + } + + public UuidXid(UUID uuid) { + this.format = FORMAT; + this.globalTransactionId = uuidToBytes(uuid.getMostSignificantBits()); + this.branchQualifier = uuidToBytes(uuid.getLeastSignificantBits()); + this.uuid = uuid.toString(); + this.hashCode = uuid.hashCode(); + } + + public UuidXid(Xid xid) { + this(xid.getFormatId(), xid.getGlobalTransactionId(), xid + .getBranchQualifier()); + } + + private UuidXid(int format, byte[] globalTransactionId, + byte[] branchQualifier) { + this.format = format; + this.globalTransactionId = globalTransactionId; + this.branchQualifier = branchQualifier; + this.uuid = bytesToUUID(globalTransactionId, branchQualifier) + .toString(); + this.hashCode = uuid.hashCode(); + } + + @Override + public int getFormatId() { + return format; + } + + @Override + public byte[] getGlobalTransactionId() { + return Arrays.copyOf(globalTransactionId, globalTransactionId.length); + } + + @Override + public byte[] getBranchQualifier() { + return Arrays.copyOf(branchQualifier, branchQualifier.length); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof UuidXid) { + UuidXid that = (UuidXid) obj; + return Arrays.equals(globalTransactionId, that.globalTransactionId) + && Arrays.equals(branchQualifier, that.branchQualifier); + } + if (obj instanceof Xid) { + Xid that = (Xid) obj; + return Arrays.equals(globalTransactionId, + that.getGlobalTransactionId()) + && Arrays + .equals(branchQualifier, that.getBranchQualifier()); + } + return uuid.equals(obj.toString()); + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return new UuidXid(format, globalTransactionId, branchQualifier); + } + + @Override + public String toString() { + return uuid; + } + + public UUID asUuid() { + return bytesToUUID(globalTransactionId, branchQualifier); + } + + public static byte[] uuidToBytes(long bits) { + ByteBuffer buffer = ByteBuffer.allocate(BYTES_PER_LONG); + buffer.putLong(0, bits); + return buffer.array(); + } + + public static UUID bytesToUUID(byte[] most, byte[] least) { + if (most.length < BYTES_PER_LONG) + most = Arrays.copyOf(most, BYTES_PER_LONG); + if (least.length < BYTES_PER_LONG) + least = Arrays.copyOf(least, BYTES_PER_LONG); + ByteBuffer buffer = ByteBuffer.allocate(2 * BYTES_PER_LONG); + buffer.put(most, 0, BYTES_PER_LONG); + buffer.put(least, 0, BYTES_PER_LONG); + buffer.flip(); + return new UUID(buffer.getLong(), buffer.getLong()); + } + + // public static void main(String[] args) { + // UUID uuid = UUID.randomUUID(); + // System.out.println(uuid); + // uuid = bytesToUUID(uuidToBytes(uuid.getMostSignificantBits()), + // uuidToBytes(uuid.getLeastSignificantBits())); + // System.out.println(uuid); + // } +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkContext.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkContext.java new file mode 100644 index 000000000..5493dde91 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkContext.java @@ -0,0 +1,11 @@ +package org.argeo.api.cms.transaction; + +import javax.transaction.xa.XAResource; + +/** + * A minimalistic interface similar to OSGi transaction context in order to + * register XA resources. + */ +public interface WorkContext { + void registerXAResource(XAResource resource, String recoveryId); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkControl.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkControl.java new file mode 100644 index 000000000..de03150c5 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkControl.java @@ -0,0 +1,15 @@ +package org.argeo.api.cms.transaction; + +import java.util.concurrent.Callable; + +/** + * A minimalistic interface inspired by OSGi transaction control in order to + * commit units of work externally. + */ +public interface WorkControl { + T required(Callable work); + + void setRollbackOnly(); + + WorkContext getWorkContext(); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkTransaction.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkTransaction.java new file mode 100644 index 000000000..39c188d54 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkTransaction.java @@ -0,0 +1,15 @@ +package org.argeo.api.cms.transaction; + +/** + * A minimalistic interface inspired by JTA user transaction in order to commit + * units of work externally. + */ +public interface WorkTransaction { + void begin(); + + void commit(); + + void rollback(); + + boolean isNoTransactionStatus(); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopy.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopy.java new file mode 100644 index 000000000..c79423c8c --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopy.java @@ -0,0 +1,18 @@ +package org.argeo.api.cms.transaction; + +import java.util.Map; + +public interface WorkingCopy { + void startEditing(DATA user); + + boolean noModifications(); + + void cleanUp(); + + Map getNewData(); + + Map getDeletedData(); + + Map getModifiedData(); + +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyProcessor.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyProcessor.java new file mode 100644 index 000000000..9e7c9e187 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyProcessor.java @@ -0,0 +1,11 @@ +package org.argeo.api.cms.transaction; + +public interface WorkingCopyProcessor> { + void prepare(WC wc); + + void commit(WC wc); + + void rollback(WC wc); + + WC newWorkingCopy(); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyXaResource.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyXaResource.java new file mode 100644 index 000000000..16b08c287 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyXaResource.java @@ -0,0 +1,138 @@ +package org.argeo.api.cms.transaction; + +import java.util.HashMap; +import java.util.Map; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +/** {@link XAResource} for a user directory being edited. */ +public class WorkingCopyXaResource> implements XAResource { + private final WorkingCopyProcessor processor; + + private Map workingCopies = new HashMap(); + private Xid editingXid = null; + private int transactionTimeout = 0; + + public WorkingCopyXaResource(WorkingCopyProcessor processor) { + this.processor = processor; + } + + @Override + public synchronized void start(Xid xid, int flags) throws XAException { + if (editingXid != null) + throw new IllegalStateException("Already editing " + editingXid); + WC wc = workingCopies.put(xid, processor.newWorkingCopy()); + if (wc != null) + throw new IllegalStateException("There is already a working copy for " + xid); + this.editingXid = xid; + } + + @Override + public void end(Xid xid, int flags) throws XAException { + checkXid(xid); + } + + private WC wc(Xid xid) { + return workingCopies.get(xid); + } + + public synchronized WC wc() { + if (editingXid == null) + return null; + WC wc = workingCopies.get(editingXid); + if (wc == null) + throw new IllegalStateException("No working copy found for " + editingXid); + return wc; + } + + private synchronized void cleanUp(Xid xid) { + WC wc = workingCopies.get(xid); + if (wc != null) { + wc.cleanUp(); + workingCopies.remove(xid); + } + editingXid = null; + } + + @Override + public int prepare(Xid xid) throws XAException { + checkXid(xid); + WC wc = wc(xid); + if (wc.noModifications()) + return XA_RDONLY; + try { + processor.prepare(wc); + } catch (Exception e) { + e.printStackTrace(); + throw new XAException(XAException.XAER_RMERR); + } + return XA_OK; + } + + @Override + public void commit(Xid xid, boolean onePhase) throws XAException { + try { + checkXid(xid); + WC wc = wc(xid); + if (wc.noModifications()) + return; + if (onePhase) + processor.prepare(wc); + processor.commit(wc); + } catch (Exception e) { + e.printStackTrace(); + throw new XAException(XAException.XAER_RMERR); + } finally { + cleanUp(xid); + } + } + + @Override + public void rollback(Xid xid) throws XAException { + try { + checkXid(xid); + processor.rollback(wc(xid)); + } catch (Exception e) { + e.printStackTrace(); + throw new XAException(XAException.XAER_RMERR); + } finally { + cleanUp(xid); + } + } + + @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 { + return new Xid[0]; + } + + @Override + public int getTransactionTimeout() throws XAException { + return transactionTimeout; + } + + @Override + public boolean setTransactionTimeout(int seconds) throws XAException { + transactionTimeout = seconds; + return true; + } + + private void checkXid(Xid xid) throws XAException { + if (xid == null) + throw new XAException(XAException.XAER_OUTSIDE); + if (!xid.equals(xid)) + throw new XAException(XAException.XAER_NOTA); + } + +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/XAResourceProvider.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/XAResourceProvider.java new file mode 100644 index 000000000..904fb5f48 --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/XAResourceProvider.java @@ -0,0 +1,7 @@ +package org.argeo.api.cms.transaction; + +import javax.transaction.xa.XAResource; + +public interface XAResourceProvider { + XAResource getXaResource(); +} diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/package-info.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/package-info.java new file mode 100644 index 000000000..bbb9212bc --- /dev/null +++ b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/package-info.java @@ -0,0 +1,2 @@ +/** Minimalistic and partial XA transaction manager implementation. */ +package org.argeo.api.cms.transaction; \ No newline at end of file diff --git a/org.argeo.api.register/.classpath b/org.argeo.api.register/.classpath new file mode 100644 index 000000000..4199cd3a3 --- /dev/null +++ b/org.argeo.api.register/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/org.argeo.api.register/.project b/org.argeo.api.register/.project new file mode 100644 index 000000000..a3164c111 --- /dev/null +++ b/org.argeo.api.register/.project @@ -0,0 +1,28 @@ + + + org.argeo.api.register + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.pde.PluginNature + + diff --git a/org.argeo.api.register/META-INF/.gitignore b/org.argeo.api.register/META-INF/.gitignore new file mode 100644 index 000000000..4854a41b9 --- /dev/null +++ b/org.argeo.api.register/META-INF/.gitignore @@ -0,0 +1 @@ +/MANIFEST.MF diff --git a/org.argeo.api.register/bnd.bnd b/org.argeo.api.register/bnd.bnd new file mode 100644 index 000000000..bcebf3722 --- /dev/null +++ b/org.argeo.api.register/bnd.bnd @@ -0,0 +1,3 @@ +Import-Package: org.osgi.*;version=0.0.0,\ +!org.apache.commons.logging,\ +* diff --git a/org.argeo.api.register/build.properties b/org.argeo.api.register/build.properties new file mode 100644 index 000000000..ae2abc5ff --- /dev/null +++ b/org.argeo.api.register/build.properties @@ -0,0 +1 @@ +source.. = src/ \ No newline at end of file diff --git a/org.argeo.api.register/src/org/argeo/api/register/Component.java b/org.argeo.api.register/src/org/argeo/api/register/Component.java new file mode 100644 index 000000000..f4676d9cf --- /dev/null +++ b/org.argeo.api.register/src/org/argeo/api/register/Component.java @@ -0,0 +1,333 @@ +package org.argeo.api.register; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * A wrapper for an object, whose dependencies and life cycle can be managed. + */ +public class Component implements Supplier, Comparable> { + + private final I instance; + + private final Runnable init; + private final Runnable close; + + private final Map, PublishedType> types; + private final Set> dependencies; + private final Map properties; + + private CompletableFuture activationStarted = null; + private CompletableFuture activated = null; + + private CompletableFuture deactivationStarted = null; + private CompletableFuture deactivated = null; + + // internal + private Set> dependants = new HashSet<>(); + + private RankingKey rankingKey; + + Component(ComponentRegister register, I instance, Runnable init, Runnable close, Set> dependencies, + Set> classes, Map properties) { + assert instance != null; + assert init != null; + assert close != null; + assert dependencies != null; + assert classes != null; + + this.instance = instance; + this.init = init; + this.close = close; + + // types + Map, PublishedType> types = new HashMap<>(classes.size()); + for (Class clss : classes) { +// if (!clss.isAssignableFrom(instance.getClass())) +// throw new IllegalArgumentException( +// "Type " + clss.getName() + " is not compatible with " + instance.getClass().getName()); + types.put(clss, new PublishedType<>(this, clss)); + } + this.types = Collections.unmodifiableMap(types); + + // dependencies + this.dependencies = Collections.unmodifiableSet(new HashSet<>(dependencies)); + for (Dependency dependency : this.dependencies) { + dependency.setDependantComponent(this); + } + + // deactivated by default + deactivated = CompletableFuture.completedFuture(null); + deactivationStarted = CompletableFuture.completedFuture(null); + + // TODO check whether context is active, so that we start right away + prepareNextActivation(); + + long serviceId = register.register(this); + Map props = new HashMap<>(properties); + props.put(RankingKey.SERVICE_ID, serviceId); + this.properties = Collections.unmodifiableMap(props); + rankingKey = new RankingKey(properties); + } + + private void prepareNextActivation() { + activationStarted = new CompletableFuture(); + activated = activationStarted // + .thenComposeAsync(this::dependenciesActivated) // + .thenRun(this.init) // + .thenRun(() -> prepareNextDeactivation()); + } + + private void prepareNextDeactivation() { + deactivationStarted = new CompletableFuture(); + deactivated = deactivationStarted // + .thenComposeAsync(this::dependantsDeactivated) // + .thenRun(this.close) // + .thenRun(() -> prepareNextActivation()); + } + + CompletableFuture getActivated() { + return activated; + } + + CompletableFuture getDeactivated() { + return deactivated; + } + + void startActivating() { + if (activated.isDone() || activationStarted.isDone()) + return; + activationStarted.complete(null); + } + + void startDeactivating() { + if (deactivated.isDone() || deactivationStarted.isDone()) + return; + deactivationStarted.complete(null); + } + + CompletableFuture dependenciesActivated(Void v) { + Set> constraints = new HashSet<>(this.dependencies.size()); + for (Dependency dependency : this.dependencies) { + CompletableFuture dependencyActivated = dependency.publisherActivated() // + .thenCompose(dependency::set); + constraints.add(dependencyActivated); + } + return CompletableFuture.allOf(constraints.toArray(new CompletableFuture[constraints.size()])); + } + + CompletableFuture dependantsDeactivated(Void v) { + Set> constraints = new HashSet<>(this.dependants.size()); + for (Dependency dependant : this.dependants) { + CompletableFuture dependantDeactivated = dependant.dependantDeactivated() // + .thenCompose(dependant::unset); + constraints.add(dependantDeactivated); + } + CompletableFuture dependantsDeactivated = CompletableFuture + .allOf(constraints.toArray(new CompletableFuture[constraints.size()])); + return dependantsDeactivated; + + } + + void addDependant(Dependency dependant) { + dependants.add(dependant); + } + + @Override + public I get() { + return instance; + } + + @SuppressWarnings("unchecked") + public PublishedType getType(Class clss) { + if (!types.containsKey(clss)) + throw new IllegalArgumentException(clss.getName() + " is not a type published by this component"); + return (PublishedType) types.get(clss); + } + + public boolean isPublishedType(Class clss) { + return types.containsKey(clss); + } + + public Map getProperties() { + return properties; + } + + @Override + public int compareTo(Component o) { + return rankingKey.compareTo(rankingKey); + } + + @Override + public int hashCode() { + Long serviceId = (Long) properties.get(RankingKey.SERVICE_ID); + if (serviceId != null) + return serviceId.intValue(); + else + return super.hashCode(); + } + + @Override + public String toString() { + List classes = new ArrayList<>(); + for (Class clss : types.keySet()) { + classes.add(clss.getName()); + } + return "Component " + classes + " " + properties + ""; + } + + /** A type which has been explicitly exposed by a component. */ + public static class PublishedType { + private Component component; + private Class clss; + + private CompletableFuture value; + + public PublishedType(Component component, Class clss) { + this.clss = clss; + this.component = component; + value = CompletableFuture.completedFuture((T) component.instance); + } + + public Component getPublisher() { + return component; + } + + public Class getType() { + return clss; + } + + public CompletionStage getValue() { + return value.minimalCompletionStage(); + } + } + + /** Builds a {@link Component}. */ + public static class Builder implements Supplier { + private final I instance; + + private Runnable init; + private Runnable close; + + private Set> dependencies = new HashSet<>(); + private Set> types = new HashSet<>(); + private final Map properties = new HashMap<>(); + + public Builder(I instance) { + this.instance = instance; + } + + public Component build(ComponentRegister register) { + // default values + if (types.isEmpty()) { + types.add(getInstanceClass()); + } + + if (init == null) + init = () -> { + }; + if (close == null) + close = () -> { + }; + + // instantiation + Component component = new Component(register, instance, init, close, dependencies, types, properties); + for (Dependency dependency : dependencies) { + dependency.type.getPublisher().addDependant(dependency); + } + return component; + } + + public Builder addType(Class clss) { + types.add(clss); + return this; + } + + public Builder addActivation(Runnable init) { + if (this.init != null) + throw new IllegalArgumentException("init method is already set"); + this.init = init; + return this; + } + + public Builder addDeactivation(Runnable close) { + if (this.close != null) + throw new IllegalArgumentException("close method is already set"); + this.close = close; + return this; + } + + public Builder addDependency(PublishedType type, Consumer set, Consumer unset) { + dependencies.add(new Dependency(type, set, unset)); + return this; + } + + public void addProperty(String key, Object value) { + if (properties.containsKey(key)) + throw new IllegalStateException("Key " + key + " is already set."); + properties.put(key, value); + } + + @Override + public I get() { + return instance; + } + + @SuppressWarnings("unchecked") + private Class getInstanceClass() { + return (Class) instance.getClass(); + } + + } + + static class Dependency { + private PublishedType type; + private Consumer set; + private Consumer unset; + + // live + Component dependantComponent; + CompletableFuture setStage; + CompletableFuture unsetStage; + + public Dependency(PublishedType types, Consumer set, Consumer unset) { + super(); + this.type = types; + this.set = set != null ? set : t -> { + }; + this.unset = unset != null ? unset : t -> { + }; + } + + // live + void setDependantComponent(Component component) { + this.dependantComponent = component; + } + + CompletableFuture publisherActivated() { + return type.getPublisher().activated.copy(); + } + + CompletableFuture dependantDeactivated() { + return dependantComponent.deactivated.copy(); + } + + CompletableFuture set(Void v) { + return type.value.thenAccept(set); + } + + CompletableFuture unset(Void v) { + return type.value.thenAccept(unset); + } + + } +} diff --git a/org.argeo.api.register/src/org/argeo/api/register/ComponentRegister.java b/org.argeo.api.register/src/org/argeo/api/register/ComponentRegister.java new file mode 100644 index 000000000..1bb9036f8 --- /dev/null +++ b/org.argeo.api.register/src/org/argeo/api/register/ComponentRegister.java @@ -0,0 +1,43 @@ +package org.argeo.api.register; + +import java.util.Map; +import java.util.SortedSet; +import java.util.function.Predicate; + +/** A register of components which can coordinate their activation. */ +public interface ComponentRegister { + long register(Component component); + + SortedSet> find(Class clss, Predicate> filter); + + default Component.PublishedType getSingleton(Class type) { + SortedSet> found = find(type, null); + if (found.size() == 0) + throw new IllegalStateException("No component found for " + type); + return found.first().getType(type); + } + + default T getObject(Class clss) { + SortedSet> found = find(clss, null); + if (found.size() == 0) + return null; + return found.first().get(); + } + + Component get(Object instance); + +// default PublishedType getType(Class clss) { +// SortedSet> components = find(clss, null); +// if (components.size() == 0) +// return null; +// return components.first().getType(clss); +// } + + void activate(); + + void deactivate(); + + boolean isActive(); + + void clear(); +} diff --git a/org.argeo.api.register/src/org/argeo/api/register/RankingKey.java b/org.argeo.api.register/src/org/argeo/api/register/RankingKey.java new file mode 100644 index 000000000..3886afe65 --- /dev/null +++ b/org.argeo.api.register/src/org/argeo/api/register/RankingKey.java @@ -0,0 +1,105 @@ +package org.argeo.api.register; + +import java.util.Map; +import java.util.Objects; + +/** + * Key used to classify and filter available components. + */ +public class RankingKey implements Comparable { + public final static String SERVICE_PID = "service.pid"; + public final static String SERVICE_ID = "service.id"; + public final static String SERVICE_RANKING = "service.ranking"; + + private String pid; + private Integer ranking = 0; + private Long id = 0l; + + public RankingKey(String pid, Integer ranking, Long id) { + super(); + this.pid = pid; + this.ranking = ranking; + this.id = id; + } + + public RankingKey(Map properties) { + this.pid = properties.containsKey(SERVICE_PID) ? properties.get(SERVICE_PID).toString() : null; + this.ranking = properties.containsKey(SERVICE_RANKING) + ? Integer.parseInt(properties.get(SERVICE_RANKING).toString()) + : 0; + this.id = properties.containsKey(SERVICE_ID) ? (Long) properties.get(SERVICE_ID) : null; + } + + @Override + public int hashCode() { + Integer result = 0; + if (pid != null) + result = +pid.hashCode(); + if (ranking != null) + result = +ranking; + return result; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return new RankingKey(pid, ranking, id); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(""); + if (pid != null) + sb.append(pid); + if (ranking != null && ranking != 0) + sb.append(' ').append(ranking); + return sb.toString(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RankingKey)) + return false; + RankingKey other = (RankingKey) obj; + return Objects.equals(pid, other.pid) && Objects.equals(ranking, other.ranking) && Objects.equals(id, other.id); + } + + @Override + public int compareTo(RankingKey o) { + if (pid != null && o.pid != null) { + if (pid.equals(o.pid)) { + if (ranking.equals(o.ranking)) + if (id != null && o.id != null) + return id.compareTo(o.id); + else + return 0; + else + return ranking.compareTo(o.ranking); + } else { + return pid.compareTo(o.pid); + } + + } else { + } + return -1; + } + + public String getPid() { + return pid; + } + + public Integer getRanking() { + return ranking; + } + + public Long getId() { + return id; + } + + public static RankingKey minPid(String pid) { + return new RankingKey(pid, Integer.MIN_VALUE, null); + } + + public static RankingKey maxPid(String pid) { + return new RankingKey(pid, Integer.MAX_VALUE, null); + } +} diff --git a/org.argeo.api.register/src/org/argeo/api/register/SimpleRegister.java b/org.argeo.api.register/src/org/argeo/api/register/SimpleRegister.java new file mode 100644 index 000000000..9ed7e765f --- /dev/null +++ b/org.argeo.api.register/src/org/argeo/api/register/SimpleRegister.java @@ -0,0 +1,124 @@ +package org.argeo.api.register; + +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Predicate; + +/** A minimal component register. */ +public class SimpleRegister implements ComponentRegister { + private final AtomicBoolean started = new AtomicBoolean(false); + private final IdentityHashMap> components = new IdentityHashMap<>(); + private final AtomicLong nextServiceId = new AtomicLong(0l); + + @Override + public long register(Component component) { + return registerComponent(component); + } + + @SuppressWarnings({ "unchecked" }) + @Override + public synchronized SortedSet> find(Class clss, + Predicate> filter) { + SortedSet> result = new TreeSet<>(); + instances: for (Object instance : components.keySet()) { + if (!clss.isAssignableFrom(instance.getClass())) + continue instances; + Component component = (Component) components.get(instance); + + if (component.isPublishedType(clss)) { + if (filter != null) { + filter.test(component.getProperties()); + } + result.add(component); + } + } + if (result.isEmpty()) + return null; + return result; + + } + + synchronized long registerComponent(Component component) { + if (started.get()) // TODO make it really dynamic + throw new IllegalStateException("Already activated"); + if (components.containsKey(component.get())) + throw new IllegalArgumentException("Already registered as component"); + components.put(component.get(), component); + return nextServiceId.incrementAndGet(); + } + + @Override + public synchronized Component get(Object instance) { + if (!components.containsKey(instance)) + throw new IllegalArgumentException("Not registered as component"); + return components.get(instance); + } + + @Override + public synchronized void activate() { + if (started.get()) + throw new IllegalStateException("Already activated"); + Set> constraints = new HashSet<>(); + for (Component component : components.values()) { + component.startActivating(); + constraints.add(component.getActivated()); + } + + // wait + try { + CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(true)) + .get(); + } catch (InterruptedException e) { + throw new RuntimeException("Register activation has been interrupted", e); + } catch (ExecutionException e) { + if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) { + throw (RuntimeException) e.getCause(); + } else { + throw new IllegalStateException("Cannot activate register", e.getCause()); + } + } + } + + @Override + public synchronized void deactivate() { + if (!started.get()) + throw new IllegalStateException("Not activated"); + Set> constraints = new HashSet<>(); + for (Component component : components.values()) { + component.startDeactivating(); + constraints.add(component.getDeactivated()); + } + + // wait + try { + CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(false)) + .get(); + } catch (InterruptedException e) { + throw new RuntimeException("Register deactivation has been interrupted", e); + } catch (ExecutionException e) { + if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) { + throw (RuntimeException) e.getCause(); + } else { + throw new IllegalStateException("Cannot deactivate register", e.getCause()); + } + } + } + + @Override + public synchronized boolean isActive() { + return started.get(); + } + + @Override + public synchronized void clear() { + components.clear(); + } +} diff --git a/org.argeo.cms.ee/bnd.bnd b/org.argeo.cms.ee/bnd.bnd index 08ce1365c..f09995c00 100644 --- a/org.argeo.cms.ee/bnd.bnd +++ b/org.argeo.cms.ee/bnd.bnd @@ -1,5 +1,4 @@ Import-Package:\ -org.argeo.util.http,\ org.osgi.service.http;version=0.0.0,\ org.osgi.service.http.whiteboard;version=0.0.0,\ org.osgi.framework.namespace;version=0.0.0,\ diff --git a/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java index 205699464..672722946 100644 --- a/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java +++ b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java @@ -6,7 +6,7 @@ import java.io.Writer; import javax.servlet.http.HttpServletResponse; import org.argeo.api.cms.CmsLog; -import org.argeo.util.ExceptionsChain; +import org.argeo.cms.util.ExceptionsChain; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsTokenServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsTokenServlet.java index 983202ad2..e170609bb 100644 --- a/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsTokenServlet.java +++ b/org.argeo.cms.ee/src/org/argeo/cms/integration/CmsTokenServlet.java @@ -15,13 +15,13 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.argeo.api.acr.ldap.NamingUtils; import org.argeo.api.cms.CmsAuth; import org.argeo.cms.CmsUserManager; import org.argeo.cms.auth.RemoteAuthCallback; import org.argeo.cms.auth.RemoteAuthCallbackHandler; import org.argeo.cms.servlet.ServletHttpRequest; import org.argeo.cms.servlet.ServletHttpResponse; -import org.argeo.util.naming.NamingUtils; import org.osgi.service.useradmin.Authorization; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/org.argeo.cms.ee/src/org/argeo/cms/servlet/CmsServletContext.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/CmsServletContext.java index 6a5208730..d3c0eb540 100644 --- a/org.argeo.cms.ee/src/org/argeo/cms/servlet/CmsServletContext.java +++ b/org.argeo.cms.ee/src/org/argeo/cms/servlet/CmsServletContext.java @@ -2,11 +2,8 @@ package org.argeo.cms.servlet; import java.io.IOException; import java.net.URL; -import java.net.http.HttpHeaders; -import java.security.PrivilegedAction; import java.util.Map; -import javax.security.auth.Subject; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import javax.servlet.http.HttpServletRequest; @@ -19,7 +16,6 @@ import org.argeo.cms.auth.RemoteAuthRequest; import org.argeo.cms.auth.RemoteAuthResponse; import org.argeo.cms.auth.RemoteAuthUtils; import org.argeo.cms.servlet.internal.HttpUtils; -import org.argeo.util.http.HttpHeader; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkUtil; import org.osgi.service.http.context.ServletContextHelper; diff --git a/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java b/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java index dd0b70aff..2b2ffcb10 100644 --- a/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java +++ b/org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java @@ -12,9 +12,9 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.argeo.cms.osgi.FilterRequirement; import org.argeo.cms.osgi.PublishNamespace; -import org.argeo.osgi.util.FilterRequirement; -import org.argeo.util.StreamUtils; +import org.argeo.cms.util.StreamUtils; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; diff --git a/org.argeo.cms.ee/src/org/argeo/cms/websocket/server/TestEndpoint.java b/org.argeo.cms.ee/src/org/argeo/cms/websocket/server/TestEndpoint.java index cc608136b..f0c7fca3a 100644 --- a/org.argeo.cms.ee/src/org/argeo/cms/websocket/server/TestEndpoint.java +++ b/org.argeo.cms.ee/src/org/argeo/cms/websocket/server/TestEndpoint.java @@ -16,9 +16,9 @@ import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; +import org.argeo.api.acr.ldap.NamingUtils; import org.argeo.api.cms.CmsLog; import org.argeo.cms.integration.CmsExceptionsChain; -import org.argeo.util.naming.NamingUtils; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServiceRegistration; diff --git a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java index d393e4b2b..4141cd8cc 100644 --- a/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java +++ b/org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java @@ -13,7 +13,7 @@ import javax.websocket.server.ServerContainer; import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsState; import org.argeo.cms.CmsDeployProperty; -import org.argeo.util.http.HttpServerUtils; +import org.argeo.cms.http.HttpServerUtils; import org.eclipse.jetty.http.UriCompliance; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/BasicSshServer.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/BasicSshServer.java index be41f9cbd..cde6c935a 100644 --- a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/BasicSshServer.java +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/BasicSshServer.java @@ -9,7 +9,7 @@ import org.apache.sshd.server.SshServer; import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.shell.ProcessShellFactory; -import org.argeo.util.OS; +import org.argeo.cms.util.OS; /** A simple SSH server with some defaults. Supports SCP. */ public class BasicSshServer { diff --git a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshSync.java b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshSync.java index 660db5967..31e63411a 100644 --- a/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshSync.java +++ b/org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshSync.java @@ -24,7 +24,7 @@ import org.apache.sshd.common.config.keys.FilePasswordProvider; import org.apache.sshd.sftp.client.fs.SftpFileSystem; import org.apache.sshd.sftp.client.fs.SftpFileSystemProvider; import org.argeo.api.cms.CmsLog; -import org.argeo.util.StreamUtils; +import org.argeo.cms.util.StreamUtils; public class SshSync { private final static CmsLog log = CmsLog.getLog(SshSync.class); diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/CmsUxUtils.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/CmsUxUtils.java index 916cc74f2..087b4ff7c 100644 --- a/org.argeo.cms.ux/src/org/argeo/cms/ux/CmsUxUtils.java +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/CmsUxUtils.java @@ -5,7 +5,7 @@ import org.argeo.api.acr.ContentRepository; import org.argeo.api.acr.ContentSession; import org.argeo.api.cms.ux.Cms2DSize; import org.argeo.api.cms.ux.CmsView; -import org.argeo.util.CurrentSubject; +import org.argeo.cms.util.CurrentSubject; public class CmsUxUtils { public static ContentSession getContentSession(ContentRepository contentRepository, CmsView cmsView) { diff --git a/org.argeo.cms/OSGI-INF/cmsUserAdmin.xml b/org.argeo.cms/OSGI-INF/cmsUserAdmin.xml index 50a9ea6eb..52c75318e 100644 --- a/org.argeo.cms/OSGI-INF/cmsUserAdmin.xml +++ b/org.argeo.cms/OSGI-INF/cmsUserAdmin.xml @@ -2,8 +2,8 @@ - - + + diff --git a/org.argeo.cms/OSGI-INF/cmsUserManager.xml b/org.argeo.cms/OSGI-INF/cmsUserManager.xml index d22660c9f..1f0033171 100644 --- a/org.argeo.cms/OSGI-INF/cmsUserManager.xml +++ b/org.argeo.cms/OSGI-INF/cmsUserManager.xml @@ -5,5 +5,5 @@ - + diff --git a/org.argeo.cms/OSGI-INF/transactionManager.xml b/org.argeo.cms/OSGI-INF/transactionManager.xml index 81997476e..df317e937 100644 --- a/org.argeo.cms/OSGI-INF/transactionManager.xml +++ b/org.argeo.cms/OSGI-INF/transactionManager.xml @@ -1,8 +1,8 @@ - + - - + + diff --git a/org.argeo.cms/src/org/argeo/cms/AbstractKeyring.java b/org.argeo.cms/src/org/argeo/cms/AbstractKeyring.java new file mode 100644 index 000000000..c2c9c6107 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/AbstractKeyring.java @@ -0,0 +1,290 @@ +package org.argeo.cms; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.Iterator; + +import javax.crypto.SecretKey; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.TextOutputCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.keyring.PBEKeySpecCallback; +import org.argeo.cms.util.CurrentSubject; +import org.argeo.cms.util.StreamUtils; +import org.argeo.api.cms.keyring.CryptoKeyring; +import org.argeo.api.cms.keyring.Keyring; + +/** username / password based keyring. TODO internationalize */ +public abstract class AbstractKeyring implements Keyring, CryptoKeyring { + // public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING"; + + // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT; + private CallbackHandler defaultCallbackHandler; + + private String charset = "UTF-8"; + + /** + * Default provider is bouncy castle, in order to have consistent behaviour + * across implementations + */ + private String securityProviderName = "BC"; + + /** + * Whether the keyring has already been created in the past with a master + * password + */ + protected abstract Boolean isSetup(); + + /** + * Setup the keyring persistently, {@link #isSetup()} must return true + * afterwards + */ + protected abstract void setup(char[] password); + + /** Populates the key spec callback */ + protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback); + + protected abstract void encrypt(String path, InputStream unencrypted); + + protected abstract InputStream decrypt(String path); + + /** Triggers lazy initialization */ + protected SecretKey getSecretKey(char[] password) { + Subject subject = CurrentSubject.current(); + if (subject == null) + throw new IllegalStateException("Current subject cannot be null"); + // we assume only one secrete key is available + Iterator iterator = subject.getPrivateCredentials(SecretKey.class).iterator(); + if (!iterator.hasNext() || password != null) {// not initialized + CallbackHandler callbackHandler = password == null ? new KeyringCallbackHandler() + : new PasswordProvidedCallBackHandler(password); + ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); + try { + LoginContext loginContext = new LoginContext(CmsAuth.LOGIN_CONTEXT_KEYRING, subject, callbackHandler); + loginContext.login(); + // FIXME will login even if password is wrong + iterator = subject.getPrivateCredentials(SecretKey.class).iterator(); + return iterator.next(); + } catch (LoginException e) { + throw new IllegalStateException("Keyring login failed", e); + } finally { + Thread.currentThread().setContextClassLoader(currentContextClassLoader); + } + + } else { + SecretKey secretKey = iterator.next(); + if (iterator.hasNext()) + throw new IllegalStateException("More than one secret key in private credentials"); + return secretKey; + } + } + + public InputStream getAsStream(String path) { + return decrypt(path); + } + + public void set(String path, InputStream in) { + encrypt(path, in); + } + + public char[] getAsChars(String path) { + // InputStream in = getAsStream(path); + // CharArrayWriter writer = null; + // Reader reader = null; + try (InputStream in = getAsStream(path); + CharArrayWriter writer = new CharArrayWriter(); + Reader reader = new InputStreamReader(in, charset);) { + StreamUtils.copy(reader, writer); + return writer.toCharArray(); + } catch (IOException e) { + throw new IllegalStateException("Cannot decrypt to char array", e); + } finally { + // IOUtils.closeQuietly(reader); + // IOUtils.closeQuietly(in); + // IOUtils.closeQuietly(writer); + } + } + + public void set(String path, char[] arr) { + // ByteArrayOutputStream out = new ByteArrayOutputStream(); + // ByteArrayInputStream in = null; + // Writer writer = null; + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer writer = new OutputStreamWriter(out, charset);) { + // writer = new OutputStreamWriter(out, charset); + writer.write(arr); + writer.flush(); + // in = new ByteArrayInputStream(out.toByteArray()); + try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());) { + set(path, in); + } + } catch (IOException e) { + throw new IllegalStateException("Cannot encrypt to char array", e); + } finally { + // IOUtils.closeQuietly(writer); + // IOUtils.closeQuietly(out); + // IOUtils.closeQuietly(in); + } + } + + public void unlock(char[] password) { + if (!isSetup()) + setup(password); + SecretKey secretKey = getSecretKey(password); + if (secretKey == null) + throw new IllegalStateException("Could not unlock keyring"); + } + + protected Provider getSecurityProvider() { + return Security.getProvider(securityProviderName); + } + + public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) { + this.defaultCallbackHandler = defaultCallbackHandler; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public void setSecurityProviderName(String securityProviderName) { + this.securityProviderName = securityProviderName; + } + + // @Deprecated + // protected static byte[] hash(char[] password, byte[] salt, Integer + // iterationCount) { + // ByteArrayOutputStream out = null; + // OutputStreamWriter writer = null; + // try { + // out = new ByteArrayOutputStream(); + // writer = new OutputStreamWriter(out, "UTF-8"); + // writer.write(password); + // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256"); + // pwDigest.reset(); + // pwDigest.update(salt); + // byte[] btPass = pwDigest.digest(out.toByteArray()); + // for (int i = 0; i < iterationCount; i++) { + // pwDigest.reset(); + // btPass = pwDigest.digest(btPass); + // } + // return btPass; + // } catch (Exception e) { + // throw new CmsException("Cannot hash", e); + // } finally { + // IOUtils.closeQuietly(out); + // IOUtils.closeQuietly(writer); + // } + // + // } + + /** + * Convenience method using the underlying callback to ask for a password + * (typically used when the password is not saved in the keyring) + */ + protected char[] ask() { + PasswordCallback passwordCb = new PasswordCallback("Password", false); + Callback[] dialogCbs = new Callback[] { passwordCb }; + try { + defaultCallbackHandler.handle(dialogCbs); + char[] password = passwordCb.getPassword(); + return password; + } catch (Exception e) { + throw new IllegalStateException("Cannot ask for a password", e); + } + + } + + class KeyringCallbackHandler implements CallbackHandler { + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + // checks + if (callbacks.length != 2) + throw new IllegalArgumentException( + "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}"); + if (!(callbacks[0] instanceof PasswordCallback)) + throw new UnsupportedCallbackException(callbacks[0]); + if (!(callbacks[1] instanceof PBEKeySpecCallback)) + throw new UnsupportedCallbackException(callbacks[0]); + + PasswordCallback passwordCb = (PasswordCallback) callbacks[0]; + PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1]; + + if (isSetup()) { + Callback[] dialogCbs = new Callback[] { passwordCb }; + defaultCallbackHandler.handle(dialogCbs); + } else {// setup keyring + TextOutputCallback textCb1 = new TextOutputCallback(TextOutputCallback.INFORMATION, + "Enter a master password which will protect your private data"); + TextOutputCallback textCb2 = new TextOutputCallback(TextOutputCallback.INFORMATION, + "(for example your credentials to third-party services)"); + TextOutputCallback textCb3 = new TextOutputCallback(TextOutputCallback.INFORMATION, + "Don't forget this password since the data cannot be read without it"); + PasswordCallback confirmPasswordCb = new PasswordCallback("Confirm password", false); + // first try + Callback[] dialogCbs = new Callback[] { textCb1, textCb2, textCb3, passwordCb, confirmPasswordCb }; + defaultCallbackHandler.handle(dialogCbs); + + // if passwords different, retry (except if cancelled) + while (passwordCb.getPassword() != null + && !Arrays.equals(passwordCb.getPassword(), confirmPasswordCb.getPassword())) { + TextOutputCallback textCb = new TextOutputCallback(TextOutputCallback.ERROR, + "The passwords do not match"); + dialogCbs = new Callback[] { textCb, passwordCb, confirmPasswordCb }; + defaultCallbackHandler.handle(dialogCbs); + } + + if (passwordCb.getPassword() != null) {// not cancelled + setup(passwordCb.getPassword()); + } + } + + if (passwordCb.getPassword() != null) + handleKeySpecCallback(pbeCb); + } + + } + + class PasswordProvidedCallBackHandler implements CallbackHandler { + private final char[] password; + + public PasswordProvidedCallBackHandler(char[] password) { + this.password = password; + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + // checks + if (callbacks.length != 2) + throw new IllegalArgumentException( + "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}"); + if (!(callbacks[0] instanceof PasswordCallback)) + throw new UnsupportedCallbackException(callbacks[0]); + if (!(callbacks[1] instanceof PBEKeySpecCallback)) + throw new UnsupportedCallbackException(callbacks[0]); + + PasswordCallback passwordCb = (PasswordCallback) callbacks[0]; + passwordCb.setPassword(password); + PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1]; + handleKeySpecCallback(pbeCb); + } + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java b/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java index 728884b52..3e7d31e3e 100644 --- a/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java +++ b/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java @@ -7,9 +7,9 @@ import java.util.Set; import javax.security.auth.Subject; +import org.argeo.api.cms.directory.HierarchyUnit; import org.argeo.cms.auth.SystemRole; -import org.argeo.osgi.useradmin.UserDirectory; -import org.argeo.util.directory.HierarchyUnit; +import org.argeo.cms.osgi.useradmin.UserDirectory; import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.useradmin.Group; import org.osgi.service.useradmin.Role; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java index ce05dc14c..16f39609e 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java @@ -17,7 +17,7 @@ import org.argeo.api.acr.CrName; import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; -import org.argeo.util.LangUtils; +import org.argeo.cms.util.LangUtils; /** Partial reference implementation of a {@link ProvidedContent}. */ public abstract class AbstractContent extends AbstractMap implements ProvidedContent { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java index a2d8069dd..98a2fec93 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java @@ -17,6 +17,7 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; +import org.argeo.api.acr.ArgeoNamespace; import org.argeo.api.acr.Content; import org.argeo.api.acr.CrName; import org.argeo.api.acr.NamespaceUtils; @@ -117,7 +118,7 @@ public abstract class AbstractContentRepository implements ProvidedRepository { // document = dBuilder.parse(inputSource); // } else { document = dBuilder.newDocument(); - Element root = document.createElementNS(CrName.CR_NAMESPACE_URI, + Element root = document.createElementNS(ArgeoNamespace.CR_NAMESPACE_URI, NamespaceUtils.toPrefixedName(CrName.root.qName())); for (String prefix : RuntimeNamespaceContext.getPrefixes().keySet()) { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java index 474e07232..2b4de14da 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java @@ -17,7 +17,7 @@ import org.argeo.api.cms.DataAdminPrincipal; import org.argeo.api.uuid.UuidFactory; import org.argeo.cms.auth.CurrentUser; import org.argeo.cms.internal.runtime.CmsContextImpl; -import org.argeo.util.CurrentSubject; +import org.argeo.cms.util.CurrentSubject; /** * Multi-session {@link ProvidedRepository}, integrated with a CMS. diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java index fff40c1bb..9caa4e657 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java @@ -4,13 +4,13 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Objects; -import org.argeo.api.acr.CrName; +import org.argeo.api.acr.ArgeoNamespace; public enum CmsContentTypes { // // ARGEO // - CR_2(CrName.CR_DEFAULT_PREFIX, CrName.CR_NAMESPACE_URI, "cr.xsd", null), + CR_2(ArgeoNamespace.CR_DEFAULT_PREFIX, ArgeoNamespace.CR_NAMESPACE_URI, "cr.xsd", null), // SLC("slc", "http://www.argeo.org/ns/slc", null, null), // diff --git a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java index 74e93fc0a..a6acb8a34 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java @@ -15,11 +15,11 @@ import org.argeo.api.acr.ContentRepository; import org.argeo.api.acr.ContentSession; import org.argeo.api.acr.DName; import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.directory.Directory; +import org.argeo.api.cms.directory.HierarchyUnit; import org.argeo.cms.CmsUserManager; -import org.argeo.osgi.useradmin.UserDirectory; -import org.argeo.util.CurrentSubject; -import org.argeo.util.directory.Directory; -import org.argeo.util.directory.HierarchyUnit; +import org.argeo.cms.osgi.useradmin.UserDirectory; +import org.argeo.cms.util.CurrentSubject; import org.osgi.service.useradmin.Role; /** Utilities and routines around {@link Content}. */ diff --git a/org.argeo.cms/src/org/argeo/cms/acr/SingleUserContentRepository.java b/org.argeo.cms/src/org/argeo/cms/acr/SingleUserContentRepository.java index 5e7c191e2..8c87c5a93 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/SingleUserContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/SingleUserContentRepository.java @@ -10,11 +10,11 @@ import javax.security.auth.Subject; import javax.security.auth.x500.X500Principal; import org.argeo.api.acr.ContentSession; +import org.argeo.api.acr.ldap.LdapAttrs; import org.argeo.api.acr.spi.ProvidedRepository; import org.argeo.api.uuid.MacAddressUuidFactory; import org.argeo.api.uuid.UuidFactory; import org.argeo.cms.acr.fs.FsContentProvider; -import org.argeo.util.naming.LdapAttrs; /** * A standalone {@link ProvidedRepository} with a single {@link Subject} (which diff --git a/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java b/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java index 1313704f0..96e6eeaf3 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java @@ -17,7 +17,7 @@ import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.AbstractContent; import org.argeo.cms.acr.ContentUtils; import org.argeo.cms.dav.DavResponse; -import org.argeo.util.http.HttpStatus; +import org.argeo.cms.http.HttpStatus; public class DavContent extends AbstractContent { private final DavContentProvider provider; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java index bc4bbfed3..374fcebbc 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java @@ -10,7 +10,7 @@ import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.dav.DavClient; import org.argeo.cms.dav.DavResponse; -import org.argeo.util.http.HttpStatus; +import org.argeo.cms.http.HttpStatus; public class DavContentProvider implements ContentProvider { private String mountPath; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/AbstractDirectoryContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/AbstractDirectoryContent.java index 2d337c0dd..ac0a7317f 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/AbstractDirectoryContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/AbstractDirectoryContent.java @@ -10,15 +10,15 @@ import java.util.TreeSet; import javax.xml.namespace.QName; +import org.argeo.api.acr.ArgeoNamespace; import org.argeo.api.acr.ContentName; import org.argeo.api.acr.CrAttributeType; -import org.argeo.api.acr.CrName; import org.argeo.api.acr.NamespaceUtils; +import org.argeo.api.acr.ldap.LdapAttrs; +import org.argeo.api.acr.ldap.LdapObjs; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.AbstractContent; -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.LdapObjs; abstract class AbstractDirectoryContent extends AbstractContent { protected final DirectoryContentProvider provider; @@ -54,7 +54,7 @@ abstract class AbstractDirectoryContent extends AbstractContent { continue keys; if (key.equalsIgnoreCase(LdapAttrs.objectClasses.name())) continue keys; - ContentName name = new ContentName(CrName.LDAP_NAMESPACE_URI, key, provider); + ContentName name = new ContentName(ArgeoNamespace.LDAP_NAMESPACE_URI, key, provider); keys.add(name); } return keys; @@ -65,7 +65,7 @@ abstract class AbstractDirectoryContent extends AbstractContent { Dictionary properties = doGetProperties(); List contentClasses = new ArrayList<>(); String objectClass = properties.get(LdapAttrs.objectClass.name()).toString(); - contentClasses.add(new ContentName(CrName.LDAP_NAMESPACE_URI, objectClass, provider)); + contentClasses.add(new ContentName(ArgeoNamespace.LDAP_NAMESPACE_URI, objectClass, provider)); String[] objectClasses = properties.get(LdapAttrs.objectClasses.name()).toString().split("\\n"); objectClasses: for (String oc : objectClasses) { @@ -73,7 +73,7 @@ abstract class AbstractDirectoryContent extends AbstractContent { continue objectClasses; if (objectClass.equalsIgnoreCase(oc)) continue objectClasses; - contentClasses.add(new ContentName(CrName.LDAP_NAMESPACE_URI, oc, provider)); + contentClasses.add(new ContentName(ArgeoNamespace.LDAP_NAMESPACE_URI, oc, provider)); } return contentClasses; } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java index 4e738ae2b..992f0b41b 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java @@ -10,8 +10,8 @@ import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentName; import org.argeo.api.acr.spi.ProvidedSession; -import org.argeo.util.directory.Directory; -import org.argeo.util.directory.HierarchyUnit; +import org.argeo.api.cms.directory.Directory; +import org.argeo.api.cms.directory.HierarchyUnit; class DirectoryContent extends AbstractDirectoryContent { private Directory directory; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java index aab5d6dc0..08171435c 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java @@ -7,18 +7,18 @@ import java.util.List; import javax.xml.namespace.QName; +import org.argeo.api.acr.ArgeoNamespace; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentName; import org.argeo.api.acr.ContentNotFoundException; -import org.argeo.api.acr.CrName; import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.api.cms.directory.HierarchyUnit; import org.argeo.cms.CmsUserManager; import org.argeo.cms.acr.AbstractContent; import org.argeo.cms.acr.ContentUtils; -import org.argeo.osgi.useradmin.UserDirectory; -import org.argeo.util.directory.HierarchyUnit; +import org.argeo.cms.osgi.useradmin.UserDirectory; import org.osgi.service.useradmin.User; public class DirectoryContentProvider implements ContentProvider { @@ -92,16 +92,16 @@ public class DirectoryContentProvider implements ContentProvider { @Override public String getNamespaceURI(String prefix) { - if (CrName.LDAP_DEFAULT_PREFIX.equals(prefix)) - return CrName.LDAP_NAMESPACE_URI; - throw new IllegalArgumentException("Only prefix " + CrName.LDAP_DEFAULT_PREFIX + " is supported"); + if (ArgeoNamespace.LDAP_DEFAULT_PREFIX.equals(prefix)) + return ArgeoNamespace.LDAP_NAMESPACE_URI; + throw new IllegalArgumentException("Only prefix " + ArgeoNamespace.LDAP_DEFAULT_PREFIX + " is supported"); } @Override public Iterator getPrefixes(String namespaceURI) { - if (CrName.LDAP_NAMESPACE_URI.equals(namespaceURI)) - return Collections.singletonList(CrName.LDAP_DEFAULT_PREFIX).iterator(); - throw new IllegalArgumentException("Only namespace URI " + CrName.LDAP_NAMESPACE_URI + " is supported"); + if (ArgeoNamespace.LDAP_NAMESPACE_URI.equals(namespaceURI)) + return Collections.singletonList(ArgeoNamespace.LDAP_DEFAULT_PREFIX).iterator(); + throw new IllegalArgumentException("Only namespace URI " + ArgeoNamespace.LDAP_NAMESPACE_URI + " is supported"); } public void setUserManager(CmsUserManager userManager) { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java index 1e4aad773..feae4b517 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java @@ -13,9 +13,9 @@ import org.argeo.api.acr.ContentName; import org.argeo.api.acr.CrName; import org.argeo.api.acr.DName; import org.argeo.api.acr.spi.ProvidedSession; -import org.argeo.osgi.useradmin.UserDirectory; -import org.argeo.util.directory.Directory; -import org.argeo.util.directory.HierarchyUnit; +import org.argeo.api.cms.directory.Directory; +import org.argeo.api.cms.directory.HierarchyUnit; +import org.argeo.cms.osgi.useradmin.UserDirectory; import org.osgi.service.useradmin.Role; class HierarchyUnitContent extends AbstractDirectoryContent { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java index 7aa144633..3b1ae46b2 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java @@ -7,7 +7,7 @@ import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentName; import org.argeo.api.acr.spi.ProvidedSession; -import org.argeo.osgi.useradmin.UserDirectory; +import org.argeo.cms.osgi.useradmin.UserDirectory; import org.osgi.service.useradmin.Group; import org.osgi.service.useradmin.Role; import org.osgi.service.useradmin.User; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java index 2d30cedec..43cae8572 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java @@ -44,7 +44,7 @@ import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.api.cms.CmsLog; import org.argeo.cms.acr.AbstractContent; import org.argeo.cms.acr.ContentUtils; -import org.argeo.util.FsUtils; +import org.argeo.cms.util.FsUtils; /** Content persisted as a filesystem {@link Path}. */ public class FsContent extends AbstractContent implements ProvidedContent { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContentProvider.java index 59a4d8deb..9b1b96683 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContentProvider.java @@ -13,8 +13,8 @@ import java.util.Objects; import java.util.TreeMap; import java.util.stream.Collectors; +import org.argeo.api.acr.ArgeoNamespace; import org.argeo.api.acr.ContentResourceException; -import org.argeo.api.acr.CrName; import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.acr.RuntimeNamespaceContext; import org.argeo.api.acr.spi.ContentProvider; @@ -62,10 +62,10 @@ public class FsContentProvider implements ContentProvider { } // defaults - addDefaultNamespace(udfav, CrName.CR_DEFAULT_PREFIX, CrName.CR_NAMESPACE_URI); - addDefaultNamespace(udfav, "basic", CrName.CR_NAMESPACE_URI); - addDefaultNamespace(udfav, "owner", CrName.CR_NAMESPACE_URI); - addDefaultNamespace(udfav, "posix", CrName.CR_NAMESPACE_URI); + addDefaultNamespace(udfav, ArgeoNamespace.CR_DEFAULT_PREFIX, ArgeoNamespace.CR_NAMESPACE_URI); + addDefaultNamespace(udfav, "basic", ArgeoNamespace.CR_NAMESPACE_URI); + addDefaultNamespace(udfav, "owner", ArgeoNamespace.CR_NAMESPACE_URI); + addDefaultNamespace(udfav, "posix", ArgeoNamespace.CR_NAMESPACE_URI); } catch (IOException e) { throw new RuntimeException("Cannot read namespaces from " + rootPath, e); } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/xml/ElementIterator.java b/org.argeo.cms/src/org/argeo/cms/acr/xml/ElementIterator.java index ea1a17d7e..3f7476221 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/xml/ElementIterator.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/xml/ElementIterator.java @@ -3,6 +3,7 @@ package org.argeo.cms.acr.xml; import java.util.Iterator; import java.util.NoSuchElementException; +import org.argeo.api.acr.ArgeoNamespace; import org.argeo.api.acr.Content; import org.argeo.api.acr.CrName; import org.argeo.api.acr.spi.ProvidedSession; @@ -52,7 +53,7 @@ class ElementIterator implements Iterator { if (nextElement == null) throw new NoSuchElementException(); Content result; - String isMount = nextElement.getAttributeNS(CrName.CR_NAMESPACE_URI, CrName.mount.qName().getLocalPart()); + String isMount = nextElement.getAttributeNS(ArgeoNamespace.CR_NAMESPACE_URI, CrName.mount.qName().getLocalPart()); if (isMount.equals("true")) { result = session.get(parent.getPath() + '/' + nextElement.getTagName()); } diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java index d7d6c282c..289f8dcc6 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java @@ -27,7 +27,7 @@ import org.argeo.cms.internal.auth.CmsSessionImpl; import org.argeo.cms.internal.auth.ImpliedByPrincipal; import org.argeo.cms.internal.auth.RemoteCmsSessionImpl; import org.argeo.cms.internal.runtime.CmsContextImpl; -import org.argeo.osgi.useradmin.AuthenticatingUser; +import org.argeo.cms.osgi.useradmin.AuthenticatingUser; import org.osgi.service.useradmin.Authorization; /** Centralises security related registrations. */ diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsRole.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsRole.java index 6aa1c1c25..5ac23ea6a 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsRole.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsRole.java @@ -2,8 +2,8 @@ package org.argeo.cms.auth; import javax.xml.namespace.QName; +import org.argeo.api.acr.ArgeoNamespace; import org.argeo.api.acr.ContentName; -import org.argeo.api.acr.CrName; /** Standard CMS system roles. */ public enum CmsRole implements SystemRole { @@ -15,7 +15,7 @@ public enum CmsRole implements SystemRole { private final ContentName name; CmsRole() { - name = new ContentName(CrName.ROLE_NAMESPACE_URI, QUALIFIER + name()); + name = new ContentName(ArgeoNamespace.ROLE_NAMESPACE_URI, QUALIFIER + name()); } @Override diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java b/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java index 2fd8730d8..ee522a580 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java @@ -20,7 +20,7 @@ import org.argeo.api.cms.CmsSessionId; import org.argeo.cms.internal.auth.CmsSessionImpl; import org.argeo.cms.internal.auth.ImpliedByPrincipal; import org.argeo.cms.internal.runtime.CmsContextImpl; -import org.argeo.util.CurrentSubject; +import org.argeo.cms.util.CurrentSubject; import org.osgi.service.useradmin.Authorization; /** diff --git a/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java index af559df75..ebab12f2c 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java @@ -14,9 +14,9 @@ import javax.security.auth.callback.PasswordCallback; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; -import org.argeo.cms.security.PBEKeySpecCallback; -import org.argeo.util.CurrentSubject; -import org.argeo.util.PasswordEncryption; +import org.argeo.api.cms.keyring.PBEKeySpecCallback; +import org.argeo.cms.util.CurrentSubject; +import org.argeo.cms.util.PasswordEncryption; /** Adds a secret key to the private credentials */ public class KeyringLoginModule implements LoginModule { diff --git a/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java index 785eeb912..4a8f18fcd 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java @@ -12,10 +12,10 @@ import javax.security.auth.login.LoginException; import org.argeo.api.cms.CmsAuth; import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsSession; +import org.argeo.cms.http.HttpHeader; +import org.argeo.cms.http.HttpStatus; import org.argeo.cms.internal.runtime.CmsContextImpl; -import org.argeo.util.CurrentSubject; -import org.argeo.util.http.HttpHeader; -import org.argeo.util.http.HttpStatus; +import org.argeo.cms.util.CurrentSubject; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; diff --git a/org.argeo.cms/src/org/argeo/cms/auth/RemoteSessionLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/RemoteSessionLoginModule.java index 772531ede..987c3dd19 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/RemoteSessionLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/RemoteSessionLoginModule.java @@ -16,10 +16,10 @@ import javax.security.auth.spi.LoginModule; import org.argeo.api.cms.CmsLog; import org.argeo.cms.CmsDeployProperty; +import org.argeo.cms.http.HttpHeader; import org.argeo.cms.internal.auth.CmsSessionImpl; import org.argeo.cms.internal.runtime.CmsContextImpl; import org.argeo.cms.internal.runtime.CmsStateImpl; -import org.argeo.util.http.HttpHeader; import org.osgi.service.useradmin.Authorization; /** Use a remote session as the basis for authentication. */ diff --git a/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java index 35b7d5cc7..23fdb6dee 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java @@ -11,10 +11,10 @@ import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; import javax.security.auth.x500.X500Principal; +import org.argeo.api.acr.ldap.LdapAttrs; +import org.argeo.cms.directory.ldap.IpaUtils; import org.argeo.cms.internal.runtime.CmsContextImpl; -import org.argeo.osgi.useradmin.OsUserUtils; -import org.argeo.util.directory.ldap.IpaUtils; -import org.argeo.util.naming.LdapAttrs; +import org.argeo.cms.osgi.useradmin.OsUserUtils; import org.osgi.service.useradmin.Authorization; /** Login module for when the system is owned by a single user. */ diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java index 0ae84ff8a..278321c24 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java @@ -1,6 +1,6 @@ package org.argeo.cms.auth; -import static org.argeo.util.naming.LdapAttrs.cn; +import static org.argeo.api.acr.ldap.LdapAttrs.cn; import java.io.IOException; import java.security.PrivilegedAction; @@ -24,13 +24,13 @@ import javax.security.auth.login.CredentialNotFoundException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; +import org.argeo.api.acr.ldap.LdapAttrs; import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; +import org.argeo.cms.directory.ldap.IpaUtils; import org.argeo.cms.internal.runtime.CmsContextImpl; -import org.argeo.osgi.useradmin.AuthenticatingUser; -import org.argeo.osgi.useradmin.TokenUtils; -import org.argeo.util.directory.ldap.IpaUtils; -import org.argeo.util.naming.LdapAttrs; +import org.argeo.cms.osgi.useradmin.AuthenticatingUser; +import org.argeo.cms.osgi.useradmin.TokenUtils; import org.osgi.service.useradmin.Authorization; import org.osgi.service.useradmin.Group; import org.osgi.service.useradmin.User; diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminUtils.java index e3eb44249..47d2eeb41 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminUtils.java @@ -6,8 +6,8 @@ import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; +import org.argeo.api.acr.ldap.LdapAttrs; import org.argeo.api.cms.CmsConstants; -import org.argeo.util.naming.LdapAttrs; import org.osgi.service.useradmin.Role; import org.osgi.service.useradmin.User; import org.osgi.service.useradmin.UserAdmin; diff --git a/org.argeo.cms/src/org/argeo/cms/client/CmsClient.java b/org.argeo.cms/src/org/argeo/cms/client/CmsClient.java index af0656097..8cfae3a1b 100644 --- a/org.argeo.cms/src/org/argeo/cms/client/CmsClient.java +++ b/org.argeo.cms/src/org/argeo/cms/client/CmsClient.java @@ -30,7 +30,7 @@ import javax.security.auth.login.LoginException; import org.argeo.cms.auth.ConsoleCallbackHandler; import org.argeo.cms.auth.RemoteAuthUtils; -import org.argeo.util.http.HttpHeader; +import org.argeo.cms.http.HttpHeader; /** Utility to connect to a remote CMS node. */ public class CmsClient { diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java b/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java index 94f292c8e..6fe2eb617 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavClient.java @@ -15,9 +15,9 @@ import java.util.Iterator; import javax.xml.namespace.QName; -import org.argeo.util.http.HttpHeader; -import org.argeo.util.http.HttpMethod; -import org.argeo.util.http.HttpStatus; +import org.argeo.cms.http.HttpHeader; +import org.argeo.cms.http.HttpMethod; +import org.argeo.cms.http.HttpStatus; public class DavClient { diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java b/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java index 24695e7b1..c7542b55a 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java @@ -1,6 +1,6 @@ package org.argeo.cms.dav; -import org.argeo.util.http.HttpHeader; +import org.argeo.cms.http.HttpHeader; import com.sun.net.httpserver.HttpExchange; diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java b/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java index 1d6c02623..9fd03f25a 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java @@ -10,10 +10,10 @@ import java.util.function.Consumer; import javax.xml.namespace.NamespaceContext; import org.argeo.api.acr.ContentNotFoundException; -import org.argeo.util.http.HttpHeader; -import org.argeo.util.http.HttpMethod; -import org.argeo.util.http.HttpStatus; -import org.argeo.util.http.HttpServerUtils; +import org.argeo.cms.http.HttpHeader; +import org.argeo.cms.http.HttpMethod; +import org.argeo.cms.http.HttpServerUtils; +import org.argeo.cms.http.HttpStatus; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; diff --git a/org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java b/org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java index 828dc2640..8dd6bf3fc 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java @@ -10,7 +10,7 @@ import java.util.TreeSet; import javax.xml.namespace.QName; -import org.argeo.util.http.HttpStatus; +import org.argeo.cms.http.HttpStatus; /** The WebDav response for a given resource. */ public class DavResponse { diff --git a/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java index 6d22c8e29..c7b54b027 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java @@ -19,7 +19,7 @@ import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; -import org.argeo.util.http.HttpStatus; +import org.argeo.cms.http.HttpStatus; /** * Asynchronously iterate over the response statuses of the response to a diff --git a/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java index 986b2fe92..4689b8c8d 100644 --- a/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java +++ b/org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java @@ -20,7 +20,7 @@ import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; -import org.argeo.util.http.HttpStatus; +import org.argeo.cms.http.HttpStatus; class MultiStatusWriter implements Consumer { private BlockingQueue queue = new ArrayBlockingQueue<>(64); diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectory.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectory.java new file mode 100644 index 000000000..06c33b011 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectory.java @@ -0,0 +1,572 @@ +package org.argeo.cms.directory.ldap; + +import static org.argeo.cms.directory.ldap.LdapNameUtils.toLdapName; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.StringJoiner; + +import javax.naming.Context; +import javax.naming.InvalidNameException; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.transaction.xa.XAResource; + +import org.argeo.api.acr.ldap.LdapAttrs; +import org.argeo.api.acr.ldap.LdapObjs; +import org.argeo.api.cms.directory.Directory; +import org.argeo.api.cms.directory.HierarchyUnit; +import org.argeo.api.cms.transaction.WorkControl; +import org.argeo.api.cms.transaction.WorkingCopyXaResource; +import org.argeo.api.cms.transaction.XAResourceProvider; +import org.argeo.cms.osgi.useradmin.OsUserDirectory; +import org.argeo.cms.runtime.DirectoryConf; + +/** A {@link Directory} based either on LDAP or LDIF. */ +public abstract class AbstractLdapDirectory implements Directory, XAResourceProvider { + protected static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name"; + protected static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password"; + + private final LdapName baseDn; + private final Hashtable configProperties; + private final Rdn userBaseRdn, groupBaseRdn, systemRoleBaseRdn; + private final String userObjectClass, groupObjectClass; + private String memberAttributeId = "member"; + + private final boolean readOnly; + private final boolean disabled; + private final String uri; + + private String forcedPassword; + + private final boolean scoped; + + private List credentialAttributeIds = Arrays + .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() }); + + private WorkControl transactionControl; + private WorkingCopyXaResource xaResource; + + private LdapDirectoryDao directoryDao; + + /** Whether the the directory has is authenticated via a service user. */ + private boolean authenticated = false; + + public AbstractLdapDirectory(URI uriArg, Dictionary props, boolean scoped) { + this.configProperties = new Hashtable(); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + configProperties.put(key, props.get(key)); + } + + String baseDnStr = DirectoryConf.baseDn.getValue(configProperties); + if (baseDnStr == null) + throw new IllegalArgumentException("Base DN must be specified: " + configProperties); + baseDn = toLdapName(baseDnStr); + this.scoped = scoped; + + if (uriArg != null) { + uri = uriArg.toString(); + // uri from properties is ignored + } else { + String uriStr = DirectoryConf.uri.getValue(configProperties); + if (uriStr == null) + uri = null; + else + uri = uriStr; + } + + forcedPassword = DirectoryConf.forcedPassword.getValue(configProperties); + + userObjectClass = DirectoryConf.userObjectClass.getValue(configProperties); + groupObjectClass = DirectoryConf.groupObjectClass.getValue(configProperties); + + String userBase = DirectoryConf.userBase.getValue(configProperties); + String groupBase = DirectoryConf.groupBase.getValue(configProperties); + String systemRoleBase = DirectoryConf.systemRoleBase.getValue(configProperties); + try { +// baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties)); + userBaseRdn = new Rdn(userBase); +// userBaseDn = new LdapName(userBase + "," + baseDn); + groupBaseRdn = new Rdn(groupBase); +// groupBaseDn = new LdapName(groupBase + "," + baseDn); + systemRoleBaseRdn = new Rdn(systemRoleBase); + } catch (InvalidNameException e) { + throw new IllegalArgumentException( + "Badly formated base DN " + DirectoryConf.baseDn.getValue(configProperties), e); + } + + // read only + String readOnlyStr = DirectoryConf.readOnly.getValue(configProperties); + if (readOnlyStr == null) { + readOnly = readOnlyDefault(uri); + configProperties.put(DirectoryConf.readOnly.name(), Boolean.toString(readOnly)); + } else + readOnly = Boolean.parseBoolean(readOnlyStr); + + // disabled + String disabledStr = DirectoryConf.disabled.getValue(configProperties); + if (disabledStr != null) + disabled = Boolean.parseBoolean(disabledStr); + else + disabled = false; + if (!getRealm().isEmpty()) { + // IPA multiple LDAP causes URI parsing to fail + // TODO manage generic redundant LDAP case + directoryDao = new LdapDao(this); + } else { + if (uri != null) { + URI u = URI.create(uri); + if (DirectoryConf.SCHEME_LDAP.equals(u.getScheme()) + || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) { + directoryDao = new LdapDao(this); + authenticated = configProperties.get(Context.SECURITY_PRINCIPAL) != null; + } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) { + directoryDao = new LdifDao(this); + authenticated = true; + } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) { + directoryDao = new OsUserDirectory(this); + authenticated = true; + // singleUser = true; + } else { + throw new IllegalArgumentException("Unsupported scheme " + u.getScheme()); + } + } else { + // in memory + directoryDao = new LdifDao(this); + } + } + if (directoryDao != null) + xaResource = new WorkingCopyXaResource<>(directoryDao); + } + + /* + * INITIALISATION + */ + + public void init() { + getDirectoryDao().init(); + } + + public void destroy() { + getDirectoryDao().destroy(); + } + + /* + * CREATION + */ + protected abstract LdapEntry newUser(LdapName name); + + protected abstract LdapEntry newGroup(LdapName name); + + /* + * EDITION + */ + + public boolean isEditing() { + return xaResource.wc() != null; + } + + public LdapEntryWorkingCopy getWorkingCopy() { + LdapEntryWorkingCopy wc = xaResource.wc(); + if (wc == null) + return null; + return wc; + } + + public void checkEdit() { + if (xaResource.wc() == null) { + try { + transactionControl.getWorkContext().registerXAResource(xaResource, null); + } catch (Exception e) { + throw new IllegalStateException("Cannot enlist " + xaResource, e); + } + } else { + } + } + + public void setTransactionControl(WorkControl transactionControl) { + this.transactionControl = transactionControl; + } + + public XAResource getXaResource() { + return xaResource; + } + + public boolean removeEntry(LdapName dn) { + checkEdit(); + LdapEntryWorkingCopy wc = getWorkingCopy(); + boolean actuallyDeleted; + if (getDirectoryDao().entryExists(dn) || wc.getNewData().containsKey(dn)) { + LdapEntry user = doGetRole(dn); + wc.getDeletedData().put(dn, user); + actuallyDeleted = true; + } else {// just removing from groups (e.g. system roles) + actuallyDeleted = false; + } + for (LdapName groupDn : getDirectoryDao().getDirectGroups(dn)) { + LdapEntry group = doGetRole(groupDn); + group.getAttributes().get(getMemberAttributeId()).remove(dn.toString()); + } + return actuallyDeleted; + } + + /* + * RETRIEVAL + */ + + protected LdapEntry doGetRole(LdapName dn) { + LdapEntryWorkingCopy wc = getWorkingCopy(); + LdapEntry user; + try { + user = getDirectoryDao().doGetEntry(dn); + } catch (NameNotFoundException e) { + user = null; + } + if (wc != null) { + if (user == null && wc.getNewData().containsKey(dn)) + user = wc.getNewData().get(dn); + else if (wc.getDeletedData().containsKey(dn)) + user = null; + } + return user; + } + + protected void collectGroups(LdapEntry user, List allRoles) { + Attributes attrs = user.getAttributes(); + // TODO centralize attribute name + Attribute memberOf = attrs.get(LdapAttrs.memberOf.name()); + // if user belongs to this directory, we only check memberOf + if (memberOf != null && user.getDn().startsWith(getBaseDn())) { + try { + NamingEnumeration values = memberOf.getAll(); + while (values.hasMore()) { + Object value = values.next(); + LdapName groupDn = new LdapName(value.toString()); + LdapEntry group = doGetRole(groupDn); + if (group != null) { + allRoles.add(group); + } else { + // user doesn't have the right to retrieve role, but we know it exists + // otherwise memberOf would not work + group = newGroup(groupDn); + allRoles.add(group); + } + } + } catch (NamingException e) { + throw new IllegalStateException("Cannot get memberOf groups for " + user, e); + } + } else { + directGroups: for (LdapName groupDn : getDirectoryDao().getDirectGroups(user.getDn())) { + LdapEntry group = doGetRole(groupDn); + if (group != null) { + if (allRoles.contains(group)) { + // important in order to avoi loops + continue directGroups; + } + allRoles.add(group); + collectGroups(group, allRoles); + } + } + } + } + + /* + * HIERARCHY + */ + @Override + public HierarchyUnit getHierarchyUnit(String path) { + LdapName dn = pathToName(path); + return directoryDao.doGetHierarchyUnit(dn); + } + + @Override + public Iterable getDirectHierarchyUnits(boolean functionalOnly) { + return directoryDao.doGetDirectHierarchyUnits(baseDn, functionalOnly); + } + + @Override + public String getHierarchyUnitName() { + return getName(); + } + + @Override + public String getHierarchyUnitLabel(Locale locale) { + String key = LdapNameUtils.getLastRdn(getBaseDn()).getType(); + Object value = LdapEntry.getLocalized(asLdapEntry().getProperties(), key, locale); + if (value == null) + value = getHierarchyUnitName(); + assert value != null; + return value.toString(); + } + + @Override + public HierarchyUnit getParent() { + return null; + } + + @Override + public boolean isFunctional() { + return true; + } + + @Override + public Directory getDirectory() { + return this; + } + + @Override + public HierarchyUnit createHierarchyUnit(String path) { + checkEdit(); + LdapEntryWorkingCopy wc = getWorkingCopy(); + LdapName dn = pathToName(path); + if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn)) + || wc.getNewData().containsKey(dn)) + throw new IllegalArgumentException("Already a hierarchy unit " + path); + BasicAttributes attrs = new BasicAttributes(true); + attrs.put(LdapAttrs.objectClass.name(), LdapObjs.organizationalUnit.name()); + Rdn nameRdn = dn.getRdn(dn.size() - 1); + // TODO deal with multiple attr RDN + attrs.put(nameRdn.getType(), nameRdn.getValue()); + wc.getModifiedData().put(dn, attrs); + LdapHierarchyUnit newHierarchyUnit = new LdapHierarchyUnit(this, dn); + wc.getNewData().put(dn, newHierarchyUnit); + return newHierarchyUnit; + } + + /* + * PATHS + */ + + @Override + public String getBase() { + return getBaseDn().toString(); + } + + @Override + public String getName() { + return nameToSimple(getBaseDn(), "."); + } + + protected String nameToRelativePath(LdapName dn) { + LdapName name = LdapNameUtils.relativeName(getBaseDn(), dn); + return nameToSimple(name, "/"); + } + + protected String nameToSimple(LdapName name, String separator) { + StringJoiner path = new StringJoiner(separator); + for (int i = 0; i < name.size(); i++) { + path.add(name.getRdn(i).getValue().toString()); + } + return path.toString(); + + } + + protected LdapName pathToName(String path) { + try { + LdapName name = (LdapName) getBaseDn().clone(); + String[] segments = path.split("/"); + Rdn parentRdn = null; + // segments[0] is the directory itself + for (int i = 0; i < segments.length; i++) { + String segment = segments[i]; + // TODO make attr names configurable ? + String attr = getDirectory().getRealm().isPresent()/* IPA */ ? LdapAttrs.cn.name() + : LdapAttrs.ou.name(); + if (parentRdn != null) { + if (getUserBaseRdn().equals(parentRdn)) + attr = LdapAttrs.uid.name(); + else if (getGroupBaseRdn().equals(parentRdn)) + attr = LdapAttrs.cn.name(); + else if (getSystemRoleBaseRdn().equals(parentRdn)) + attr = LdapAttrs.cn.name(); + } + Rdn rdn = new Rdn(attr, segment); + name.add(rdn); + parentRdn = rdn; + } + return name; + } catch (InvalidNameException e) { + throw new IllegalStateException("Cannot convert " + path + " to LDAP name", e); + } + + } + + /* + * UTILITIES + */ + protected boolean isExternal(LdapName name) { + return !name.startsWith(baseDn); + } + + protected static boolean hasObjectClass(Attributes attrs, LdapObjs objectClass) { + return hasObjectClass(attrs, objectClass.name()); + } + + protected static boolean hasObjectClass(Attributes attrs, String objectClass) { + try { + Attribute attr = attrs.get(LdapAttrs.objectClass.name()); + NamingEnumeration en = attr.getAll(); + while (en.hasMore()) { + String v = en.next().toString(); + if (v.equalsIgnoreCase(objectClass)) + return true; + + } + return false; + } catch (NamingException e) { + throw new IllegalStateException("Cannot search for objectClass " + objectClass, e); + } + } + + private static boolean readOnlyDefault(String uriStr) { + if (uriStr == null) + return true; + /// TODO make it more generic + URI uri; + try { + uri = new URI(uriStr.split(" ")[0]); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + if (uri.getScheme() == null) + return false;// assume relative file to be writable + if (uri.getScheme().equals(DirectoryConf.SCHEME_FILE)) { + File file = new File(uri); + if (file.exists()) + return !file.canWrite(); + else + return !file.getParentFile().canWrite(); + } else if (uri.getScheme().equals(DirectoryConf.SCHEME_LDAP)) { + if (uri.getAuthority() != null)// assume writable if authenticated + return false; + } else if (uri.getScheme().equals(DirectoryConf.SCHEME_OS)) { + return true; + } + return true;// read only by default + } + + /* + * AS AN ENTRY + */ + public LdapEntry asLdapEntry() { + try { + return directoryDao.doGetEntry(baseDn); + } catch (NameNotFoundException e) { + throw new IllegalStateException("Cannot get " + baseDn + " entry", e); + } + } + + public Dictionary getProperties() { + return asLdapEntry().getProperties(); + } + + /* + * ACCESSORS + */ + @Override + public Optional getRealm() { + Object realm = configProperties.get(DirectoryConf.realm.name()); + if (realm == null) + return Optional.empty(); + return Optional.of(realm.toString()); + } + + public LdapName getBaseDn() { + return (LdapName) baseDn.clone(); + } + + public boolean isReadOnly() { + return readOnly; + } + + public boolean isDisabled() { + return disabled; + } + + public boolean isAuthenticated() { + return authenticated; + } + + public Rdn getUserBaseRdn() { + return userBaseRdn; + } + + public Rdn getGroupBaseRdn() { + return groupBaseRdn; + } + + public Rdn getSystemRoleBaseRdn() { + return systemRoleBaseRdn; + } + +// public Dictionary getConfigProperties() { +// return configProperties; +// } + + public Dictionary cloneConfigProperties() { + return new Hashtable<>(configProperties); + } + + public String getForcedPassword() { + return forcedPassword; + } + + public boolean isScoped() { + return scoped; + } + + public List getCredentialAttributeIds() { + return credentialAttributeIds; + } + + public String getUri() { + return uri; + } + + public LdapDirectoryDao getDirectoryDao() { + return directoryDao; + } + + /** dn can be null, in that case a default should be returned. */ + public String getUserObjectClass() { + return userObjectClass; + } + + public String getGroupObjectClass() { + return groupObjectClass; + } + + public String getMemberAttributeId() { + return memberAttributeId; + } + + /* + * OBJECT METHODS + */ + + @Override + public int hashCode() { + return baseDn.hashCode(); + } + + @Override + public String toString() { + return "Directory " + baseDn.toString(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectoryDao.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectoryDao.java new file mode 100644 index 000000000..c4a691032 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectoryDao.java @@ -0,0 +1,33 @@ +package org.argeo.cms.directory.ldap; + +import javax.naming.ldap.LdapName; + +/** Base class for LDAP/LDIF directory DAOs. */ +public abstract class AbstractLdapDirectoryDao implements LdapDirectoryDao { + + private AbstractLdapDirectory directory; + + public AbstractLdapDirectoryDao(AbstractLdapDirectory directory) { + this.directory = directory; + } + + public AbstractLdapDirectory getDirectory() { + return directory; + } + + @Override + public LdapEntryWorkingCopy newWorkingCopy() { + return new LdapEntryWorkingCopy(); + } + + @Override + public LdapEntry newUser(LdapName name) { + return getDirectory().newUser(name); + } + + @Override + public LdapEntry newGroup(LdapName name) { + return getDirectory().newGroup(name); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/AttributesDictionary.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AttributesDictionary.java new file mode 100644 index 000000000..9deda1be4 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AttributesDictionary.java @@ -0,0 +1,171 @@ +package org.argeo.cms.directory.ldap; + +import java.util.Dictionary; +import java.util.Enumeration; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; + +public class AttributesDictionary extends Dictionary { + private final Attributes attributes; + + /** The provided attributes is wrapped, not copied. */ + public AttributesDictionary(Attributes attributes) { + if (attributes == null) + throw new IllegalArgumentException("Attributes cannot be null"); + this.attributes = attributes; + } + + @Override + public int size() { + return attributes.size(); + } + + @Override + public boolean isEmpty() { + return attributes.size() == 0; + } + + @Override + public Enumeration keys() { + NamingEnumeration namingEnumeration = attributes.getIDs(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return namingEnumeration.hasMoreElements(); + } + + @Override + public String nextElement() { + return namingEnumeration.nextElement(); + } + + }; + } + + @Override + public Enumeration elements() { + NamingEnumeration namingEnumeration = attributes.getIDs(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return namingEnumeration.hasMoreElements(); + } + + @Override + public Object nextElement() { + String key = namingEnumeration.nextElement(); + return get(key); + } + + }; + } + + @Override + /** @returns a String or String[] */ + public Object get(Object key) { + try { + if (key == null) + throw new IllegalArgumentException("Key cannot be null"); + Attribute attr = attributes.get(key.toString()); + if (attr == null) + return null; + if (attr.size() == 0) + throw new IllegalStateException("There must be at least one value"); + else if (attr.size() == 1) { + return attr.get().toString(); + } else {// multiple + String[] res = new String[attr.size()]; + for (int i = 0; i < attr.size(); i++) { + Object value = attr.get(); + if (value == null) + throw new RuntimeException("Values cannot be null"); + res[i] = attr.get(i).toString(); + } + return res; + } + } catch (NamingException e) { + throw new RuntimeException("Cannot get value for " + key, e); + } + } + + @Override + public Object put(String key, Object value) { + if (key == null) + throw new IllegalArgumentException("Key cannot be null"); + if (value == null) + throw new IllegalArgumentException("Value cannot be null"); + + Object oldValue = get(key); + Attribute attr = attributes.get(key); + if (attr == null) { + attr = new BasicAttribute(key); + attributes.put(attr); + } + + if (value instanceof String[]) { + String[] values = (String[]) value; + // clean additional values + for (int i = values.length; i < attr.size(); i++) + attr.remove(i); + // set values + for (int i = 0; i < values.length; i++) { + attr.set(i, values[i]); + } + } else { + if (attr.size() > 1) + throw new IllegalArgumentException("Attribute " + key + " is multi-valued"); + if (attr.size() == 1) { + try { + if (!attr.get(0).equals(value)) + attr.set(0, value.toString()); + } catch (NamingException e) { + throw new RuntimeException("Cannot check existing value", e); + } + } else { + attr.add(value.toString()); + } + } + return oldValue; + } + + @Override + public Object remove(Object key) { + if (key == null) + throw new IllegalArgumentException("Key cannot be null"); + Object oldValue = get(key); + if (oldValue == null) + return null; + return attributes.remove(key.toString()); + } + + /** + * Copy the content of an {@link Attributes} to the provided + * {@link Dictionary}. + */ + public static void copy(Attributes attributes, Dictionary dictionary) { + AttributesDictionary ad = new AttributesDictionary(attributes); + Enumeration keys = ad.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + dictionary.put(key, ad.get(key)); + } + } + + /** + * Copy a {@link Dictionary} into an {@link Attributes}. + */ + public static void copy(Dictionary dictionary, Attributes attributes) { + AttributesDictionary ad = new AttributesDictionary(attributes); + Enumeration keys = dictionary.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + ad.put(key, dictionary.get(key)); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/AuthPassword.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AuthPassword.java new file mode 100644 index 000000000..f2332dbcf --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AuthPassword.java @@ -0,0 +1,140 @@ +package org.argeo.cms.directory.ldap; + +import java.io.IOException; +import java.util.Arrays; +import java.util.StringTokenizer; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +import org.argeo.api.acr.ldap.LdapAttrs; + +/** LDAP authPassword field according to RFC 3112 */ +public class AuthPassword implements CallbackHandler { + private final String authScheme; + private final String authInfo; + private final String authValue; + + public AuthPassword(String value) { + StringTokenizer st = new StringTokenizer(value, "$"); + // TODO make it more robust, deal with bad formatting + this.authScheme = st.nextToken().trim(); + this.authInfo = st.nextToken().trim(); + this.authValue = st.nextToken().trim(); + + String expectedAuthScheme = getExpectedAuthScheme(); + if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme)) + throw new IllegalArgumentException( + "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme); + } + + protected AuthPassword(String authInfo, String authValue) { + this.authScheme = getExpectedAuthScheme(); + if (authScheme == null) + throw new IllegalArgumentException("Expected auth scheme cannot be null"); + this.authInfo = authInfo; + this.authValue = authValue; + } + + protected AuthPassword(AuthPassword authPassword) { + this.authScheme = authPassword.getAuthScheme(); + this.authInfo = authPassword.getAuthInfo(); + this.authValue = authPassword.getAuthValue(); + } + + protected String getExpectedAuthScheme() { + return null; + } + + protected boolean matchAuthValue(Object object) { + return authValue.equals(object.toString()); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AuthPassword)) + return false; + AuthPassword authPassword = (AuthPassword) obj; + return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo) + && authValue.equals(authValue); + } + + public boolean keyEquals(AuthPassword authPassword) { + return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo); + } + + @Override + public int hashCode() { + return authValue.hashCode(); + } + + @Override + public String toString() { + return toAuthPassword(); + } + + public final String toAuthPassword() { + return getAuthScheme() + '$' + authInfo + '$' + authValue; + } + + public String getAuthScheme() { + return authScheme; + } + + public String getAuthInfo() { + return authInfo; + } + + public String getAuthValue() { + return authValue; + } + + public static AuthPassword matchAuthValue(Attributes attributes, char[] value) { + try { + Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); + if (authPassword != null) { + NamingEnumeration values = authPassword.getAll(); + while (values.hasMore()) { + Object val = values.next(); + AuthPassword token = new AuthPassword(val.toString()); + String auth; + if (Arrays.binarySearch(value, '$') >= 0) { + auth = token.authInfo + '$' + token.authValue; + } else { + auth = token.authValue; + } + if (Arrays.equals(auth.toCharArray(), value)) + return token; + // if (token.matchAuthValue(value)) + // return token; + } + } + return null; + } catch (NamingException e) { + throw new IllegalStateException("Cannot check attribute", e); + } + } + + public static boolean remove(Attributes attributes, AuthPassword value) { + Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); + return authPassword.remove(value.toAuthPassword()); + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) + ((NameCallback) callback).setName(toAuthPassword()); + else if (callback instanceof PasswordCallback) + ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray()); + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/DefaultLdapEntry.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/DefaultLdapEntry.java new file mode 100644 index 000000000..ad70d676e --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/DefaultLdapEntry.java @@ -0,0 +1,481 @@ +package org.argeo.cms.directory.ldap; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.ldap.LdapName; + +import org.argeo.api.acr.ldap.LdapAttrs; +import org.argeo.api.acr.ldap.LdapObjs; +import org.argeo.api.cms.directory.DirectoryDigestUtils; + +/** An entry in an LDAP (or LDIF) directory. */ +public class DefaultLdapEntry implements LdapEntry { + private final AbstractLdapDirectory directory; + + private final LdapName dn; + + private AttributeDictionary properties; + private AttributeDictionary credentials; + +// private String primaryObjectClass; +// private List objectClasses = new ArrayList<>(); + + protected DefaultLdapEntry(AbstractLdapDirectory directory, LdapName dn) { + Objects.requireNonNull(directory); + Objects.requireNonNull(dn); + this.directory = directory; + this.dn = dn; + + // Object classes +// Objects.requireNonNull(initialAttributes); +// try { +// NamingEnumeration en = initialAttributes.get(LdapAttrs.objectClass.name()).getAll(); +// String first = null; +// attrs: while (en.hasMore()) { +// String v = en.next().toString(); +// if (v.equalsIgnoreCase(LdapObjs.top.name())) +// continue attrs; +// if (first == null) +// first = v; +// if (v.equalsIgnoreCase(getDirectory().getUserObjectClass())) +// primaryObjectClass = getDirectory().getUserObjectClass(); +// else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass())) +// primaryObjectClass = getDirectory().getGroupObjectClass(); +// objectClasses.add(v); +// } +// if (primaryObjectClass == null) { +// if (first == null) +// throw new IllegalStateException("Could not find primary object class"); +// primaryObjectClass = first; +// } +// } catch (NamingException e) { +// throw new IllegalStateException("Cannot find object classes", e); +// } + + } + + @Override + public LdapName getDn() { + return dn; + } + + public synchronized Attributes getAttributes() { + return isEditing() ? getModifiedAttributes() : getDirectory().getDirectoryDao().doGetAttributes(dn); + } + + @Override + public List getReferences(String attributeId) { + Attribute memberAttribute = getAttributes().get(attributeId); + if (memberAttribute == null) + return new ArrayList(); + try { + List roles = new ArrayList(); + NamingEnumeration values = memberAttribute.getAll(); + while (values.hasMore()) { + LdapName dn = new LdapName(values.next().toString()); + roles.add(dn); + } + return roles; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get members", e); + } + + } + + /** Should only be called from working copy thread. */ + protected synchronized Attributes getModifiedAttributes() { + assert getWc() != null; + return getWc().getModifiedData().get(getDn()); + } + + protected synchronized boolean isEditing() { + return getWc() != null && getModifiedAttributes() != null; + } + + private synchronized LdapEntryWorkingCopy getWc() { + return directory.getWorkingCopy(); + } + + protected synchronized void startEditing() { +// if (frozen) +// throw new IllegalStateException("Cannot edit frozen view"); + if (directory.isReadOnly()) + throw new IllegalStateException("User directory is read-only"); + assert getModifiedAttributes() == null; + getWc().startEditing(this); + // modifiedAttributes = (Attributes) publishedAttributes.clone(); + } + + public synchronized void publishAttributes(Attributes modifiedAttributes) { +// publishedAttributes = modifiedAttributes; + } + + /* + * PROPERTIES + */ + @Override + public Dictionary getProperties() { + if (properties == null) + properties = new AttributeDictionary(false); + return properties; + } + + public Dictionary getCredentials() { + if (credentials == null) + credentials = new AttributeDictionary(true); + return credentials; + } + + /* + * CREDENTIALS + */ + @Override + public boolean hasCredential(String key, Object value) { + if (key == null) { + // TODO check other sources (like PKCS12) + // String pwd = new String((char[]) value); + // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112) + char[] password = DirectoryDigestUtils.bytesToChars(value); + + if (getDirectory().getForcedPassword() != null + && getDirectory().getForcedPassword().equals(new String(password))) + return true; + + AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password); + if (authPassword != null) { + if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) { + SharedSecret onceToken = new SharedSecret(authPassword); + if (onceToken.isExpired()) { + // AuthPassword.remove(getAttributes(), onceToken); + return false; + } else { + // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken); + return true; + } + // TODO delete expired tokens? + } else { + // TODO implement SHA + throw new UnsupportedOperationException( + "Unsupported authPassword scheme " + authPassword.getAuthScheme()); + } + } + + // Regular password +// byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256); + if (hasCredential(LdapAttrs.userPassword.name(), DirectoryDigestUtils.charsToBytes(password))) + return true; + return false; + } + + Object storedValue = getCredentials().get(key); + if (storedValue == null || value == null) + return false; + if (!(value instanceof String || value instanceof byte[])) + return false; + if (storedValue instanceof String && value instanceof String) + return storedValue.equals(value); + if (storedValue instanceof byte[] && value instanceof byte[]) { + String storedBase64 = new String((byte[]) storedValue, US_ASCII); + String passwordScheme = null; + if (storedBase64.charAt(0) == '{') { + int index = storedBase64.indexOf('}'); + if (index > 0) { + passwordScheme = storedBase64.substring(1, index); + String storedValueBase64 = storedBase64.substring(index + 1); + byte[] storedValueBytes = Base64.getDecoder().decode(storedValueBase64); + char[] passwordValue = DirectoryDigestUtils.bytesToChars((byte[]) value); + byte[] valueBytes; + if (DirectoryDigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) { + valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, + null); + } else if (DirectoryDigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { + // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/ + byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4); + BigInteger iterations = new BigInteger(iterationsArr); + byte[] salt = Arrays.copyOfRange(storedValueBytes, iterationsArr.length, + iterationsArr.length + 64); + byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length, + storedValueBytes.length); + int keyLengthBits = keyArr.length * 8; + valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt, + iterations.intValue(), keyLengthBits); + } else { + throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme); + } + return Arrays.equals(storedValueBytes, valueBytes); + } + } + } +// if (storedValue instanceof byte[] && value instanceof byte[]) { +// return Arrays.equals((byte[]) storedValue, (byte[]) value); +// } + return false; + } + + /** Hash the password */ + private static byte[] sha1hash(char[] password) { + byte[] hashedPassword = ("{SHA}" + Base64.getEncoder() + .encodeToString(DirectoryDigestUtils.sha1(DirectoryDigestUtils.charsToBytes(password)))) + .getBytes(StandardCharsets.UTF_8); + return hashedPassword; + } + + public AbstractLdapDirectory getDirectory() { + return directory; + } + + public LdapDirectoryDao getDirectoryDao() { + return directory.getDirectoryDao(); + } + + @Override + public int hashCode() { + return dn.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof LdapEntry) { + LdapEntry that = (LdapEntry) obj; + return this.dn.equals(that.getDn()); + } + return false; + } + + @Override + public String toString() { + return dn.toString(); + } + + private static boolean isAsciiPrintable(String str) { + if (str == null) { + return false; + } + int sz = str.length(); + for (int i = 0; i < sz; i++) { + if (isAsciiPrintable(str.charAt(i)) == false) { + return false; + } + } + return true; + } + + private static boolean isAsciiPrintable(char ch) { + return ch >= 32 && ch < 127; + } + + protected class AttributeDictionary extends Dictionary { + private final List effectiveKeys = new ArrayList(); + private final List attrFilter; + private final Boolean includeFilter; + + public AttributeDictionary(Boolean credentials) { + this.attrFilter = getDirectory().getCredentialAttributeIds(); + this.includeFilter = credentials; + try { + NamingEnumeration ids = getAttributes().getIDs(); + while (ids.hasMore()) { + String id = ids.next(); + if (credentials && attrFilter.contains(id)) + effectiveKeys.add(id); + else if (!credentials && !attrFilter.contains(id)) + effectiveKeys.add(id); + } + } catch (NamingException e) { + throw new IllegalStateException("Cannot initialise attribute dictionary", e); + } + if (!credentials) + effectiveKeys.add(LdapAttrs.objectClasses.name()); + } + + @Override + public int size() { + return effectiveKeys.size(); + } + + @Override + public boolean isEmpty() { + return effectiveKeys.size() == 0; + } + + @Override + public Enumeration keys() { + return Collections.enumeration(effectiveKeys); + } + + @Override + public Enumeration elements() { + final Iterator it = effectiveKeys.iterator(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public Object nextElement() { + String key = it.next(); + return get(key); + } + + }; + } + + @Override + public Object get(Object key) { + try { + Attribute attr = !key.equals(LdapAttrs.objectClasses.name()) ? getAttributes().get(key.toString()) + : getAttributes().get(LdapAttrs.objectClass.name()); + if (attr == null) + return null; + Object value = attr.get(); + if (value instanceof byte[]) { + if (key.equals(LdapAttrs.userPassword.name())) + // TODO other cases (certificates, images) + return value; + value = new String((byte[]) value, StandardCharsets.UTF_8); + } + if (attr.size() == 1) + return value; + // special case for object class + if (key.equals(LdapAttrs.objectClass.name())) { + // TODO support multiple object classes + NamingEnumeration en = attr.getAll(); + String first = null; + attrs: while (en.hasMore()) { + String v = en.next().toString(); + if (v.equalsIgnoreCase(LdapObjs.top.name())) + continue attrs; + if (first == null) + first = v; + if (v.equalsIgnoreCase(getDirectory().getUserObjectClass())) + return getDirectory().getUserObjectClass(); + else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass())) + return getDirectory().getGroupObjectClass(); + } + if (first != null) + return first; + throw new IllegalStateException("Cannot find objectClass in " + value); + } else { + NamingEnumeration en = attr.getAll(); + StringJoiner values = new StringJoiner("\n"); + while (en.hasMore()) { + String v = en.next().toString(); + values.add(v); + } + return values.toString(); + } +// else +// return value; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get value for attribute " + key, e); + } + } + + @Override + public Object put(String key, Object value) { + Objects.requireNonNull(value, "Value for key " + key + " is null"); + try { + if (key == null) { + // FIXME remove this "feature", a key should be specified + // TODO persist to other sources (like PKCS12) + char[] password = DirectoryDigestUtils.bytesToChars(value); + byte[] hashedPassword = sha1hash(password); + return put(LdapAttrs.userPassword.name(), hashedPassword); + } + if (key.startsWith("X-")) { + return put(LdapAttrs.authPassword.name(), value); + } + + // start editing + getDirectory().checkEdit(); + if (!isEditing()) + startEditing(); + + // object classes special case. + if (key.equals(LdapAttrs.objectClasses.name())) { + Attribute attribute = new BasicAttribute(LdapAttrs.objectClass.name()); + String[] objectClasses = value.toString().split("\n"); + for (String objectClass : objectClasses) { + if (objectClass.trim().equals("")) + continue; + attribute.add(objectClass); + } + Attribute previousAttribute = getModifiedAttributes().put(attribute); + if (previousAttribute != null) + return previousAttribute.get(); + else + return null; + } + + if (!(value instanceof String || value instanceof byte[])) + throw new IllegalArgumentException("Value must be String or byte[]"); + + if (includeFilter && !attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " not included"); + else if (!includeFilter && attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " excluded"); + + Attribute attribute = getModifiedAttributes().get(key.toString()); + // if (attribute == null) // block unit tests + attribute = new BasicAttribute(key.toString()); + if (value instanceof String && !isAsciiPrintable(((String) value))) + attribute.add(((String) value).getBytes(StandardCharsets.UTF_8)); + else + attribute.add(value); + Attribute previousAttribute = getModifiedAttributes().put(attribute); + if (previousAttribute != null) + return previousAttribute.get(); + else + return null; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get value for attribute " + key, e); + } + } + + @Override + public Object remove(Object key) { + getDirectory().checkEdit(); + if (!isEditing()) + startEditing(); + + if (includeFilter && !attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " not included"); + else if (!includeFilter && attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " excluded"); + + try { + Attribute attr = getModifiedAttributes().remove(key.toString()); + if (attr != null) + return attr.get(); + else + return null; + } catch (NamingException e) { + throw new IllegalStateException("Cannot remove attribute " + key, e); + } + } + + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/IpaUtils.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/IpaUtils.java new file mode 100644 index 000000000..a31cdaca1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/IpaUtils.java @@ -0,0 +1,141 @@ +package org.argeo.cms.directory.ldap; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.StringJoiner; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +import org.argeo.api.acr.ldap.LdapAttrs; +import org.argeo.cms.dns.DnsBrowser; +import org.argeo.cms.runtime.DirectoryConf; + +/** Free IPA specific conventions. */ +public class IpaUtils { + public final static String IPA_USER_BASE = "cn=users"; + public final static String IPA_GROUP_BASE = "cn=groups"; + public final static String IPA_ROLE_BASE = "cn=roles"; + public final static String IPA_SERVICE_BASE = "cn=services"; + + public final static String IPA_ACCOUNTS_BASE = "cn=accounts"; + + private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase(); + + public final static String IPA_USER_DIRECTORY_CONFIG = DirectoryConf.userBase + "=" + IPA_USER_BASE + "&" + + DirectoryConf.groupBase + "=" + IPA_GROUP_BASE + "&" + DirectoryConf.systemRoleBase + "=" + IPA_ROLE_BASE + + "&" + DirectoryConf.readOnly + "=true"; + + @Deprecated + static String domainToUserDirectoryConfigPath(String realm) { + return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + DirectoryConf.realm.name() + "=" + realm; + } + + public static void addIpaConfig(String realm, Dictionary properties) { + properties.put(DirectoryConf.baseDn.name(), domainToBaseDn(realm)); + properties.put(DirectoryConf.realm.name(), realm); + properties.put(DirectoryConf.userBase.name(), IPA_USER_BASE); + properties.put(DirectoryConf.groupBase.name(), IPA_GROUP_BASE); + properties.put(DirectoryConf.systemRoleBase.name(), IPA_ROLE_BASE); + properties.put(DirectoryConf.readOnly.name(), Boolean.TRUE.toString()); + } + + public static String domainToBaseDn(String domain) { + String[] dcs = domain.split("\\."); + StringJoiner sj = new StringJoiner(","); + for (int i = 0; i < dcs.length; i++) { + String dc = dcs[i]; + sj.add(LdapAttrs.dc.name() + '=' + dc.toLowerCase()); + } + return IPA_ACCOUNTS_BASE + ',' + sj.toString(); + } + + public static LdapName kerberosToDn(String kerberosName) { + String[] kname = kerberosName.split("@"); + String username = kname[0]; + String baseDn = domainToBaseDn(kname[1]); + String dn; + if (!username.contains("/")) + dn = LdapAttrs.uid + "=" + username + "," + IPA_USER_BASE + "," + baseDn; + else + dn = KRB_PRINCIPAL_NAME + "=" + kerberosName + "," + IPA_SERVICE_BASE + "," + baseDn; + try { + return new LdapName(dn); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Badly formatted name for " + kerberosName + ": " + dn); + } + } + + private IpaUtils() { + + } + + public static String kerberosDomainFromDns() { + String kerberosDomain; + try (DnsBrowser dnsBrowser = new DnsBrowser()) { + InetAddress localhost = InetAddress.getLocalHost(); + String hostname = localhost.getHostName(); + String dnsZone = hostname.substring(hostname.indexOf('.') + 1); + kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT"); + return kerberosDomain; + } catch (IOException e) { + throw new IllegalStateException("Cannot determine Kerberos domain from DNS", e); + } + + } + + public static Dictionary convertIpaUri(URI uri) { + String path = uri.getPath(); + String kerberosRealm; + if (path == null || path.length() <= 1) { + kerberosRealm = kerberosDomainFromDns(); + } else { + kerberosRealm = path.substring(1); + } + + if (kerberosRealm == null) + throw new IllegalStateException("No Kerberos domain available for " + uri); + // TODO intergrate CA certificate in truststore + // String schemeToUse = SCHEME_LDAPS; + String schemeToUse = DirectoryConf.SCHEME_LDAP; + List ldapHosts; + String ldapHostsStr = uri.getHost(); + if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) { + try (DnsBrowser dnsBrowser = new DnsBrowser()) { + ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase(), + schemeToUse.equals(DirectoryConf.SCHEME_LDAP) ? true : false); + if (ldapHosts == null || ldapHosts.size() == 0) { + throw new IllegalStateException("Cannot configure LDAP for IPA " + uri); + } else { + ldapHostsStr = ldapHosts.get(0); + } + } catch (IOException e) { + throw new IllegalStateException("Cannot convert IPA uri " + uri, e); + } + } else { + ldapHosts = new ArrayList<>(); + ldapHosts.add(ldapHostsStr); + } + + StringBuilder uriStr = new StringBuilder(); + try { + for (String host : ldapHosts) { + URI convertedUri = new URI(schemeToUse + "://" + host + "/"); + uriStr.append(convertedUri).append(' '); + } + } catch (URISyntaxException e) { + throw new IllegalStateException("Cannot convert IPA uri " + uri, e); + } + + Hashtable res = new Hashtable<>(); + res.put(DirectoryConf.uri.name(), uriStr.toString()); + addIpaConfig(kerberosRealm, res); + return res; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapConnection.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapConnection.java new file mode 100644 index 000000000..100441cc1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapConnection.java @@ -0,0 +1,162 @@ +package org.argeo.cms.directory.ldap; + +import java.util.Dictionary; +import java.util.Hashtable; + +import javax.naming.CommunicationException; +import javax.naming.Context; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapName; + +import org.argeo.api.acr.ldap.LdapAttrs; +import org.argeo.api.cms.transaction.WorkingCopy; + +/** A synchronized wrapper for a single {@link InitialLdapContext}. */ +// TODO implement multiple contexts and connection pooling. +public class LdapConnection { + private InitialLdapContext initialLdapContext = null; + + public LdapConnection(String url, Dictionary properties) { + try { + Hashtable connEnv = new Hashtable(); + connEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + connEnv.put(Context.PROVIDER_URL, url); + connEnv.put("java.naming.ldap.attributes.binary", LdapAttrs.userPassword.name()); + // use pooling in order to avoid connection timeout +// connEnv.put("com.sun.jndi.ldap.connect.pool", "true"); +// connEnv.put("com.sun.jndi.ldap.connect.pool.timeout", 300000); + + initialLdapContext = new InitialLdapContext(connEnv, null); + // StartTlsResponse tls = (StartTlsResponse) ctx + // .extendedOperation(new StartTlsRequest()); + // tls.negotiate(); + Object securityAuthentication = properties.get(Context.SECURITY_AUTHENTICATION); + if (securityAuthentication != null) + initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, securityAuthentication); + else + initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); + Object principal = properties.get(Context.SECURITY_PRINCIPAL); + if (principal != null) { + initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString()); + Object creds = properties.get(Context.SECURITY_CREDENTIALS); + if (creds != null) { + initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString()); + } + } + } catch (NamingException e) { + throw new IllegalStateException("Cannot connect to LDAP", e); + } + + } + + public void init() { + + } + + public void destroy() { + try { + // tls.close(); + initialLdapContext.close(); + initialLdapContext = null; + } catch (NamingException e) { + e.printStackTrace(); + } + } + + protected InitialLdapContext getLdapContext() { + return initialLdapContext; + } + + protected void reconnect() throws NamingException { + initialLdapContext.reconnect(initialLdapContext.getConnectControls()); + } + + public synchronized NamingEnumeration search(LdapName searchBase, String searchFilter, + SearchControls searchControls) throws NamingException { + NamingEnumeration results; + try { + results = getLdapContext().search(searchBase, searchFilter, searchControls); + } catch (CommunicationException e) { + reconnect(); + results = getLdapContext().search(searchBase, searchFilter, searchControls); + } + return results; + } + + public synchronized Attributes getAttributes(LdapName name) throws NamingException { + try { + return getLdapContext().getAttributes(name); + } catch (CommunicationException e) { + reconnect(); + return getLdapContext().getAttributes(name); + } + } + + public synchronized boolean entryExists(LdapName name) throws NamingException { + String[] noAttrOID = new String[] { "1.1" }; + try { + getLdapContext().getAttributes(name, noAttrOID); + return true; + } catch (CommunicationException e) { + reconnect(); + getLdapContext().getAttributes(name, noAttrOID); + return true; + } catch (NameNotFoundException e) { + return false; + } + } + + public synchronized void prepareChanges(WorkingCopy wc) throws NamingException { + // make sure connection will work + reconnect(); + + // delete + for (LdapName dn : wc.getDeletedData().keySet()) { + if (!entryExists(dn)) + throw new IllegalStateException("User to delete no found " + dn); + } + // add + for (LdapName dn : wc.getNewData().keySet()) { + if (entryExists(dn)) + throw new IllegalStateException("User to create found " + dn); + } + // modify + for (LdapName dn : wc.getModifiedData().keySet()) { + if (!wc.getNewData().containsKey(dn) && !entryExists(dn)) + throw new IllegalStateException("User to modify not found " + dn); + } + + } + +// protected boolean entryExists(LdapName dn) throws NamingException { +// try { +// return getAttributes(dn).size() != 0; +// } catch (NameNotFoundException e) { +// return false; +// } +// } + + public synchronized void commitChanges(LdapEntryWorkingCopy wc) throws NamingException { + // delete + for (LdapName dn : wc.getDeletedData().keySet()) { + getLdapContext().destroySubcontext(dn); + } + // add + for (LdapName dn : wc.getNewData().keySet()) { + LdapEntry user = wc.getNewData().get(dn); + getLdapContext().createSubcontext(dn, user.getAttributes()); + } + // modify + for (LdapName dn : wc.getModifiedData().keySet()) { + Attributes modifiedAttrs = wc.getModifiedData().get(dn); + getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDao.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDao.java new file mode 100644 index 000000000..461013fea --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDao.java @@ -0,0 +1,264 @@ +package org.argeo.cms.directory.ldap; + +import static org.argeo.api.acr.ldap.LdapAttrs.objectClass; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.AuthenticationNotSupportedException; +import javax.naming.Binding; +import javax.naming.InvalidNameException; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.argeo.api.acr.ldap.LdapAttrs; +import org.argeo.api.acr.ldap.LdapObjs; +import org.argeo.api.cms.directory.HierarchyUnit; + +/** A user admin based on a LDAP server. */ +public class LdapDao extends AbstractLdapDirectoryDao { + private LdapConnection ldapConnection; + + public LdapDao(AbstractLdapDirectory directory) { + super(directory); + } + + @Override + public void init() { + ldapConnection = new LdapConnection(getDirectory().getUri().toString(), getDirectory().cloneConfigProperties()); + } + + public void destroy() { + ldapConnection.destroy(); + } + + @Override + public boolean checkConnection() { + try { + return ldapConnection.entryExists(getDirectory().getBaseDn()); + } catch (NamingException e) { + return false; + } + } + + @Override + public boolean entryExists(LdapName dn) { + try { + return ldapConnection.entryExists(dn); + } catch (NameNotFoundException e) { + return false; + } catch (NamingException e) { + throw new IllegalStateException("Cannot check " + dn, e); + } + } + + @Override + public LdapEntry doGetEntry(LdapName name) throws NameNotFoundException { +// if (!entryExists(name)) +// throw new NameNotFoundException(name + " was not found in " + getDirectory().getBaseDn()); + try { + Attributes attrs = ldapConnection.getAttributes(name); + + LdapEntry res; + Rdn technicalRdn = LdapNameUtils.getParentRdn(name); + if (getDirectory().getGroupBaseRdn().equals(technicalRdn)) { + if (attrs.size() == 0) {// exists but not accessible + attrs = new BasicAttributes(); + attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name()); + attrs.put(LdapAttrs.objectClass.name(), getDirectory().getGroupObjectClass()); + } + res = newGroup(name); + } else if (getDirectory().getSystemRoleBaseRdn().equals(technicalRdn)) { + if (attrs.size() == 0) {// exists but not accessible + attrs = new BasicAttributes(); + attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name()); + attrs.put(LdapAttrs.objectClass.name(), getDirectory().getGroupObjectClass()); + } + res = newGroup(name); + } else if (getDirectory().getUserBaseRdn().equals(technicalRdn)) { + if (attrs.size() == 0) {// exists but not accessible + attrs = new BasicAttributes(); + attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name()); + attrs.put(LdapAttrs.objectClass.name(), getDirectory().getUserObjectClass()); + } + res = newUser(name); + } else { + res = new DefaultLdapEntry(getDirectory(), name); + } + return res; + } catch (NameNotFoundException e) { + throw e; + } catch (NamingException e) { + throw new IllegalStateException("Cannot retrieve entry " + name, e); + } + } + + @Override + public Attributes doGetAttributes(LdapName name) { + try { + Attributes attrs = ldapConnection.getAttributes(name); + return attrs; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get attributes for " + name); + } + } + + @Override + public List doGetEntries(LdapName searchBase, String f, boolean deep) { + ArrayList res = new ArrayList<>(); + try { + String searchFilter = f != null ? f.toString() + : "(|(" + objectClass.name() + "=" + getDirectory().getUserObjectClass() + ")(" + objectClass.name() + + "=" + getDirectory().getGroupObjectClass() + "))"; + SearchControls searchControls = new SearchControls(); + // only attribute needed is objectClass + searchControls.setReturningAttributes(new String[] { objectClass.name() }); + // FIXME make one level consistent with deep + searchControls.setSearchScope(deep ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE); + + // LdapName searchBase = getBaseDn(); + NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); + + results: while (results.hasMoreElements()) { + SearchResult searchResult = results.next(); + Attributes attrs = searchResult.getAttributes(); + Attribute objectClassAttr = attrs.get(objectClass.name()); + LdapName dn = toDn(searchBase, searchResult); + LdapEntry role; + if (objectClassAttr.contains(getDirectory().getGroupObjectClass()) + || objectClassAttr.contains(getDirectory().getGroupObjectClass().toLowerCase())) + role = newGroup(dn); + else if (objectClassAttr.contains(getDirectory().getUserObjectClass()) + || objectClassAttr.contains(getDirectory().getUserObjectClass().toLowerCase())) + role = newUser(dn); + else { +// log.warn("Unsupported LDAP type for " + searchResult.getName()); + continue results; + } + res.add(role); + } + return res; + } catch (AuthenticationNotSupportedException e) { + // ignore (typically an unsupported anonymous bind) + // TODO better logging + return res; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get roles for filter " + f, e); + } + } + + private LdapName toDn(LdapName baseDn, Binding binding) throws InvalidNameException { + return new LdapName(binding.isRelative() ? binding.getName() + "," + baseDn : binding.getName()); + } + + @Override + public List getDirectGroups(LdapName dn) { + List directGroups = new ArrayList(); + try { + String searchFilter = "(&(" + objectClass + "=" + getDirectory().getGroupObjectClass() + ")(" + + getDirectory().getMemberAttributeId() + "=" + dn + "))"; + + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + + LdapName searchBase = getDirectory().getBaseDn(); + NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); + + while (results.hasMoreElements()) { + SearchResult searchResult = (SearchResult) results.nextElement(); + directGroups.add(toDn(searchBase, searchResult)); + } + return directGroups; + } catch (NamingException e) { + throw new IllegalStateException("Cannot populate direct members of " + dn, e); + } + } + + @Override + public void prepare(LdapEntryWorkingCopy wc) { + try { + ldapConnection.prepareChanges(wc); + } catch (NamingException e) { + throw new IllegalStateException("Cannot prepare LDAP", e); + } + } + + @Override + public void commit(LdapEntryWorkingCopy wc) { + try { + ldapConnection.commitChanges(wc); + } catch (NamingException e) { + throw new IllegalStateException("Cannot commit LDAP", e); + } + } + + @Override + public void rollback(LdapEntryWorkingCopy wc) { + // prepare not impacting + } + + /* + * HIERARCHY + */ + + @Override + public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { + List res = new ArrayList<>(); + try { + String structuralFilter = functionalOnly ? "" + : "(" + getDirectory().getUserBaseRdn() + ")(" + getDirectory().getGroupBaseRdn() + ")(" + + getDirectory().getSystemRoleBaseRdn() + ")"; + String searchFilter = "(|(" + objectClass + "=" + LdapObjs.organizationalUnit.name() + ")(" + objectClass + + "=" + LdapObjs.organization.name() + ")" + structuralFilter + ")"; + + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); + // no attributes needed + searchControls.setReturningAttributes(new String[0]); + + NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); + + while (results.hasMoreElements()) { + SearchResult searchResult = (SearchResult) results.nextElement(); + LdapName dn = toDn(searchBase, searchResult); +// Attributes attrs = searchResult.getAttributes(); + LdapHierarchyUnit hierarchyUnit = new LdapHierarchyUnit(getDirectory(), dn); + if (functionalOnly) { + if (hierarchyUnit.isFunctional()) + res.add(hierarchyUnit); + } else { + res.add(hierarchyUnit); + } + } + return res; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get direct hierarchy units ", e); + } + } + + @Override + public HierarchyUnit doGetHierarchyUnit(LdapName dn) { + try { + if (getDirectory().getBaseDn().equals(dn)) + return getDirectory(); + if (!dn.startsWith(getDirectory().getBaseDn())) + throw new IllegalArgumentException(dn + " does not start with base DN " + getDirectory().getBaseDn()); + if (!ldapConnection.entryExists(dn)) + return null; + return new LdapHierarchyUnit(getDirectory(), dn); + } catch (NameNotFoundException e) { + return null; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get hierarchy unit " + dn, e); + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDirectoryDao.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDirectoryDao.java new file mode 100644 index 000000000..03b03ea11 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDirectoryDao.java @@ -0,0 +1,37 @@ +package org.argeo.cms.directory.ldap; + +import java.util.List; + +import javax.naming.NameNotFoundException; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.argeo.api.cms.directory.HierarchyUnit; +import org.argeo.api.cms.transaction.WorkingCopyProcessor; + +/** Low-level access to an LDAP/LDIF directory. */ +public interface LdapDirectoryDao extends WorkingCopyProcessor { + boolean checkConnection(); + + boolean entryExists(LdapName dn); + + LdapEntry doGetEntry(LdapName name) throws NameNotFoundException; + + Attributes doGetAttributes(LdapName name); + + List doGetEntries(LdapName searchBase, String filter, boolean deep); + + List getDirectGroups(LdapName dn); + + Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly); + + HierarchyUnit doGetHierarchyUnit(LdapName dn); + + LdapEntry newUser(LdapName name); + + LdapEntry newGroup(LdapName name); + + void init(); + + void destroy(); +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntry.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntry.java new file mode 100644 index 000000000..e2587621a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntry.java @@ -0,0 +1,65 @@ +package org.argeo.cms.directory.ldap; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Dictionary; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.StringJoiner; +import java.util.TreeSet; + +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.argeo.api.acr.ldap.LdapAttrs; + +/** An LDAP entry. */ +public interface LdapEntry { + LdapName getDn(); + + Attributes getAttributes(); + + void publishAttributes(Attributes modifiedAttributes); + + List getReferences(String attributeId); + + Dictionary getProperties(); + + boolean hasCredential(String key, Object value); + + /* + * UTILITIES + */ + /** + * Convert a collection of object classes to the format expected by an LDAP + * backend. + */ + public static void addObjectClasses(Dictionary properties, Collection objectClasses) { + String value = properties.get(LdapAttrs.objectClasses.name()).toString(); + Set currentObjectClasses = new TreeSet<>(Arrays.asList(value.toString().split("\n"))); + currentObjectClasses.addAll(objectClasses); + StringJoiner values = new StringJoiner("\n"); + currentObjectClasses.forEach((s) -> values.add(s)); + properties.put(LdapAttrs.objectClasses.name(), values.toString()); + } + + public static Object getLocalized(Dictionary properties, String key, Locale locale) { + if (locale == null) + return null; + Object value = null; + value = properties.get(key + ";lang-" + locale.getLanguage() + "-" + locale.getCountry()); + if (value == null) + value = properties.get(key + ";lang-" + locale.getLanguage()); + return value; + } + + public static String toLocalizedKey(String key, Locale locale) { + String country = locale.getCountry(); + if ("".equals(country)) { + return key + ";lang-" + locale.getLanguage(); + } else { + return key + ";lang-" + locale.getLanguage() + "-" + locale.getCountry(); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntryWorkingCopy.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntryWorkingCopy.java new file mode 100644 index 000000000..b5afc9dce --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntryWorkingCopy.java @@ -0,0 +1,19 @@ +package org.argeo.cms.directory.ldap; + +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.argeo.api.cms.transaction.AbstractWorkingCopy; + +/** Working copy for a user directory being edited. */ +public class LdapEntryWorkingCopy extends AbstractWorkingCopy { + @Override + protected LdapName getId(LdapEntry entry) { + return entry.getDn(); + } + + @Override + protected Attributes cloneAttributes(LdapEntry entry) { + return (Attributes) entry.getAttributes().clone(); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapHierarchyUnit.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapHierarchyUnit.java new file mode 100644 index 000000000..7abf09885 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapHierarchyUnit.java @@ -0,0 +1,64 @@ +package org.argeo.cms.directory.ldap; + +import java.util.Locale; + +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.argeo.api.cms.directory.HierarchyUnit; + +/** LDIF/LDAP based implementation of {@link HierarchyUnit}. */ +public class LdapHierarchyUnit extends DefaultLdapEntry implements HierarchyUnit { + private final boolean functional; + + public LdapHierarchyUnit(AbstractLdapDirectory directory, LdapName dn) { + super(directory, dn); + + Rdn rdn = LdapNameUtils.getLastRdn(dn); + functional = !(directory.getUserBaseRdn().equals(rdn) || directory.getGroupBaseRdn().equals(rdn) + || directory.getSystemRoleBaseRdn().equals(rdn)); + } + + @Override + public HierarchyUnit getParent() { + return getDirectoryDao().doGetHierarchyUnit(LdapNameUtils.getParent(getDn())); + } + + @Override + public Iterable getDirectHierarchyUnits(boolean functionalOnly) { + return getDirectoryDao().doGetDirectHierarchyUnits(getDn(), functionalOnly); + } + + @Override + public boolean isFunctional() { + return functional; + } + + @Override + public String getHierarchyUnitName() { + String name = LdapNameUtils.getLastRdnValue(getDn()); + // TODO check ou, o, etc. + return name; + } + + @Override + public String getHierarchyUnitLabel(Locale locale) { + String key = LdapNameUtils.getLastRdn(getDn()).getType(); + Object value = LdapEntry.getLocalized(getProperties(), key, locale); + if (value == null) + value = getHierarchyUnitName(); + assert value != null; + return value.toString(); + } + + @Override + public String getBase() { + return getDn().toString(); + } + + @Override + public String toString() { + return "Hierarchy Unit " + getDn().toString(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapNameUtils.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapNameUtils.java new file mode 100644 index 000000000..74f23da67 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapNameUtils.java @@ -0,0 +1,69 @@ +package org.argeo.cms.directory.ldap; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +/** Utilities to simplify using {@link LdapName}. */ +public class LdapNameUtils { + + public static LdapName relativeName(LdapName prefix, LdapName dn) { + try { + if (!dn.startsWith(prefix)) + throw new IllegalArgumentException("Prefix " + prefix + " not consistent with " + dn); + LdapName res = (LdapName) dn.clone(); + for (int i = 0; i < prefix.size(); i++) { + res.remove(0); + } + return res; + } catch (InvalidNameException e) { + throw new IllegalStateException("Cannot find realtive name", e); + } + } + + public static LdapName getParent(LdapName dn) { + try { + LdapName parent = (LdapName) dn.clone(); + parent.remove(parent.size() - 1); + return parent; + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot get parent of " + dn, e); + } + } + + public static Rdn getParentRdn(LdapName dn) { + if (dn.size() < 2) + throw new IllegalArgumentException(dn + " has no parent"); + Rdn parentRdn = dn.getRdn(dn.size() - 2); + return parentRdn; + } + + public static LdapName toLdapName(String distinguishedName) { + try { + return new LdapName(distinguishedName); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot parse " + distinguishedName + " as LDAP name", e); + } + } + + public static Rdn getLastRdn(LdapName dn) { + return dn.getRdn(dn.size() - 1); + } + + public static String getLastRdnAsString(LdapName dn) { + return getLastRdn(dn).toString(); + } + + public static String getLastRdnValue(String dn) { + return getLastRdnValue(toLdapName(dn)); + } + + public static String getLastRdnValue(LdapName dn) { + return getLastRdn(dn).getValue().toString(); + } + + /** singleton */ + private LdapNameUtils() { + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifDao.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifDao.java new file mode 100644 index 000000000..1f3389896 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifDao.java @@ -0,0 +1,306 @@ +package org.argeo.cms.directory.ldap; + +import static org.argeo.api.acr.ldap.LdapAttrs.objectClass; +import static org.argeo.api.acr.ldap.LdapObjs.inetOrgPerson; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.argeo.api.acr.ldap.LdapObjs; +import org.argeo.api.cms.directory.HierarchyUnit; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Role; + +/** A user admin based on a LDIF files. */ +public class LdifDao extends AbstractLdapDirectoryDao { + private NavigableMap entries = new TreeMap<>(); + private NavigableMap hierarchy = new TreeMap<>(); + + private NavigableMap values = new TreeMap<>(); + + public LdifDao(AbstractLdapDirectory directory) { + super(directory); + } + + public void init() { + String uri = getDirectory().getUri(); + if (uri == null) + return; + try { + URI u = new URI(uri); + if (u.getScheme().equals("file")) { + File file = new File(u); + if (!file.exists()) + return; + } + load(u.toURL().openStream()); + } catch (IOException | URISyntaxException e) { + throw new IllegalStateException("Cannot open URL " + getDirectory().getUri(), e); + } + } + + public void save() { + if (getDirectory().getUri() == null) + return; // ignore + if (getDirectory().isReadOnly()) + throw new IllegalStateException( + "Cannot save LDIF user admin: " + getDirectory().getUri() + " is read-only"); + try (FileOutputStream out = new FileOutputStream(new File(new URI(getDirectory().getUri())))) { + save(out); + } catch (IOException | URISyntaxException e) { + throw new IllegalStateException("Cannot save user admin to " + getDirectory().getUri(), e); + } + } + + public void save(OutputStream out) throws IOException { + try { + LdifWriter ldifWriter = new LdifWriter(out); + for (LdapName name : hierarchy.keySet()) + ldifWriter.writeEntry(name, hierarchy.get(name).getAttributes()); + for (LdapName name : entries.keySet()) + ldifWriter.writeEntry(name, entries.get(name).getAttributes()); + } finally { + out.close(); + } + } + + public void load(InputStream in) { + try { + entries.clear(); + hierarchy.clear(); + + LdifParser ldifParser = new LdifParser(); + SortedMap allEntries = ldifParser.read(in); + for (LdapName key : allEntries.keySet()) { + Attributes attributes = allEntries.get(key); + // check for inconsistency + Set lowerCase = new HashSet(); + NamingEnumeration ids = attributes.getIDs(); + while (ids.hasMoreElements()) { + String id = ids.nextElement().toLowerCase(); + if (lowerCase.contains(id)) + throw new IllegalStateException(key + " has duplicate id " + id); + lowerCase.add(id); + } + + values.put(key, attributes); + + // analyse object classes + NamingEnumeration objectClasses = attributes.get(objectClass.name()).getAll(); + // System.out.println(key); + objectClasses: while (objectClasses.hasMore()) { + String objectClass = objectClasses.next().toString(); + // System.out.println(" " + objectClass); + if (objectClass.toLowerCase().equals(inetOrgPerson.name().toLowerCase())) { + entries.put(key, newUser(key)); + break objectClasses; + } else if (objectClass.toLowerCase().equals(getDirectory().getGroupObjectClass().toLowerCase())) { + entries.put(key, newGroup(key)); + break objectClasses; + } else if (objectClass.equalsIgnoreCase(LdapObjs.organizationalUnit.name())) { + // TODO skip if it does not contain groups or users + hierarchy.put(key, new LdapHierarchyUnit(getDirectory(), key)); + break objectClasses; + } + } + } + + } catch (NamingException | IOException e) { + throw new IllegalStateException("Cannot load user admin service from LDIF", e); + } + } + + public void destroy() { +// if (users == null || groups == null) + if (entries == null) + throw new IllegalStateException("User directory " + getDirectory().getBaseDn() + " is already destroyed"); +// users = null; +// groups = null; + entries = null; + } + + /* + * USER ADMIN + */ + + @Override + public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException { + if (entries.containsKey(key)) + return entries.get(key); + throw new NameNotFoundException(key + " not persisted"); + } + + @Override + public Attributes doGetAttributes(LdapName name) { + if (!values.containsKey(name)) + throw new IllegalStateException(name + " doe not exist in " + getDirectory().getBaseDn()); + return values.get(name); + } + + @Override + public boolean checkConnection() { + return true; + } + + @Override + public boolean entryExists(LdapName dn) { + return entries.containsKey(dn);// || groups.containsKey(dn); + } + + @Override + public List doGetEntries(LdapName searchBase, String f, boolean deep) { + Objects.requireNonNull(searchBase); + ArrayList res = new ArrayList<>(); + if (f == null && deep && getDirectory().getBaseDn().equals(searchBase)) { + res.addAll(entries.values()); + } else { + filterRoles(entries, searchBase, f, deep, res); + } + return res; + } + + private void filterRoles(SortedMap map, LdapName searchBase, String f, boolean deep, + List res) { + // FIXME get rid of OSGi references + try { + // TODO reduce map with search base ? + Filter filter = f != null ? FrameworkUtil.createFilter(f) : null; + roles: for (LdapEntry user : map.values()) { + LdapName dn = user.getDn(); + if (dn.startsWith(searchBase)) { + if (!deep && dn.size() != (searchBase.size() + 1)) + continue roles; + if (filter == null) + res.add(user); + else { + if (user instanceof Role) { + if (filter.match(((Role) user).getProperties())) + res.add(user); + } + } + } + } + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot create filter " + f, e); + } + + } + + @Override + public List getDirectGroups(LdapName dn) { + List directGroups = new ArrayList(); + entries: for (LdapName name : entries.keySet()) { + LdapEntry group; + try { + LdapEntry entry = doGetEntry(name); + if (AbstractLdapDirectory.hasObjectClass(entry.getAttributes(), getDirectory().getGroupObjectClass())) { + group = entry; + } else { + continue entries; + } + } catch (NameNotFoundException e) { + throw new IllegalArgumentException("Group " + dn + " not found", e); + } + if (group.getReferences(getDirectory().getMemberAttributeId()).contains(dn)) { + directGroups.add(group.getDn()); + } + } + return directGroups; + } + + @Override + public void prepare(LdapEntryWorkingCopy wc) { + // delete + for (LdapName dn : wc.getDeletedData().keySet()) { + if (entries.containsKey(dn)) + entries.remove(dn); + else + throw new IllegalStateException("User to delete not found " + dn); + } + // add + for (LdapName dn : wc.getNewData().keySet()) { + LdapEntry user = (LdapEntry) wc.getNewData().get(dn); + if (entries.containsKey(dn)) + throw new IllegalStateException("User to create found " + dn); + entries.put(dn, user); + } + // modify + for (LdapName dn : wc.getModifiedData().keySet()) { + Attributes modifiedAttrs = wc.getModifiedData().get(dn); + LdapEntry user; + try { + user = doGetEntry(dn); + } catch (NameNotFoundException e) { + throw new IllegalStateException("User to modify no found " + dn, e); + } + if (user == null) + throw new IllegalStateException("User to modify no found " + dn); + user.publishAttributes(modifiedAttrs); + } + } + + @Override + public void commit(LdapEntryWorkingCopy wc) { + save(); + } + + @Override + public void rollback(LdapEntryWorkingCopy wc) { + init(); + } + + /* + * HIERARCHY + */ + @Override + public HierarchyUnit doGetHierarchyUnit(LdapName dn) { + if (getDirectory().getBaseDn().equals(dn)) + return getDirectory(); + return hierarchy.get(dn); + } + + @Override + public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { + List res = new ArrayList<>(); + for (LdapName n : hierarchy.keySet()) { + if (n.size() == searchBase.size() + 1) { + if (n.startsWith(searchBase)) { + HierarchyUnit hu = hierarchy.get(n); + if (functionalOnly) { + if (hu.isFunctional()) + res.add(hu); + } else { + res.add(hu); + } + } + } + } + return res; + } + + public void scope(LdifDao scoped) { + scoped.entries = Collections.unmodifiableNavigableMap(entries); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifParser.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifParser.java new file mode 100644 index 000000000..c76c362cb --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifParser.java @@ -0,0 +1,161 @@ +package org.argeo.cms.directory.ldap; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.naming.InvalidNameException; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +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 org.argeo.api.acr.ldap.LdapAttrs; + +/** Basic LDIF parser. */ +public class LdifParser { + private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + protected Attributes addAttributes(SortedMap res, int lineNumber, LdapName currentDn, + Attributes currentAttributes) { + try { + Rdn nameRdn = currentDn.getRdn(currentDn.size() - 1); + Attribute nameAttr = currentAttributes.get(nameRdn.getType()); + if (nameAttr == null) + currentAttributes.put(nameRdn.getType(), nameRdn.getValue()); + else if (!nameAttr.get().equals(nameRdn.getValue())) + throw new IllegalStateException( + "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + currentDn + + " (shortly before line " + lineNumber + " in LDIF file)"); + Attributes previous = res.put(currentDn, currentAttributes); + return previous; + } catch (NamingException e) { + throw new IllegalStateException("Cannot add " + currentDn, e); + } + } + + /** With UTF-8 charset */ + public SortedMap read(InputStream in) throws IOException { + try (Reader reader = new InputStreamReader(in, DEFAULT_CHARSET)) { + return read(reader); + } finally { + try { + in.close(); + } catch (IOException e) { + // silent + } + } + } + + /** Will close the reader. */ + public SortedMap read(Reader reader) throws IOException { + SortedMap res = new TreeMap(); + try { + List lines = new ArrayList<>(); + try (BufferedReader br = new BufferedReader(reader)) { + String line; + while ((line = br.readLine()) != null) { + lines.add(line); + } + } + if (lines.size() == 0) + return res; + // add an empty new line since the last line is not checked + if (!lines.get(lines.size() - 1).equals("")) + lines.add(""); + + LdapName currentDn = null; + Attributes currentAttributes = null; + StringBuilder currentEntry = new StringBuilder(); + + readLines: for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { + String line = lines.get(lineNumber); + boolean isLastLine = false; + if (lineNumber == lines.size() - 1) + isLastLine = true; + if (line.startsWith(" ")) { + currentEntry.append(line.substring(1)); + if (!isLastLine) + continue readLines; + } + + if (currentEntry.length() != 0 || isLastLine) { + // read previous attribute + StringBuilder attrId = new StringBuilder(8); + boolean isBase64 = false; + readAttrId: for (int i = 0; i < currentEntry.length(); i++) { + char c = currentEntry.charAt(i); + if (c == ':') { + if (i + 1 < currentEntry.length() && currentEntry.charAt(i + 1) == ':') + isBase64 = true; + currentEntry.delete(0, i + (isBase64 ? 2 : 1)); + break readAttrId; + } else { + attrId.append(c); + } + } + + String attributeId = attrId.toString(); + // TODO should we really trim the end of the string as well? + String cleanValueStr = currentEntry.toString().trim(); + Object attributeValue = isBase64 ? Base64.getDecoder().decode(cleanValueStr) : cleanValueStr; + + // manage DN attributes + if (attributeId.equals(LdapAttrs.DN) || isLastLine) { + if (currentDn != null) { + // + // ADD + // + Attributes previous = addAttributes(res, lineNumber, currentDn, currentAttributes); + if (previous != null) { +// log.warn("There was already an entry with DN " + currentDn +// + ", which has been discarded by a subsequent one."); + } + } + + if (attributeId.equals(LdapAttrs.DN)) + try { + currentDn = new LdapName(attributeValue.toString()); + currentAttributes = new BasicAttributes(true); + } catch (InvalidNameException e) { +// log.error(attributeValue + " not a valid DN, skipping the entry."); + currentDn = null; + currentAttributes = null; + } + } + + // store attribute + if (currentAttributes != null) { + Attribute attribute = currentAttributes.get(attributeId); + if (attribute == null) { + attribute = new BasicAttribute(attributeId); + currentAttributes.put(attribute); + } + attribute.add(attributeValue); + } + currentEntry = new StringBuilder(); + } + currentEntry.append(line); + } + } finally { + try { + reader.close(); + } catch (IOException e) { + // silent + } + } + return res; + } +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifWriter.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifWriter.java new file mode 100644 index 000000000..2a1ed811f --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifWriter.java @@ -0,0 +1,104 @@ +package org.argeo.cms.directory.ldap; + +import static org.argeo.api.acr.ldap.LdapAttrs.DN; +import static org.argeo.api.acr.ldap.LdapAttrs.member; +import static org.argeo.api.acr.ldap.LdapAttrs.objectClass; +import static org.argeo.api.acr.ldap.LdapAttrs.uniqueMember; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +/** Basic LDIF writer */ +public class LdifWriter { + private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + private final Writer writer; + + /** Writer must be closed by caller */ + public LdifWriter(Writer writer) { + this.writer = writer; + } + + /** Stream must be closed by caller */ + public LdifWriter(OutputStream out) { + this(new OutputStreamWriter(out, DEFAULT_CHARSET)); + } + + public void writeEntry(LdapName name, Attributes attributes) throws IOException { + try { + // check consistency + Rdn nameRdn = name.getRdn(name.size() - 1); + Attribute nameAttr = attributes.get(nameRdn.getType()); + if (!nameAttr.get().equals(nameRdn.getValue())) + throw new IllegalArgumentException( + "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + name); + + writer.append(DN + ": ").append(name.toString()).append('\n'); + Attribute objectClassAttr = attributes.get(objectClass.name()); + if (objectClassAttr != null) + writeAttribute(objectClassAttr); + attributes: for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { + Attribute attribute = attrs.next(); + if (attribute.getID().equals(DN) || attribute.getID().equals(objectClass.name())) + continue attributes;// skip DN attribute + if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) + continue attributes;// skip member and uniqueMember attributes, so that they are always written last + writeAttribute(attribute); + } + // write member and uniqueMember attributes last + for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { + Attribute attribute = attrs.next(); + if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) + writeMemberAttribute(attribute); + } + writer.append('\n'); + writer.flush(); + } catch (NamingException e) { + throw new IllegalStateException("Cannot write LDIF", e); + } + } + + public void write(Map entries) throws IOException { + for (LdapName dn : entries.keySet()) + writeEntry(dn, entries.get(dn)); + } + + protected void writeAttribute(Attribute attribute) throws NamingException, IOException { + for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { + Object value = attrValues.next(); + if (value instanceof byte[]) { + String encoded = Base64.getEncoder().encodeToString((byte[]) value); + writer.append(attribute.getID()).append(":: ").append(encoded).append('\n'); + } else { + writer.append(attribute.getID()).append(": ").append(value.toString()).append('\n'); + } + } + } + + protected void writeMemberAttribute(Attribute attribute) throws NamingException, IOException { + // Note: duplicate entries will be swallowed + SortedSet values = new TreeSet<>(); + for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { + String value = attrValues.next().toString(); + values.add(value); + } + + for (String value : values) { + writer.append(attribute.getID()).append(": ").append(value).append('\n'); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/SharedSecret.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/SharedSecret.java new file mode 100644 index 000000000..2c52ee12a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/directory/ldap/SharedSecret.java @@ -0,0 +1,48 @@ +package org.argeo.cms.directory.ldap; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import org.argeo.api.acr.ldap.NamingUtils; + +public class SharedSecret extends AuthPassword { + public final static String X_SHARED_SECRET = "X-SharedSecret"; + private final Instant expiry; + + public SharedSecret(String authInfo, String authValue) { + super(authInfo, authValue); + expiry = null; + } + + public SharedSecret(AuthPassword authPassword) { + super(authPassword); + String authInfo = getAuthInfo(); + if (authInfo.length() == 16) { + expiry = NamingUtils.ldapDateToInstant(authInfo); + } else { + expiry = null; + } + } + + public SharedSecret(ZonedDateTime expiryTimestamp, String value) { + super(NamingUtils.instantToLdapDate(expiryTimestamp), value); + expiry = expiryTimestamp.withZoneSameInstant(ZoneOffset.UTC).toInstant(); + } + + public SharedSecret(int hours, String value) { + this(ZonedDateTime.now().plusHours(hours), value); + } + + @Override + protected String getExpectedAuthScheme() { + return X_SHARED_SECRET; + } + + public boolean isExpired() { + if (expiry == null) + return false; + return expiry.isBefore(Instant.now()); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/dns/DnsBrowser.java b/org.argeo.cms/src/org/argeo/cms/dns/DnsBrowser.java new file mode 100644 index 000000000..c6b653015 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/dns/DnsBrowser.java @@ -0,0 +1,216 @@ +package org.argeo.cms.dns; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.SortedSet; +import java.util.StringJoiner; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.naming.Binding; +import javax.naming.Context; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + +public class DnsBrowser implements Closeable { + private final DirContext initialCtx; + + public DnsBrowser() { + this(new ArrayList<>()); + } + + public DnsBrowser(List dnsServerUrls) { + try { + Objects.requireNonNull(dnsServerUrls); + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); + if (!dnsServerUrls.isEmpty()) { + boolean specified = false; + StringJoiner providerUrl = new StringJoiner(" "); + for (String dnsUrl : dnsServerUrls) { + if (dnsUrl != null) { + providerUrl.add(dnsUrl); + specified = true; + } + } + if (specified) + env.put(Context.PROVIDER_URL, providerUrl.toString()); + } + initialCtx = new InitialDirContext(env); + } catch (NamingException e) { + throw new IllegalStateException("Cannot initialise DNS borowser.", e); + } + } + + public Map> getAllRecords(String name) { + try { + Map> res = new TreeMap<>(); + Attributes attrs = initialCtx.getAttributes(name); + NamingEnumeration ids = attrs.getIDs(); + while (ids.hasMore()) { + String recordType = ids.next(); + List lst = new ArrayList(); + res.put(recordType, lst); + Attribute attr = attrs.get(recordType); + addValues(attr, lst); + } + return Collections.unmodifiableMap(res); + } catch (NamingException e) { + throw new IllegalStateException("Cannot get allrecords of " + name, e); + } + } + + /** + * Return a single record (typically A, AAAA, etc. or null if not available. + * Will fail if multiple records. + */ + public String getRecord(String name, String recordType) { + try { + Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType }); + if (attrs.size() == 0) + return null; + Attribute attr = attrs.get(recordType); + if (attr.size() > 1) + throw new IllegalArgumentException("Multiple record type " + recordType); + assert attr.size() != 0; + Object value = attr.get(); + assert value != null; + return value.toString(); + } catch (NameNotFoundException e) { + return null; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get DNS entry " + recordType + " of " + name, e); + } + } + + /** + * Return records of a given type. + */ + public List getRecords(String name, String recordType) { + try { + List res = new ArrayList(); + Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType }); + Attribute attr = attrs.get(recordType); + addValues(attr, res); + return res; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get records " + recordType + " of " + name, e); + } + } + + /** Ordered, with preferred first. */ + public List getSrvRecordsAsHosts(String name, boolean withPort) { + List raw = getRecords(name, "SRV"); + if (raw.size() == 0) + return null; + SortedSet res = new TreeSet<>(); + for (int i = 0; i < raw.size(); i++) { + String record = raw.get(i); + String[] arr = record.split(" "); + Integer priority = Integer.parseInt(arr[0]); + Integer weight = Integer.parseInt(arr[1]); + Integer port = Integer.parseInt(arr[2]); + String hostname = arr[3]; + SrvRecord order = new SrvRecord(priority, weight, port, hostname); + res.add(order); + } + List lst = new ArrayList<>(); + for (SrvRecord order : res) { + lst.add(order.toHost(withPort)); + } + return Collections.unmodifiableList(lst); + } + + private void addValues(Attribute attr, List lst) throws NamingException { + NamingEnumeration values = attr.getAll(); + while (values.hasMore()) { + Object value = values.next(); + if (value != null) { + if (value instanceof byte[]) { + String str = Base64.getEncoder().encodeToString((byte[]) value); + lst.add(str); + } else + lst.add(value.toString()); + } + } + + } + + public List listEntries(String name) { + try { + List res = new ArrayList(); + NamingEnumeration ne = initialCtx.listBindings(name); + while (ne.hasMore()) { + Binding b = ne.next(); + res.add(b.getName()); + } + return Collections.unmodifiableList(res); + } catch (NamingException e) { + throw new IllegalStateException("Cannot list entries of " + name, e); + } + } + + @Override + public void close() throws IOException { + destroy(); + } + + public void destroy() { + try { + initialCtx.close(); + } catch (NamingException e) { + // silent + } + } + + public static void main(String[] args) { + if (args.length == 0) { + printUsage(System.err); + System.exit(1); + } + try (DnsBrowser dnsBrowser = new DnsBrowser()) { + String hostname = args[0]; + String recordType = args.length > 1 ? args[1] : "A"; + if (recordType.equals("*")) { + Map> records = dnsBrowser.getAllRecords(hostname); + for (String type : records.keySet()) { + for (String record : records.get(type)) { + String typeLabel; + if ("44".equals(type)) + typeLabel = "SSHFP"; + else if ("46".equals(type)) + typeLabel = "RRSIG"; + else if ("48".equals(type)) + typeLabel = "DNSKEY"; + else + typeLabel = type; + System.out.println(typeLabel + "\t" + record); + } + } + } else { + System.out.println(dnsBrowser.getRecord(hostname, recordType)); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void printUsage(PrintStream out) { + out.println("java org.argeo.naming.DnsBrowser [ | *]"); + } + +} \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/dns/SrvRecord.java b/org.argeo.cms/src/org/argeo/cms/dns/SrvRecord.java new file mode 100644 index 000000000..bdbdc769a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/dns/SrvRecord.java @@ -0,0 +1,52 @@ +package org.argeo.cms.dns; + +class SrvRecord implements Comparable { + private final Integer priority; + private final Integer weight; + private final Integer port; + private final String hostname; + + public SrvRecord(Integer priority, Integer weight, Integer port, String hostname) { + this.priority = priority; + this.weight = weight; + this.port = port; + this.hostname = hostname; + } + + @Override + public int compareTo(SrvRecord other) { + // https: // en.wikipedia.org/wiki/SRV_record + if (priority != other.priority) + return priority - other.priority; + if (weight != other.weight) + return other.weight - other.weight; + String host = toHost(false); + String otherHost = other.toHost(false); + if (host.length() == otherHost.length()) + return host.compareTo(otherHost); + else + return host.length() - otherHost.length(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SrvRecord) { + SrvRecord other = (SrvRecord) obj; + return priority == other.priority && weight == other.weight && port == other.port + && hostname.equals(other.hostname); + } + return false; + } + + @Override + public String toString() { + return priority + " " + weight; + } + + public String toHost(boolean withPort) { + String hostStr = hostname; + if (hostname.charAt(hostname.length() - 1) == '.') + hostStr = hostname.substring(0, hostname.length() - 1); + return hostStr + (withPort ? ":" + port : ""); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/file/ChecksumFactory.java b/org.argeo.cms/src/org/argeo/cms/file/ChecksumFactory.java new file mode 100644 index 000000000..6aea8bea0 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/file/ChecksumFactory.java @@ -0,0 +1,149 @@ +package org.argeo.cms.file; + +import java.io.IOException; +import java.math.BigInteger; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.zip.Checksum; + +/** Allows to fine tune how files are read. */ +public class ChecksumFactory { + private int regionSize = 10 * 1024 * 1024; + + public byte[] digest(Path path, final String algo) { + try { + final MessageDigest md = MessageDigest.getInstance(algo); + if (Files.isDirectory(path)) { + long begin = System.currentTimeMillis(); + Files.walkFileTree(path, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!Files.isDirectory(file)) { + byte[] digest = digest(file, algo); + md.update(digest); + } + return FileVisitResult.CONTINUE; + } + + }); + byte[] digest = md.digest(); + long duration = System.currentTimeMillis() - begin; + System.out.println(printBase64Binary(digest) + " " + path + " (" + duration / 1000 + "s)"); + return digest; + } else { + long begin = System.nanoTime(); + long length = -1; + try (FileChannel channel = (FileChannel) Files.newByteChannel(path);) { + length = channel.size(); + long cursor = 0; + while (cursor < length) { + long effectiveSize = Math.min(regionSize, length - cursor); + MappedByteBuffer mb = channel.map(FileChannel.MapMode.READ_ONLY, cursor, effectiveSize); + // md.update(mb); + byte[] buffer = new byte[1024]; + while (mb.hasRemaining()) { + mb.get(buffer); + md.update(buffer); + } + + // sub digest + // mb.flip(); + // MessageDigest subMd = + // MessageDigest.getInstance(algo); + // subMd.update(mb); + // byte[] subDigest = subMd.digest(); + // System.out.println(" -> " + cursor); + // System.out.println(IOUtils.encodeHexString(subDigest)); + // System.out.println(new BigInteger(1, + // subDigest).toString(16)); + // System.out.println(new BigInteger(1, subDigest) + // .toString(Character.MAX_RADIX)); + // System.out.println(printBase64Binary(subDigest)); + + cursor = cursor + regionSize; + } + byte[] digest = md.digest(); + long duration = System.nanoTime() - begin; + System.out.println(printBase64Binary(digest) + " " + path.getFileName() + " (" + duration / 1000000 + + "ms, " + (length / 1024) + "kB, " + (length / (duration / 1000000)) * 1000 / (1024 * 1024) + + " MB/s)"); + return digest; + } + } + } catch (NoSuchAlgorithmException | IOException e) { + throw new IllegalStateException("Cannot digest " + path, e); + } + } + + /** Whether the file should be mapped. */ + protected boolean mapFile(FileChannel fileChannel) throws IOException { + long size = fileChannel.size(); + if (size > (regionSize / 10)) + return true; + return false; + } + + public long checksum(Path path, Checksum crc) { + final int bufferSize = 2 * 1024 * 1024; + long begin = System.currentTimeMillis(); + try (FileChannel channel = (FileChannel) Files.newByteChannel(path);) { + byte[] bytes = new byte[bufferSize]; + long length = channel.size(); + long cursor = 0; + while (cursor < length) { + long effectiveSize = Math.min(regionSize, length - cursor); + MappedByteBuffer mb = channel.map(FileChannel.MapMode.READ_ONLY, cursor, effectiveSize); + int nGet; + while (mb.hasRemaining()) { + nGet = Math.min(mb.remaining(), bufferSize); + mb.get(bytes, 0, nGet); + crc.update(bytes, 0, nGet); + } + cursor = cursor + regionSize; + } + return crc.getValue(); + } catch (IOException e) { + throw new IllegalStateException("Cannot checksum " + path, e); + } finally { + long duration = System.currentTimeMillis() - begin; + System.out.println(duration / 1000 + "s"); + } + } + + public static void main(String... args) { + ChecksumFactory cf = new ChecksumFactory(); + // Path path = + // Paths.get("/home/mbaudier/apache-maven-3.2.3-bin.tar.gz"); + Path path; + if (args.length > 0) { + path = Paths.get(args[0]); + } else { + path = Paths.get("/home/mbaudier/Downloads/torrents/CentOS-7-x86_64-DVD-1503-01/" + + "CentOS-7-x86_64-DVD-1503-01.iso"); + } + // long adler = cf.checksum(path, new Adler32()); + // System.out.format("Adler=%d%n", adler); + // long crc = cf.checksum(path, new CRC32()); + // System.out.format("CRC=%d%n", crc); + String algo = "SHA1"; + byte[] digest = cf.digest(path, algo); + System.out.println(algo + " " + printBase64Binary(digest)); + System.out.println(algo + " " + new BigInteger(1, digest).toString(16)); + // String sha1 = printBase64Binary(cf.digest(path, "SHA1")); + // System.out.format("SHA1=%s%n", sha1); + } + + private static String printBase64Binary(byte[] arr) { + return Base64.getEncoder().encodeToString(arr); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java b/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java new file mode 100644 index 000000000..217b7a48a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java @@ -0,0 +1,33 @@ +package org.argeo.cms.http; + +/** Standard HTTP headers (including WebDav). */ +public enum HttpHeader { + AUTHORIZATION("Authorization"), // + WWW_AUTHENTICATE("WWW-Authenticate"), // + ALLOW("Allow"), // + + // WebDav + DAV("DAV"), // + DEPTH("Depth"), // + ; + + public final static String BASIC = "Basic"; + public final static String REALM = "realm"; + public final static String NEGOTIATE = "Negotiate"; + + private final String name; + + private HttpHeader(String headerName) { + this.name = headerName; + } + + public String getHeaderName() { + return name; + } + + @Override + public String toString() { + return getHeaderName(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpMethod.java b/org.argeo.cms/src/org/argeo/cms/http/HttpMethod.java new file mode 100644 index 000000000..786904564 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/http/HttpMethod.java @@ -0,0 +1,19 @@ +package org.argeo.cms.http; + +/** Generic HTTP methods. */ +public enum HttpMethod { + OPTIONS, // + HEAD, // + GET, // + POST, // + PUT, // + DELETE, // + + // WebDav + PROPFIND, // + PROPPATCH, // + MKCOL, // + MOVE, // + COPY, // + ; +} diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpServerUtils.java b/org.argeo.cms/src/org/argeo/cms/http/HttpServerUtils.java new file mode 100644 index 000000000..fc04fbfaa --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/http/HttpServerUtils.java @@ -0,0 +1,45 @@ +package org.argeo.cms.http; + +import java.net.URI; +import java.util.Objects; + +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; + +public class HttpServerUtils { + private final static String SLASH = "/"; + + private static String extractPathWithingContext(HttpContext httpContext, String fullPath, boolean startWithSlash) { + Objects.requireNonNull(fullPath); + String contextPath = httpContext.getPath(); + if (!fullPath.startsWith(contextPath)) + throw new IllegalArgumentException(fullPath + " does not belong to context" + contextPath); + String path = fullPath.substring(contextPath.length()); + // TODO optimise? + if (!startWithSlash && path.startsWith(SLASH)) { + path = path.substring(1); + } else if (startWithSlash && !path.startsWith(SLASH)) { + path = SLASH + path; + } + return path; + } + + /** Path within the context, NOT starting with a slash. */ + public static String relativize(HttpExchange exchange) { + URI uri = exchange.getRequestURI(); + HttpContext httpContext = exchange.getHttpContext(); + return extractPathWithingContext(httpContext, uri.getPath(), false); + } + + /** Path within the context, starting with a slash. */ + public static String subPath(HttpExchange exchange) { + URI uri = exchange.getRequestURI(); + HttpContext httpContext = exchange.getHttpContext(); + return extractPathWithingContext(httpContext, uri.getPath(), true); + } + + /** singleton */ + private HttpServerUtils() { + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java b/org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java new file mode 100644 index 000000000..3b9a47a38 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java @@ -0,0 +1,66 @@ +package org.argeo.cms.http; + +/** + * Standard HTTP response status codes (including WebDav ones). + * + * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" + */ +public enum HttpStatus { + // Successful responses (200–299) + OK(200, "OK"), // + NO_CONTENT(204, "No Content"), // + MULTI_STATUS(207, "Multi-Status"), // WebDav + // Client error responses (400–499) + UNAUTHORIZED(401, "Unauthorized"), // + FORBIDDEN(403, "Forbidden"), // + NOT_FOUND(404, "Not Found"), // + // Server error responses (500-599) + INTERNAL_SERVER_ERROR(500, "Internal Server Error"), // + NOT_IMPLEMENTED(501, "Not Implemented"), // + ; + + private final int code; + private final String reasonPhrase; + + HttpStatus(int statusCode, String reasonPhrase) { + this.code = statusCode; + this.reasonPhrase = reasonPhrase; + } + + public int getCode() { + return code; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + /** + * The status line, as defined by RFC2616. + * + * @see "https://www.rfc-editor.org/rfc/rfc2616#section-6.1" + */ + public String getStatusLine(String httpVersion) { + return httpVersion + " " + code + " " + reasonPhrase; + } + + public static HttpStatus parseStatusLine(String statusLine) { + try { + String[] arr = statusLine.split(" "); + int code = Integer.parseInt(arr[1]); + for (HttpStatus status : values()) { + if (status.getCode() == code) + return status; + } + } catch (Exception e) { + throw new IllegalArgumentException("Invalid status line: " + statusLine, e); + } + throw new IllegalArgumentException("Unkown status code: " + statusLine); + } + + @Override + public String toString() { + return code + " " + reasonPhrase; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java index b7445633b..a4c482663 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java @@ -1,8 +1,8 @@ package org.argeo.cms.internal.auth; -import static org.argeo.util.naming.LdapAttrs.cn; -import static org.argeo.util.naming.LdapAttrs.description; -import static org.argeo.util.naming.LdapAttrs.owner; +import static org.argeo.api.acr.ldap.LdapAttrs.cn; +import static org.argeo.api.acr.ldap.LdapAttrs.description; +import static org.argeo.api.acr.ldap.LdapAttrs.owner; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -25,22 +25,22 @@ import javax.naming.ldap.LdapName; import javax.security.auth.Subject; import org.argeo.api.acr.NamespaceUtils; +import org.argeo.api.acr.ldap.LdapAttrs; +import org.argeo.api.acr.ldap.NamingUtils; import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.directory.HierarchyUnit; +import org.argeo.api.cms.transaction.WorkTransaction; import org.argeo.cms.CmsUserManager; import org.argeo.cms.auth.CurrentUser; import org.argeo.cms.auth.SystemRole; import org.argeo.cms.auth.UserAdminUtils; -import org.argeo.osgi.useradmin.AggregatingUserAdmin; -import org.argeo.osgi.useradmin.TokenUtils; -import org.argeo.osgi.useradmin.UserDirectory; -import org.argeo.util.directory.DirectoryConf; -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.directory.ldap.LdapEntry; -import org.argeo.util.directory.ldap.SharedSecret; -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.NamingUtils; -import org.argeo.util.transaction.WorkTransaction; +import org.argeo.cms.directory.ldap.LdapEntry; +import org.argeo.cms.directory.ldap.SharedSecret; +import org.argeo.cms.osgi.useradmin.AggregatingUserAdmin; +import org.argeo.cms.osgi.useradmin.TokenUtils; +import org.argeo.cms.osgi.useradmin.UserDirectory; +import org.argeo.cms.runtime.DirectoryConf; import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.useradmin.Authorization; import org.osgi.service.useradmin.Group; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/ImpliedByPrincipal.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/ImpliedByPrincipal.java index 73f474637..7472908cd 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/auth/ImpliedByPrincipal.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/auth/ImpliedByPrincipal.java @@ -8,7 +8,7 @@ import java.util.Set; import javax.xml.namespace.QName; -import org.argeo.api.acr.CrName; +import org.argeo.api.acr.ArgeoNamespace; import org.argeo.api.acr.NamespaceUtils; import org.argeo.cms.auth.RoleNameUtils; import org.osgi.service.useradmin.Authorization; @@ -35,7 +35,7 @@ public final class ImpliedByPrincipal implements Principal { this.name = name; String cn = RoleNameUtils.getLastRdnValue(name); roleName = NamespaceUtils.parsePrefixedName(getNamespaceContext(), cn); - if (roleName.getNamespaceURI().equals(CrName.ROLE_NAMESPACE_URI)) { + if (roleName.getNamespaceURI().equals(ArgeoNamespace.ROLE_NAMESPACE_URI)) { systemRole = true; } context = RoleNameUtils.getContext(name); diff --git a/org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java b/org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java index 3358ed825..85f045bae 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java @@ -5,7 +5,7 @@ import java.util.Enumeration; import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; -import org.argeo.util.directory.DirectoryConf; +import org.argeo.cms.runtime.DirectoryConf; import org.osgi.framework.Bundle; import org.osgi.framework.Constants; import org.osgi.framework.ServiceReference; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java index c36f410e2..c80933a55 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java @@ -25,9 +25,9 @@ import org.argeo.cms.dav.DavDepth; import org.argeo.cms.dav.DavHttpHandler; import org.argeo.cms.dav.DavPropfind; import org.argeo.cms.dav.DavResponse; +import org.argeo.cms.http.HttpStatus; import org.argeo.cms.internal.http.RemoteAuthHttpExchange; -import org.argeo.util.StreamUtils; -import org.argeo.util.http.HttpStatus; +import org.argeo.cms.util.StreamUtils; import com.sun.net.httpserver.HttpExchange; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java index 6921de77a..5c3838a0a 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java @@ -38,7 +38,7 @@ import org.argeo.api.cms.CmsState; import org.argeo.api.uuid.UuidFactory; import org.argeo.cms.CmsDeployProperty; import org.argeo.cms.auth.ident.IdentClient; -import org.argeo.util.FsUtils; +import org.argeo.cms.util.FsUtils; /** * Implementation of a {@link CmsState}, initialising the required services. diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java index 7f4314b99..6aa490a69 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java @@ -29,14 +29,14 @@ import org.argeo.api.cms.CmsAuth; import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsState; +import org.argeo.api.cms.transaction.WorkControl; +import org.argeo.api.cms.transaction.WorkTransaction; import org.argeo.cms.CmsDeployProperty; -import org.argeo.osgi.useradmin.AggregatingUserAdmin; -import org.argeo.osgi.useradmin.DirectoryUserAdmin; -import org.argeo.osgi.useradmin.UserDirectory; -import org.argeo.util.directory.DirectoryConf; -import org.argeo.util.naming.dns.DnsBrowser; -import org.argeo.util.transaction.WorkControl; -import org.argeo.util.transaction.WorkTransaction; +import org.argeo.cms.dns.DnsBrowser; +import org.argeo.cms.osgi.useradmin.AggregatingUserAdmin; +import org.argeo.cms.osgi.useradmin.DirectoryUserAdmin; +import org.argeo.cms.osgi.useradmin.UserDirectory; +import org.argeo.cms.runtime.DirectoryConf; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/BundleCmsTheme.java b/org.argeo.cms/src/org/argeo/cms/osgi/BundleCmsTheme.java index 3443d73a6..f60d3352e 100644 --- a/org.argeo.cms/src/org/argeo/cms/osgi/BundleCmsTheme.java +++ b/org.argeo.cms/src/org/argeo/cms/osgi/BundleCmsTheme.java @@ -17,7 +17,7 @@ import java.util.TreeSet; import java.util.stream.Collectors; import org.argeo.api.cms.ux.CmsTheme; -import org.argeo.util.StreamUtils; +import org.argeo.cms.util.StreamUtils; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/FilterRequirement.java b/org.argeo.cms/src/org/argeo/cms/osgi/FilterRequirement.java new file mode 100644 index 000000000..5582c3481 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/FilterRequirement.java @@ -0,0 +1,42 @@ +package org.argeo.cms.osgi; + +import java.util.HashMap; +import java.util.Map; + +import org.osgi.resource.Namespace; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +/** Simplify filtering resources. */ +public class FilterRequirement implements Requirement { + private String namespace; + private String filter; + + public FilterRequirement(String namespace, String filter) { + this.namespace = namespace; + this.filter = filter; + } + + @Override + public Resource getResource() { + return null; + } + + @Override + public String getNamespace() { + return namespace; + } + + @Override + public Map getDirectives() { + Map directives = new HashMap<>(); + directives.put(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter); + return directives; + } + + @Override + public Map getAttributes() { + return new HashMap<>(); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingAuthorization.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingAuthorization.java new file mode 100644 index 000000000..50131758d --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingAuthorization.java @@ -0,0 +1,77 @@ +package org.argeo.cms.osgi.useradmin; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.security.auth.x500.X500Principal; + +import org.osgi.service.useradmin.Authorization; + +/** An {@link Authorization} which combines roles form various auth sources. */ +class AggregatingAuthorization implements Authorization { + private final String name; + private final String displayName; + private final Set systemRoles; + private final Set roles; + + public AggregatingAuthorization(String name, String displayName, Set systemRoles, String[] roles) { + this.name = new X500Principal(name).getName(); + this.displayName = displayName; + this.systemRoles = Collections.unmodifiableSet(new HashSet<>(systemRoles)); + Set temp = new HashSet<>(); + for (String role : roles) { + if (!temp.contains(role)) + temp.add(role); + } + this.roles = Collections.unmodifiableSet(temp); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean hasRole(String name) { + if (systemRoles.contains(name)) + return true; + if (roles.contains(name)) + return true; + return false; + } + + @Override + public String[] getRoles() { + int size = systemRoles.size() + roles.size(); + List res = new ArrayList(size); + res.addAll(systemRoles); + res.addAll(roles); + return res.toArray(new String[size]); + } + + @Override + public int hashCode() { + if (name == null) + return super.hashCode(); + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Authorization)) + return false; + Authorization that = (Authorization) obj; + if (name == null) + return that.getName() == null; + return name.equals(that.getName()); + } + + @Override + public String toString() { + return displayName; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingUserAdmin.java new file mode 100644 index 000000000..2d438cbf0 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingUserAdmin.java @@ -0,0 +1,328 @@ +package org.argeo.cms.osgi.useradmin; + +import static org.argeo.cms.osgi.useradmin.DirectoryUserAdmin.toLdapName; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +import org.argeo.cms.runtime.DirectoryConf; +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; + +/** + * Aggregates multiple {@link UserDirectory} and integrates them with system + * roles. + */ +public class AggregatingUserAdmin implements UserAdmin { + private final LdapName systemRolesBaseDn; + private final LdapName tokensBaseDn; + + // DAOs + private DirectoryUserAdmin systemRoles = null; + private DirectoryUserAdmin tokens = null; + private Map businessRoles = new HashMap(); + + // TODO rather use an empty constructor and an init method + public AggregatingUserAdmin(String systemRolesBaseDn, String tokensBaseDn) { + try { + this.systemRolesBaseDn = new LdapName(systemRolesBaseDn); + if (tokensBaseDn != null) + this.tokensBaseDn = new LdapName(tokensBaseDn); + else + this.tokensBaseDn = null; + } catch (InvalidNameException e) { + throw new IllegalStateException("Cannot initialize " + AggregatingUserAdmin.class, e); + } + } + + @Override + public Role createRole(String name, int type) { + return findUserAdmin(name).createRole(name, type); + } + + @Override + public boolean removeRole(String name) { + boolean actuallyDeleted = findUserAdmin(name).removeRole(name); + systemRoles.removeRole(name); + return actuallyDeleted; + } + + @Override + public Role getRole(String name) { + return findUserAdmin(name).getRole(name); + } + + @Override + public Role[] getRoles(String filter) throws InvalidSyntaxException { + List res = new ArrayList(); + for (UserAdmin userAdmin : businessRoles.values()) { + res.addAll(Arrays.asList(userAdmin.getRoles(filter))); + } + res.addAll(Arrays.asList(systemRoles.getRoles(filter))); + return res.toArray(new Role[res.size()]); + } + + @Override + public User getUser(String key, String value) { + List res = new ArrayList(); + for (UserAdmin userAdmin : businessRoles.values()) { + User u = userAdmin.getUser(key, value); + if (u != null) + res.add(u); + } + // Note: node roles cannot contain users, so it is not searched + return res.size() == 1 ? res.get(0) : null; + } + + /** Builds an authorisation by scanning all referentials. */ + @Override + public Authorization getAuthorization(User user) { + if (user == null) {// anonymous + return systemRoles.getAuthorization(null); + } + DirectoryUserAdmin userReferentialOfThisUser = findUserAdmin(user.getName()); + Authorization rawAuthorization = userReferentialOfThisUser.getAuthorization(user); + User retrievedUser = (User) userReferentialOfThisUser.getRole(user.getName()); + String usernameToUse; + String displayNameToUse; + if (user instanceof Group) { + // TODO check whether this is still working + String ownerDn = TokenUtils.userDn((Group) user); + if (ownerDn != null) {// tokens + UserAdmin ownerUserAdmin = findUserAdmin(ownerDn); + User ownerUser = (User) ownerUserAdmin.getRole(ownerDn); + usernameToUse = ownerDn; + displayNameToUse = LdifAuthorization.extractDisplayName(ownerUser); + } else { + usernameToUse = rawAuthorization.getName(); + displayNameToUse = rawAuthorization.toString(); + } + } else {// regular users + usernameToUse = rawAuthorization.getName(); + displayNameToUse = rawAuthorization.toString(); + } + + // gather roles from other referentials + List rawRoles = Arrays.asList(rawAuthorization.getRoles()); + List allRoles = new ArrayList<>(rawRoles); + for (LdapName otherBaseDn : businessRoles.keySet()) { + if (otherBaseDn.equals(userReferentialOfThisUser.getBaseDn())) + continue; + DirectoryUserAdmin otherUserAdmin = userAdminToUse(user, businessRoles.get(otherBaseDn)); + if (otherUserAdmin == null) + continue; + for (String roleStr : rawRoles) { + User role = (User) findUserAdmin(roleStr).getRole(roleStr); + Authorization auth = otherUserAdmin.getAuthorization(role); + allRoles.addAll(Arrays.asList(auth.getRoles())); + } + + } + + // integrate system roles + final DirectoryUserAdmin userAdminToUse = userAdminToUse(retrievedUser, userReferentialOfThisUser); + Objects.requireNonNull(userAdminToUse); + + try { + Set sysRoles = new HashSet(); + for (String role : rawAuthorization.getRoles()) { + User userOrGroup = (User) userAdminToUse.getRole(role); + Authorization auth = systemRoles.getAuthorization(userOrGroup); + systemRoles: for (String systemRole : auth.getRoles()) { + if (role.equals(systemRole)) + continue systemRoles; + sysRoles.add(systemRole); + } +// sysRoles.addAll(Arrays.asList(auth.getRoles())); + } + addAbstractSystemRoles(rawAuthorization, sysRoles); + Authorization authorization = new AggregatingAuthorization(usernameToUse, displayNameToUse, sysRoles, + allRoles.toArray(new String[allRoles.size()])); + return authorization; + } finally { + if (userAdminToUse != null && userAdminToUse.isScoped()) { + userAdminToUse.destroy(); + } + } + } + + /** Decide whether to scope or not */ + private DirectoryUserAdmin userAdminToUse(User user, DirectoryUserAdmin userAdmin) { + if (userAdmin.isAuthenticated()) + return userAdmin; + if (user instanceof DirectoryUser) { + return userAdmin; + } else if (user instanceof AuthenticatingUser) { + return userAdmin.scope(user).orElse(null); + } else { + throw new IllegalArgumentException("Unsupported user type " + user.getClass()); + } + + } + + /** + * Enrich with application-specific roles which are strictly programmatic, such + * as anonymous/user semantics. + */ + protected void addAbstractSystemRoles(Authorization rawAuthorization, Set sysRoles) { + + } + + // + // USER ADMIN AGGREGATOR + // + protected void addUserDirectory(UserDirectory ud) { + if (!(ud instanceof DirectoryUserAdmin)) + throw new IllegalArgumentException("Only " + DirectoryUserAdmin.class.getName() + " is supported"); + DirectoryUserAdmin userDirectory = (DirectoryUserAdmin) ud; + String basePath = userDirectory.getBase(); + if (isSystemRolesBaseDn(basePath)) { + this.systemRoles = userDirectory; + systemRoles.setExternalRoles(this); + } else if (isTokensBaseDn(basePath)) { + this.tokens = userDirectory; + tokens.setExternalRoles(this); + } else { + LdapName baseDn = toLdapName(basePath); + if (businessRoles.containsKey(baseDn)) + throw new IllegalStateException("There is already a user admin for " + baseDn); + businessRoles.put(baseDn, userDirectory); + } + userDirectory.init(); + postAdd(userDirectory); + } + + /** Called after a new user directory has been added */ + protected void postAdd(UserDirectory userDirectory) { + } + + private DirectoryUserAdmin findUserAdmin(String name) { + try { + return findUserAdmin(new LdapName(name)); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Badly formatted name " + name, e); + } + } + + private DirectoryUserAdmin findUserAdmin(LdapName name) { + if (name.startsWith(systemRolesBaseDn)) + return systemRoles; + if (tokensBaseDn != null && name.startsWith(tokensBaseDn)) + return tokens; + List res = new ArrayList<>(1); + userDirectories: for (LdapName baseDn : businessRoles.keySet()) { + DirectoryUserAdmin userDirectory = businessRoles.get(baseDn); + if (name.startsWith(baseDn)) { + if (userDirectory.isDisabled()) + continue userDirectories; +// if (res.isEmpty()) { + res.add(userDirectory); +// } else { +// for (AbstractUserDirectory ud : res) { +// LdapName bd = ud.getBaseDn(); +// if (userDirectory.getBaseDn().startsWith(bd)) { +// // child user directory +// } +// } +// } + } + } + if (res.size() == 0) + throw new IllegalStateException("Cannot find user admin for " + name); + if (res.size() > 1) + throw new IllegalStateException("Multiple user admin found for " + name); + return res.get(0); + } + + protected boolean isSystemRolesBaseDn(String basePath) { + return toLdapName(basePath).equals(systemRolesBaseDn); + } + + protected boolean isTokensBaseDn(String basePath) { + return tokensBaseDn != null && toLdapName(basePath).equals(tokensBaseDn); + } + +// protected Dictionary currentState() { +// Dictionary res = new Hashtable(); +// // res.put(NodeConstants.CN, NodeConstants.DEFAULT); +// for (LdapName name : businessRoles.keySet()) { +// AbstractUserDirectory userDirectory = businessRoles.get(name); +// String uri = UserAdminConf.propertiesAsUri(userDirectory.getProperties()).toString(); +// res.put(uri, ""); +// } +// return res; +// } + + public void start() { + if (systemRoles == null) { + // TODO do we really need separate system roles? + Hashtable properties = new Hashtable<>(); + properties.put(DirectoryConf.baseDn.name(), "ou=roles,ou=system"); + systemRoles = new DirectoryUserAdmin(properties); + } + } + + public void stop() { + for (LdapName name : businessRoles.keySet()) { + DirectoryUserAdmin userDirectory = businessRoles.get(name); + destroy(userDirectory); + } + businessRoles.clear(); + businessRoles = null; + destroy(systemRoles); + systemRoles = null; + } + + private void destroy(DirectoryUserAdmin userDirectory) { + preDestroy(userDirectory); + userDirectory.destroy(); + } + +// protected void removeUserDirectory(UserDirectory userDirectory) { +// LdapName baseDn = toLdapName(userDirectory.getContext()); +// businessRoles.remove(baseDn); +// if (userDirectory instanceof DirectoryUserAdmin) +// destroy((DirectoryUserAdmin) userDirectory); +// } + + @Deprecated + protected void removeUserDirectory(String basePath) { + if (isSystemRolesBaseDn(basePath)) + throw new IllegalArgumentException("System roles cannot be removed "); + LdapName baseDn = toLdapName(basePath); + if (!businessRoles.containsKey(baseDn)) + throw new IllegalStateException("No user directory registered for " + baseDn); + DirectoryUserAdmin userDirectory = businessRoles.remove(baseDn); + destroy(userDirectory); + } + + /** + * Called before each user directory is destroyed, so that additional actions + * can be performed. + */ + protected void preDestroy(UserDirectory userDirectory) { + } + + public Set getUserDirectories() { + TreeSet res = new TreeSet<>((o1, o2) -> o1.getBase().compareTo(o2.getBase())); + res.addAll(businessRoles.values()); + res.add(systemRoles); + return res; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AuthenticatingUser.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AuthenticatingUser.java new file mode 100644 index 000000000..b87dc9bf4 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AuthenticatingUser.java @@ -0,0 +1,83 @@ +package org.argeo.cms.osgi.useradmin; + +import java.util.Dictionary; +import java.util.Hashtable; + +import javax.naming.ldap.LdapName; + +import org.argeo.api.cms.directory.DirectoryDigestUtils; +import org.osgi.service.useradmin.User; + +/** + * A special user type used during authentication in order to provide the + * credentials required for scoping the user admin. + */ +public class AuthenticatingUser implements User { + /** From com.sun.security.auth.module.*LoginModule */ + public final static String SHARED_STATE_NAME = "javax.security.auth.login.name"; + /** From com.sun.security.auth.module.*LoginModule */ + public final static String SHARED_STATE_PWD = "javax.security.auth.login.password"; + + private final String name; + private final Dictionary credentials; + + public AuthenticatingUser(LdapName name) { + if (name == null) + throw new NullPointerException("Provided name cannot be null."); + this.name = name.toString(); + this.credentials = new Hashtable<>(); + } + + public AuthenticatingUser(String name, Dictionary credentials) { + this.name = name; + this.credentials = credentials; + } + + public AuthenticatingUser(String name, char[] password) { + if (name == null) + throw new NullPointerException("Provided name cannot be null."); + this.name = name; + credentials = new Hashtable<>(); + credentials.put(SHARED_STATE_NAME, name); + byte[] pwd = DirectoryDigestUtils.charsToBytes(password); + credentials.put(SHARED_STATE_PWD, pwd); + } + + @Override + public String getName() { + return name; + } + + @Override + public int getType() { + return User.USER; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Dictionary getProperties() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Dictionary getCredentials() { + return credentials; + } + + @Override + public boolean hasCredential(String key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "Authenticating user " + name; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryGroup.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryGroup.java new file mode 100644 index 000000000..d372c0507 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryGroup.java @@ -0,0 +1,8 @@ +package org.argeo.cms.osgi.useradmin; + +import org.osgi.service.useradmin.Group; + +/** A group in a user directroy. */ +interface DirectoryGroup extends Group, DirectoryUser { +// List getMemberNames(); +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUser.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUser.java new file mode 100644 index 000000000..8fe0af654 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUser.java @@ -0,0 +1,7 @@ +package org.argeo.cms.osgi.useradmin; + +import org.osgi.service.useradmin.User; + +/** A user in a user directory. */ +interface DirectoryUser extends User { +} 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); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifAuthorization.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifAuthorization.java new file mode 100644 index 000000000..3bedeab05 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifAuthorization.java @@ -0,0 +1,85 @@ +package org.argeo.cms.osgi.useradmin; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Dictionary; +import java.util.List; + +import org.argeo.api.acr.ldap.LdapAttrs; +import org.osgi.service.useradmin.Authorization; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; + +/** Basic authorization. */ +class LdifAuthorization implements Authorization { + private final String name; + private final String displayName; + private final List allRoles; + + public LdifAuthorization(User user, List allRoles) { + if (user == null) { + this.name = null; + this.displayName = "anonymous"; + } else { + this.name = user.getName(); + this.displayName = extractDisplayName(user); + } + // roles + String[] roles = new String[allRoles.size()]; + for (int i = 0; i < allRoles.size(); i++) { + roles[i] = allRoles.get(i).getName(); + } + this.allRoles = Collections.unmodifiableList(Arrays.asList(roles)); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean hasRole(String name) { + return allRoles.contains(name); + } + + @Override + public String[] getRoles() { + return allRoles.toArray(new String[allRoles.size()]); + } + + @Override + public int hashCode() { + if (name == null) + return super.hashCode(); + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Authorization)) + return false; + Authorization that = (Authorization) obj; + if (name == null) + return that.getName() == null; + return name.equals(that.getName()); + } + + @Override + public String toString() { + return displayName; + } + + final static String extractDisplayName(User user) { + Dictionary props = user.getProperties(); + Object displayName = props.get(LdapAttrs.displayName.name()); + if (displayName == null) + displayName = props.get(LdapAttrs.cn.name()); + if (displayName == null) + displayName = props.get(LdapAttrs.uid.name()); + if (displayName == null) + displayName = user.getName(); + if (displayName == null) + throw new IllegalStateException("Cannot set display name for " + user); + return displayName.toString(); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifGroup.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifGroup.java new file mode 100644 index 000000000..882f34a17 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifGroup.java @@ -0,0 +1,127 @@ +package org.argeo.cms.osgi.useradmin; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.InvalidNameException; +import javax.naming.directory.Attribute; +import javax.naming.ldap.LdapName; + +import org.argeo.cms.directory.ldap.AbstractLdapDirectory; +import org.osgi.service.useradmin.Role; + +/** Directory group implementation */ +class LdifGroup extends LdifUser implements DirectoryGroup { + private final String memberAttributeId; + + LdifGroup(AbstractLdapDirectory userAdmin, LdapName dn) { + super(userAdmin, dn); + memberAttributeId = userAdmin.getMemberAttributeId(); + } + + @Override + public boolean addMember(Role role) { + try { + Role foundRole = findRole(new LdapName(role.getName())); + if (foundRole == null) + throw new UnsupportedOperationException( + "Adding role " + role.getName() + " is unsupported within this context."); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Role name" + role.getName() + " is badly formatted"); + } + + getUserAdmin().checkEdit(); + if (!isEditing()) + startEditing(); + + Attribute member = getAttributes().get(memberAttributeId); + if (member != null) { + if (member.contains(role.getName())) + return false; + else + member.add(role.getName()); + } else + getAttributes().put(memberAttributeId, role.getName()); + return true; + } + + @Override + public boolean addRequiredMember(Role role) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeMember(Role role) { + getUserAdmin().checkEdit(); + if (!isEditing()) + startEditing(); + + Attribute member = getAttributes().get(memberAttributeId); + if (member != null) { + if (!member.contains(role.getName())) + return false; + member.remove(role.getName()); + return true; + } else + return false; + } + + @Override + public Role[] getMembers() { + List directMembers = new ArrayList(); + for (LdapName ldapName : getReferences(memberAttributeId)) { + Role role = findRole(ldapName); + if (role == null) { + throw new IllegalStateException("Role " + ldapName + " not found."); + } + directMembers.add(role); + } + return directMembers.toArray(new Role[directMembers.size()]); + } + + /** + * Whether a role with this name can be found from this context. + * + * @return The related {@link Role} or null. + */ + protected Role findRole(LdapName ldapName) { + Role role = getUserAdmin().getRole(ldapName.toString()); + if (role == null) { + if (getUserAdmin().getExternalRoles() != null) + role = getUserAdmin().getExternalRoles().getRole(ldapName.toString()); + } + return role; + } + +// @Override +// public List getMemberNames() { +// Attribute memberAttribute = getAttributes().get(memberAttributeId); +// if (memberAttribute == null) +// return new ArrayList(); +// try { +// List roles = new ArrayList(); +// NamingEnumeration values = memberAttribute.getAll(); +// while (values.hasMore()) { +// LdapName dn = new LdapName(values.next().toString()); +// roles.add(dn); +// } +// return roles; +// } catch (NamingException e) { +// throw new IllegalStateException("Cannot get members", e); +// } +// } + + @Override + public Role[] getRequiredMembers() { + throw new UnsupportedOperationException(); + } + + @Override + public int getType() { + return GROUP; + } + + protected DirectoryUserAdmin getUserAdmin() { + return (DirectoryUserAdmin) getDirectory(); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifUser.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifUser.java new file mode 100644 index 000000000..2341ec430 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifUser.java @@ -0,0 +1,24 @@ +package org.argeo.cms.osgi.useradmin; + +import javax.naming.ldap.LdapName; + +import org.argeo.cms.directory.ldap.AbstractLdapDirectory; +import org.argeo.cms.directory.ldap.DefaultLdapEntry; + +/** Directory user implementation */ +class LdifUser extends DefaultLdapEntry implements DirectoryUser { + LdifUser(AbstractLdapDirectory userAdmin, LdapName dn) { + super(userAdmin, dn); + } + + @Override + public String getName() { + return getDn().toString(); + } + + @Override + public int getType() { + return USER; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserDirectory.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserDirectory.java new file mode 100644 index 000000000..950a401db --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserDirectory.java @@ -0,0 +1,111 @@ +package org.argeo.cms.osgi.useradmin; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.argeo.api.acr.ldap.LdapAttrs; +import org.argeo.api.cms.directory.HierarchyUnit; +import org.argeo.cms.directory.ldap.AbstractLdapDirectory; +import org.argeo.cms.directory.ldap.AbstractLdapDirectoryDao; +import org.argeo.cms.directory.ldap.LdapEntry; +import org.argeo.cms.directory.ldap.LdapEntryWorkingCopy; + +/** Pseudo user directory to be used when logging in as OS user. */ +public class OsUserDirectory extends AbstractLdapDirectoryDao { + private final String osUsername = System.getProperty("user.name"); + private final LdapName osUserDn; + private final LdapEntry osUser; + + public OsUserDirectory(AbstractLdapDirectory directory) { + super(directory); + try { + osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + directory.getUserBaseRdn() + "," + + directory.getBaseDn()); +// Attributes attributes = new BasicAttributes(); +// attributes.put(LdapAttrs.uid.name(), osUsername); + osUser = newUser(osUserDn); + } catch (NamingException e) { + throw new IllegalStateException("Cannot create system user", e); + } + } + + @Override + public List getDirectGroups(LdapName dn) { + return new ArrayList<>(); + } + + @Override + public boolean entryExists(LdapName dn) { + return osUserDn.equals(dn); + } + + @Override + public boolean checkConnection() { + return true; + } + + @Override + public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException { + if (osUserDn.equals(key)) + return osUser; + else + throw new NameNotFoundException("Not an OS role"); + } + + @Override + public List doGetEntries(LdapName searchBase, String f, boolean deep) { + List res = new ArrayList<>(); +// if (f == null || f.match(osUser.getProperties())) + res.add(osUser); + return res; + } + + @Override + public HierarchyUnit doGetHierarchyUnit(LdapName dn) { + return null; + } + + @Override + public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { + return new ArrayList<>(); + } + + public void prepare(LdapEntryWorkingCopy wc) { + + } + + public void commit(LdapEntryWorkingCopy wc) { + + } + + public void rollback(LdapEntryWorkingCopy wc) { + + } + + @Override + public void init() { + // TODO Auto-generated method stub + + } + + @Override + public void destroy() { + // TODO Auto-generated method stub + + } + + @Override + public Attributes doGetAttributes(LdapName name) { + try { + return doGetEntry(name).getAttributes(); + } catch (NameNotFoundException e) { + throw new IllegalStateException(name + " doe not exist in " + getDirectory().getBaseDn(), e); + } + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserUtils.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserUtils.java new file mode 100644 index 000000000..f71878060 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserUtils.java @@ -0,0 +1,54 @@ +package org.argeo.cms.osgi.useradmin; + +import java.net.URISyntaxException; +import java.net.URL; +import java.security.NoSuchAlgorithmException; +import java.security.URIParameter; + +import javax.security.auth.Subject; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +/** Log in based on JDK-provided OS integration. */ +public class OsUserUtils { + private final static String LOGIN_CONTEXT_USER_NIX = "USER_NIX"; + private final static String LOGIN_CONTEXT_USER_NT = "USER_NT"; + + public static String getOsUsername() { + return System.getProperty("user.name"); + } + + public static LoginContext loginAsSystemUser(Subject subject) { + try { + URL jaasConfigurationUrl = OsUserUtils.class.getClassLoader() + .getResource("org/argeo/osgi/useradmin/jaas-os.cfg"); + URIParameter uriParameter = new URIParameter(jaasConfigurationUrl.toURI()); + Configuration jaasConfiguration = Configuration.getInstance("JavaLoginConfig", uriParameter); + LoginContext lc = new LoginContext(isWindows() ? LOGIN_CONTEXT_USER_NT : LOGIN_CONTEXT_USER_NIX, subject, + null, jaasConfiguration); + lc.login(); + return lc; + } catch (URISyntaxException | NoSuchAlgorithmException | LoginException e) { + throw new RuntimeException("Cannot login as system user", e); + } + } + + public static void main(String args[]) { + Subject subject = new Subject(); + LoginContext loginContext = loginAsSystemUser(subject); + System.out.println(subject); + try { + loginContext.logout(); + } catch (LoginException e) { + // silent + } + } + + private static boolean isWindows() { + return System.getProperty("os.name").startsWith("Windows"); + } + + private OsUserUtils() { + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/TokenUtils.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/TokenUtils.java new file mode 100644 index 000000000..eb9429866 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/TokenUtils.java @@ -0,0 +1,87 @@ +package org.argeo.cms.osgi.useradmin; + +import static org.argeo.api.acr.ldap.LdapAttrs.description; +import static org.argeo.api.acr.ldap.LdapAttrs.owner; + +import java.security.Principal; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.security.auth.Subject; + +import org.argeo.api.acr.ldap.NamingUtils; +import org.osgi.service.useradmin.Group; + +/** + * Canonically implements the Argeo token conventions. + */ +public class TokenUtils { + public static Set tokensUsed(Subject subject, String tokensBaseDn) { + Set res = new HashSet<>(); + for (Principal principal : subject.getPrincipals()) { + String name = principal.getName(); + if (name.endsWith(tokensBaseDn)) { + try { + LdapName ldapName = new LdapName(name); + String token = ldapName.getRdn(ldapName.size()).getValue().toString(); + res.add(token); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Invalid principal " + principal, e); + } + } + } + return res; + } + + /** The user related to this token group */ + public static String userDn(Group tokenGroup) { + return (String) tokenGroup.getProperties().get(owner.name()); + } + + public static boolean isExpired(Group tokenGroup) { + return isExpired(tokenGroup, Instant.now()); + + } + + public static boolean isExpired(Group tokenGroup, Instant instant) { + String expiryDateStr = (String) tokenGroup.getProperties().get(description.name()); + if (expiryDateStr != null) { + Instant expiryDate = NamingUtils.ldapDateToInstant(expiryDateStr); + if (expiryDate.isBefore(instant)) { + return true; + } + } + return false; + } + +// private final String token; +// +// public TokenUtils(String token) { +// this.token = token; +// } +// +// public String getToken() { +// return token; +// } +// +// @Override +// public int hashCode() { +// return token.hashCode(); +// } +// +// @Override +// public boolean equals(Object obj) { +// if ((obj instanceof TokenUtils) && ((TokenUtils) obj).token.equals(token)) +// return true; +// return false; +// } +// +// @Override +// public String toString() { +// return "Token #" + hashCode(); +// } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/UserDirectory.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/UserDirectory.java new file mode 100644 index 000000000..463316ba1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/UserDirectory.java @@ -0,0 +1,19 @@ +package org.argeo.cms.osgi.useradmin; + +import org.argeo.api.cms.directory.Directory; +import org.argeo.api.cms.directory.HierarchyUnit; +import org.osgi.service.useradmin.Role; + +/** Information about a user directory. */ +public interface UserDirectory extends Directory { + + HierarchyUnit getHierarchyUnit(Role role); + + Iterable getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep); + + String getRolePath(Role role); + + String getRoleSimpleName(Role role); + + Role getRoleByPath(String path); +} diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/jaas-os.cfg b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/jaas-os.cfg new file mode 100644 index 000000000..da04505a7 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/jaas-os.cfg @@ -0,0 +1,8 @@ +USER_NIX { + com.sun.security.auth.module.UnixLoginModule requisite; +}; + +USER_NT { + com.sun.security.auth.module.NTLoginModule requisite; +}; + diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/package-info.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/package-info.java new file mode 100644 index 000000000..766c59b3e --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/package-info.java @@ -0,0 +1,2 @@ +/** LDAP and LDIF based OSGi useradmin implementation. */ +package org.argeo.cms.osgi.useradmin; \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/runtime/DirectoryConf.java b/org.argeo.cms/src/org/argeo/cms/runtime/DirectoryConf.java new file mode 100644 index 000000000..a4e44ccaf --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/runtime/DirectoryConf.java @@ -0,0 +1,247 @@ +package org.argeo.cms.runtime; + +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import org.argeo.api.acr.ldap.NamingUtils; +import org.argeo.api.cms.directory.DirectoryDigestUtils; +import org.argeo.cms.directory.ldap.IpaUtils; + +/** Properties used to configure user admins. */ +public enum DirectoryConf { + /** Base DN (cannot be configured externally) */ + baseDn(null), + + /** URI of the underlying resource (cannot be configured externally) */ + uri(null), + + /** User objectClass */ + userObjectClass("inetOrgPerson"), + + /** Relative base DN for users */ + userBase("ou=People"), + + /** Groups objectClass */ + groupObjectClass("groupOfNames"), + + /** Relative base DN for users */ + groupBase("ou=Groups"), + + /** Relative base DN for users */ + systemRoleBase("ou=Roles"), + + /** Read-only source */ + readOnly(null), + + /** Disabled source */ + disabled(null), + + /** Authentication realm */ + realm(null), + + /** Override all passwords with this value (typically for testing purposes) */ + forcedPassword(null); + + public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config"; + + public final static String SCHEME_LDAP = "ldap"; + public final static String SCHEME_LDAPS = "ldaps"; + public final static String SCHEME_FILE = "file"; + public final static String SCHEME_OS = "os"; + public final static String SCHEME_IPA = "ipa"; + + private final static String SECURITY_PRINCIPAL = "java.naming.security.principal"; + private final static String SECURITY_CREDENTIALS = "java.naming.security.credentials"; + + /** The default value. */ + private Object def; + + DirectoryConf(Object def) { + this.def = def; + } + + public Object getDefault() { + return def; + } + + /** + * For use as Java property. + * + * @deprecated use {@link #name()} instead + */ + @Deprecated + public String property() { + return name(); + } + + public String getValue(Dictionary properties) { + Object res = getRawValue(properties); + if (res == null) + return null; + return res.toString(); + } + + @SuppressWarnings("unchecked") + public T getRawValue(Dictionary properties) { + Object res = properties.get(name()); + if (res == null) + res = getDefault(); + return (T) res; + } + + /** @deprecated use {@link #valueOf(String)} instead */ + @Deprecated + public static DirectoryConf local(String property) { + return DirectoryConf.valueOf(property); + } + + /** Hides host and credentials. */ + public static URI propertiesAsUri(Dictionary properties) { + StringBuilder query = new StringBuilder(); + + boolean first = true; +// for (Enumeration keys = properties.keys(); keys.hasMoreElements();) { +// String key = keys.nextElement(); +// // TODO clarify which keys are relevant (list only the enum?) +// if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn") +// && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name()) +// && !key.equals(uri.name()) && !key.equals(Constants.OBJECTCLASS) +// && !key.equals(Constants.SERVICE_ID) && !key.equals("bundle.id")) { +// if (first) +// first = false; +// else +// query.append('&'); +// query.append(valueOf(key).name()); +// query.append('=').append(properties.get(key).toString()); +// } +// } + + keys: for (DirectoryConf key : DirectoryConf.values()) { + if (key.equals(baseDn) || key.equals(uri)) + continue keys; + Object value = properties.get(key.name()); + if (value == null) + continue keys; + if (first) + first = false; + else + query.append('&'); + query.append(key.name()); + query.append('=').append(value.toString()); + + } + + Object bDnObj = properties.get(baseDn.name()); + String bDn = bDnObj != null ? bDnObj.toString() : null; + try { + return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null, + null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot create URI from properties", e); + } + } + + public static Dictionary uriAsProperties(String uriStr) { + try { + Hashtable res = new Hashtable(); + URI u = new URI(uriStr); + String scheme = u.getScheme(); + if (scheme != null && scheme.equals(SCHEME_IPA)) { + return IpaUtils.convertIpaUri(u); +// scheme = u.getScheme(); + } + String path = u.getPath(); + // base DN + String bDn = path.substring(path.lastIndexOf('/') + 1, path.length()); + if (bDn.equals("") && SCHEME_OS.equals(scheme)) { + bDn = getBaseDnFromHostname(); + } + + if (bDn.endsWith(".ldif")) + bDn = bDn.substring(0, bDn.length() - ".ldif".length()); + + // Normalize base DN as LDAP name +// bDn = new LdapName(bDn).toString(); + + String principal = null; + String credentials = null; + if (scheme != null) + if (scheme.equals(SCHEME_LDAP) || scheme.equals(SCHEME_LDAPS)) { + // TODO additional checks + if (u.getUserInfo() != null) { + String[] userInfo = u.getUserInfo().split(":"); + principal = userInfo.length > 0 ? userInfo[0] : null; + credentials = userInfo.length > 1 ? userInfo[1] : null; + } + } else if (scheme.equals(SCHEME_FILE)) { + } else if (scheme.equals(SCHEME_IPA)) { + } else if (scheme.equals(SCHEME_OS)) { + } else + throw new IllegalArgumentException("Unsupported scheme " + scheme); + Map> query = NamingUtils.queryToMap(u); + for (String key : query.keySet()) { + DirectoryConf ldapProp = DirectoryConf.valueOf(key); + List values = query.get(key); + if (values.size() == 1) { + res.put(ldapProp.name(), values.get(0)); + } else { + throw new IllegalArgumentException("Only single values are supported"); + } + } + res.put(baseDn.name(), bDn); + if (SCHEME_OS.equals(scheme)) + res.put(readOnly.name(), "true"); + if (principal != null) + res.put(SECURITY_PRINCIPAL, principal); + if (credentials != null) + res.put(SECURITY_CREDENTIALS, credentials); + if (scheme != null) {// relative URIs are dealt with externally + if (SCHEME_OS.equals(scheme)) { + res.put(uri.name(), SCHEME_OS + ":///"); + } else { + URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(), + scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null); + res.put(uri.name(), bareUri.toString()); + } + } + return res; + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot convert " + uri + " to properties", e); + } + } + + private static String getBaseDnFromHostname() { + String hostname; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + hostname = "localhost.localdomain"; + } + int dotIdx = hostname.indexOf('.'); + if (dotIdx >= 0) { + String domain = hostname.substring(dotIdx + 1, hostname.length()); + String bDn = ("." + domain).replaceAll("\\.", ",dc="); + bDn = bDn.substring(1, bDn.length()); + return bDn; + } else { + return "dc=" + hostname; + } + } + + /** + * Hash the base DN in order to have a deterministic string to be used as a cn + * for the underlying user directory. + */ + public static String baseDnHash(Dictionary properties) { + String bDn = (String) properties.get(baseDn.name()); + if (bDn == null) + throw new IllegalStateException("No baseDn in " + properties); + return DirectoryDigestUtils.sha1str(bDn); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java b/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java index e473d2799..0034e3f7b 100644 --- a/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java +++ b/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java @@ -9,6 +9,12 @@ import org.argeo.api.acr.spi.ProvidedRepository; import org.argeo.api.cms.CmsContext; import org.argeo.api.cms.CmsDeployment; import org.argeo.api.cms.CmsState; +import org.argeo.api.cms.transaction.SimpleTransactionManager; +import org.argeo.api.cms.transaction.WorkControl; +import org.argeo.api.cms.transaction.WorkTransaction; +import org.argeo.api.register.Component; +import org.argeo.api.register.ComponentRegister; +import org.argeo.api.register.SimpleRegister; import org.argeo.api.uuid.UuidFactory; import org.argeo.cms.CmsUserManager; import org.argeo.cms.acr.CmsUuidFactory; @@ -18,12 +24,6 @@ import org.argeo.cms.internal.runtime.CmsDeploymentImpl; import org.argeo.cms.internal.runtime.CmsStateImpl; import org.argeo.cms.internal.runtime.CmsUserAdmin; import org.argeo.cms.internal.runtime.DeployedContentRepository; -import org.argeo.util.register.Component; -import org.argeo.util.register.ComponentRegister; -import org.argeo.util.register.SimpleRegister; -import org.argeo.util.transaction.SimpleTransactionManager; -import org.argeo.util.transaction.WorkControl; -import org.argeo.util.transaction.WorkTransaction; import org.osgi.service.useradmin.UserAdmin; /** diff --git a/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java b/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java deleted file mode 100644 index 3de2e1451..000000000 --- a/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java +++ /dev/null @@ -1,287 +0,0 @@ -package org.argeo.cms.security; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.CharArrayWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; -import java.security.Provider; -import java.security.Security; -import java.util.Arrays; -import java.util.Iterator; - -import javax.crypto.SecretKey; -import javax.security.auth.Subject; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.TextOutputCallback; -import javax.security.auth.callback.UnsupportedCallbackException; -import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; - -import org.argeo.api.cms.CmsAuth; -import org.argeo.util.CurrentSubject; -import org.argeo.util.StreamUtils; - -/** username / password based keyring. TODO internationalize */ -public abstract class AbstractKeyring implements Keyring, CryptoKeyring { - // public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING"; - - // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT; - private CallbackHandler defaultCallbackHandler; - - private String charset = "UTF-8"; - - /** - * Default provider is bouncy castle, in order to have consistent behaviour - * across implementations - */ - private String securityProviderName = "BC"; - - /** - * Whether the keyring has already been created in the past with a master - * password - */ - protected abstract Boolean isSetup(); - - /** - * Setup the keyring persistently, {@link #isSetup()} must return true - * afterwards - */ - protected abstract void setup(char[] password); - - /** Populates the key spec callback */ - protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback); - - protected abstract void encrypt(String path, InputStream unencrypted); - - protected abstract InputStream decrypt(String path); - - /** Triggers lazy initialization */ - protected SecretKey getSecretKey(char[] password) { - Subject subject = CurrentSubject.current(); - if (subject == null) - throw new IllegalStateException("Current subject cannot be null"); - // we assume only one secrete key is available - Iterator iterator = subject.getPrivateCredentials(SecretKey.class).iterator(); - if (!iterator.hasNext() || password != null) {// not initialized - CallbackHandler callbackHandler = password == null ? new KeyringCallbackHandler() - : new PasswordProvidedCallBackHandler(password); - ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); - try { - LoginContext loginContext = new LoginContext(CmsAuth.LOGIN_CONTEXT_KEYRING, subject, callbackHandler); - loginContext.login(); - // FIXME will login even if password is wrong - iterator = subject.getPrivateCredentials(SecretKey.class).iterator(); - return iterator.next(); - } catch (LoginException e) { - throw new IllegalStateException("Keyring login failed", e); - } finally { - Thread.currentThread().setContextClassLoader(currentContextClassLoader); - } - - } else { - SecretKey secretKey = iterator.next(); - if (iterator.hasNext()) - throw new IllegalStateException("More than one secret key in private credentials"); - return secretKey; - } - } - - public InputStream getAsStream(String path) { - return decrypt(path); - } - - public void set(String path, InputStream in) { - encrypt(path, in); - } - - public char[] getAsChars(String path) { - // InputStream in = getAsStream(path); - // CharArrayWriter writer = null; - // Reader reader = null; - try (InputStream in = getAsStream(path); - CharArrayWriter writer = new CharArrayWriter(); - Reader reader = new InputStreamReader(in, charset);) { - StreamUtils.copy(reader, writer); - return writer.toCharArray(); - } catch (IOException e) { - throw new IllegalStateException("Cannot decrypt to char array", e); - } finally { - // IOUtils.closeQuietly(reader); - // IOUtils.closeQuietly(in); - // IOUtils.closeQuietly(writer); - } - } - - public void set(String path, char[] arr) { - // ByteArrayOutputStream out = new ByteArrayOutputStream(); - // ByteArrayInputStream in = null; - // Writer writer = null; - try (ByteArrayOutputStream out = new ByteArrayOutputStream(); - Writer writer = new OutputStreamWriter(out, charset);) { - // writer = new OutputStreamWriter(out, charset); - writer.write(arr); - writer.flush(); - // in = new ByteArrayInputStream(out.toByteArray()); - try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());) { - set(path, in); - } - } catch (IOException e) { - throw new IllegalStateException("Cannot encrypt to char array", e); - } finally { - // IOUtils.closeQuietly(writer); - // IOUtils.closeQuietly(out); - // IOUtils.closeQuietly(in); - } - } - - public void unlock(char[] password) { - if (!isSetup()) - setup(password); - SecretKey secretKey = getSecretKey(password); - if (secretKey == null) - throw new IllegalStateException("Could not unlock keyring"); - } - - protected Provider getSecurityProvider() { - return Security.getProvider(securityProviderName); - } - - public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) { - this.defaultCallbackHandler = defaultCallbackHandler; - } - - public void setCharset(String charset) { - this.charset = charset; - } - - public void setSecurityProviderName(String securityProviderName) { - this.securityProviderName = securityProviderName; - } - - // @Deprecated - // protected static byte[] hash(char[] password, byte[] salt, Integer - // iterationCount) { - // ByteArrayOutputStream out = null; - // OutputStreamWriter writer = null; - // try { - // out = new ByteArrayOutputStream(); - // writer = new OutputStreamWriter(out, "UTF-8"); - // writer.write(password); - // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256"); - // pwDigest.reset(); - // pwDigest.update(salt); - // byte[] btPass = pwDigest.digest(out.toByteArray()); - // for (int i = 0; i < iterationCount; i++) { - // pwDigest.reset(); - // btPass = pwDigest.digest(btPass); - // } - // return btPass; - // } catch (Exception e) { - // throw new CmsException("Cannot hash", e); - // } finally { - // IOUtils.closeQuietly(out); - // IOUtils.closeQuietly(writer); - // } - // - // } - - /** - * Convenience method using the underlying callback to ask for a password - * (typically used when the password is not saved in the keyring) - */ - protected char[] ask() { - PasswordCallback passwordCb = new PasswordCallback("Password", false); - Callback[] dialogCbs = new Callback[] { passwordCb }; - try { - defaultCallbackHandler.handle(dialogCbs); - char[] password = passwordCb.getPassword(); - return password; - } catch (Exception e) { - throw new IllegalStateException("Cannot ask for a password", e); - } - - } - - class KeyringCallbackHandler implements CallbackHandler { - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - // checks - if (callbacks.length != 2) - throw new IllegalArgumentException( - "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}"); - if (!(callbacks[0] instanceof PasswordCallback)) - throw new UnsupportedCallbackException(callbacks[0]); - if (!(callbacks[1] instanceof PBEKeySpecCallback)) - throw new UnsupportedCallbackException(callbacks[0]); - - PasswordCallback passwordCb = (PasswordCallback) callbacks[0]; - PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1]; - - if (isSetup()) { - Callback[] dialogCbs = new Callback[] { passwordCb }; - defaultCallbackHandler.handle(dialogCbs); - } else {// setup keyring - TextOutputCallback textCb1 = new TextOutputCallback(TextOutputCallback.INFORMATION, - "Enter a master password which will protect your private data"); - TextOutputCallback textCb2 = new TextOutputCallback(TextOutputCallback.INFORMATION, - "(for example your credentials to third-party services)"); - TextOutputCallback textCb3 = new TextOutputCallback(TextOutputCallback.INFORMATION, - "Don't forget this password since the data cannot be read without it"); - PasswordCallback confirmPasswordCb = new PasswordCallback("Confirm password", false); - // first try - Callback[] dialogCbs = new Callback[] { textCb1, textCb2, textCb3, passwordCb, confirmPasswordCb }; - defaultCallbackHandler.handle(dialogCbs); - - // if passwords different, retry (except if cancelled) - while (passwordCb.getPassword() != null - && !Arrays.equals(passwordCb.getPassword(), confirmPasswordCb.getPassword())) { - TextOutputCallback textCb = new TextOutputCallback(TextOutputCallback.ERROR, - "The passwords do not match"); - dialogCbs = new Callback[] { textCb, passwordCb, confirmPasswordCb }; - defaultCallbackHandler.handle(dialogCbs); - } - - if (passwordCb.getPassword() != null) {// not cancelled - setup(passwordCb.getPassword()); - } - } - - if (passwordCb.getPassword() != null) - handleKeySpecCallback(pbeCb); - } - - } - - class PasswordProvidedCallBackHandler implements CallbackHandler { - private final char[] password; - - public PasswordProvidedCallBackHandler(char[] password) { - this.password = password; - } - - @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - // checks - if (callbacks.length != 2) - throw new IllegalArgumentException( - "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}"); - if (!(callbacks[0] instanceof PasswordCallback)) - throw new UnsupportedCallbackException(callbacks[0]); - if (!(callbacks[1] instanceof PBEKeySpecCallback)) - throw new UnsupportedCallbackException(callbacks[0]); - - PasswordCallback passwordCb = (PasswordCallback) callbacks[0]; - passwordCb.setPassword(password); - PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1]; - handleKeySpecCallback(pbeCb); - } - - } -} diff --git a/org.argeo.cms/src/org/argeo/cms/security/ChecksumFactory.java b/org.argeo.cms/src/org/argeo/cms/security/ChecksumFactory.java deleted file mode 100644 index 7344f01bc..000000000 --- a/org.argeo.cms/src/org/argeo/cms/security/ChecksumFactory.java +++ /dev/null @@ -1,149 +0,0 @@ -package org.argeo.cms.security; - -import java.io.IOException; -import java.math.BigInteger; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; -import java.util.zip.Checksum; - -/** Allows to fine tune how files are read. */ -public class ChecksumFactory { - private int regionSize = 10 * 1024 * 1024; - - public byte[] digest(Path path, final String algo) { - try { - final MessageDigest md = MessageDigest.getInstance(algo); - if (Files.isDirectory(path)) { - long begin = System.currentTimeMillis(); - Files.walkFileTree(path, new SimpleFileVisitor() { - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if (!Files.isDirectory(file)) { - byte[] digest = digest(file, algo); - md.update(digest); - } - return FileVisitResult.CONTINUE; - } - - }); - byte[] digest = md.digest(); - long duration = System.currentTimeMillis() - begin; - System.out.println(printBase64Binary(digest) + " " + path + " (" + duration / 1000 + "s)"); - return digest; - } else { - long begin = System.nanoTime(); - long length = -1; - try (FileChannel channel = (FileChannel) Files.newByteChannel(path);) { - length = channel.size(); - long cursor = 0; - while (cursor < length) { - long effectiveSize = Math.min(regionSize, length - cursor); - MappedByteBuffer mb = channel.map(FileChannel.MapMode.READ_ONLY, cursor, effectiveSize); - // md.update(mb); - byte[] buffer = new byte[1024]; - while (mb.hasRemaining()) { - mb.get(buffer); - md.update(buffer); - } - - // sub digest - // mb.flip(); - // MessageDigest subMd = - // MessageDigest.getInstance(algo); - // subMd.update(mb); - // byte[] subDigest = subMd.digest(); - // System.out.println(" -> " + cursor); - // System.out.println(IOUtils.encodeHexString(subDigest)); - // System.out.println(new BigInteger(1, - // subDigest).toString(16)); - // System.out.println(new BigInteger(1, subDigest) - // .toString(Character.MAX_RADIX)); - // System.out.println(printBase64Binary(subDigest)); - - cursor = cursor + regionSize; - } - byte[] digest = md.digest(); - long duration = System.nanoTime() - begin; - System.out.println(printBase64Binary(digest) + " " + path.getFileName() + " (" + duration / 1000000 - + "ms, " + (length / 1024) + "kB, " + (length / (duration / 1000000)) * 1000 / (1024 * 1024) - + " MB/s)"); - return digest; - } - } - } catch (NoSuchAlgorithmException | IOException e) { - throw new IllegalStateException("Cannot digest " + path, e); - } - } - - /** Whether the file should be mapped. */ - protected boolean mapFile(FileChannel fileChannel) throws IOException { - long size = fileChannel.size(); - if (size > (regionSize / 10)) - return true; - return false; - } - - public long checksum(Path path, Checksum crc) { - final int bufferSize = 2 * 1024 * 1024; - long begin = System.currentTimeMillis(); - try (FileChannel channel = (FileChannel) Files.newByteChannel(path);) { - byte[] bytes = new byte[bufferSize]; - long length = channel.size(); - long cursor = 0; - while (cursor < length) { - long effectiveSize = Math.min(regionSize, length - cursor); - MappedByteBuffer mb = channel.map(FileChannel.MapMode.READ_ONLY, cursor, effectiveSize); - int nGet; - while (mb.hasRemaining()) { - nGet = Math.min(mb.remaining(), bufferSize); - mb.get(bytes, 0, nGet); - crc.update(bytes, 0, nGet); - } - cursor = cursor + regionSize; - } - return crc.getValue(); - } catch (IOException e) { - throw new IllegalStateException("Cannot checksum " + path, e); - } finally { - long duration = System.currentTimeMillis() - begin; - System.out.println(duration / 1000 + "s"); - } - } - - public static void main(String... args) { - ChecksumFactory cf = new ChecksumFactory(); - // Path path = - // Paths.get("/home/mbaudier/apache-maven-3.2.3-bin.tar.gz"); - Path path; - if (args.length > 0) { - path = Paths.get(args[0]); - } else { - path = Paths.get("/home/mbaudier/Downloads/torrents/CentOS-7-x86_64-DVD-1503-01/" - + "CentOS-7-x86_64-DVD-1503-01.iso"); - } - // long adler = cf.checksum(path, new Adler32()); - // System.out.format("Adler=%d%n", adler); - // long crc = cf.checksum(path, new CRC32()); - // System.out.format("CRC=%d%n", crc); - String algo = "SHA1"; - byte[] digest = cf.digest(path, algo); - System.out.println(algo + " " + printBase64Binary(digest)); - System.out.println(algo + " " + new BigInteger(1, digest).toString(16)); - // String sha1 = printBase64Binary(cf.digest(path, "SHA1")); - // System.out.format("SHA1=%s%n", sha1); - } - - private static String printBase64Binary(byte[] arr) { - return Base64.getEncoder().encodeToString(arr); - } -} diff --git a/org.argeo.cms/src/org/argeo/cms/security/CryptoKeyring.java b/org.argeo.cms/src/org/argeo/cms/security/CryptoKeyring.java deleted file mode 100644 index df26c6b41..000000000 --- a/org.argeo.cms/src/org/argeo/cms/security/CryptoKeyring.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.argeo.cms.security; - -/** - * Marker interface for an advanced keyring based on cryptography. - */ -public interface CryptoKeyring extends Keyring { - public void changePassword(char[] oldPassword, char[] newPassword); - - public void unlock(char[] password); -} diff --git a/org.argeo.cms/src/org/argeo/cms/security/Keyring.java b/org.argeo.cms/src/org/argeo/cms/security/Keyring.java deleted file mode 100644 index 53740c693..000000000 --- a/org.argeo.cms/src/org/argeo/cms/security/Keyring.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.argeo.cms.security; - -import java.io.InputStream; - -/** - * Access to private (typically encrypted) data. The keyring is responsible for - * retrieving the necessary credentials. Experimental. This API may - * change. - */ -public interface Keyring { - /** - * Returns the confidential information as chars. Must ask for it if it is - * not stored. - */ - public char[] getAsChars(String path); - - /** - * Returns the confidential information as a stream. Must ask for it if it - * is not stored. - */ - public InputStream getAsStream(String path); - - public void set(String path, char[] arr); - - public void set(String path, InputStream in); -} diff --git a/org.argeo.cms/src/org/argeo/cms/security/PBEKeySpecCallback.java b/org.argeo.cms/src/org/argeo/cms/security/PBEKeySpecCallback.java deleted file mode 100644 index 13e8d753b..000000000 --- a/org.argeo.cms/src/org/argeo/cms/security/PBEKeySpecCallback.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.argeo.cms.security; - -import javax.crypto.spec.PBEKeySpec; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.PasswordCallback; - -/** - * All information required to set up a {@link PBEKeySpec} bar the password - * itself (use a {@link PasswordCallback}) - */ -public class PBEKeySpecCallback implements Callback { - private String secretKeyFactory; - private byte[] salt; - private Integer iterationCount; - /** Can be null for some algorithms */ - private Integer keyLength; - /** Can be null, will trigger secret key encryption if not */ - private String secretKeyEncryption; - - private String encryptedPasswordHashCipher; - private byte[] encryptedPasswordHash; - - public void set(String secretKeyFactory, byte[] salt, - Integer iterationCount, Integer keyLength, - String secretKeyEncryption) { - this.secretKeyFactory = secretKeyFactory; - this.salt = salt; - this.iterationCount = iterationCount; - this.keyLength = keyLength; - this.secretKeyEncryption = secretKeyEncryption; -// this.encryptedPasswordHashCipher = encryptedPasswordHashCipher; -// this.encryptedPasswordHash = encryptedPasswordHash; - } - - public String getSecretKeyFactory() { - return secretKeyFactory; - } - - public byte[] getSalt() { - return salt; - } - - public Integer getIterationCount() { - return iterationCount; - } - - public Integer getKeyLength() { - return keyLength; - } - - public String getSecretKeyEncryption() { - return secretKeyEncryption; - } - - public String getEncryptedPasswordHashCipher() { - return encryptedPasswordHashCipher; - } - - public byte[] getEncryptedPasswordHash() { - return encryptedPasswordHash; - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/security/package-info.java b/org.argeo.cms/src/org/argeo/cms/security/package-info.java deleted file mode 100644 index e99405436..000000000 --- a/org.argeo.cms/src/org/argeo/cms/security/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Argeo CMS reusable security components. */ -package org.argeo.cms.security; \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/ArrayTabularRow.java b/org.argeo.cms/src/org/argeo/cms/tabular/ArrayTabularRow.java deleted file mode 100644 index cfd482729..000000000 --- a/org.argeo.cms/src/org/argeo/cms/tabular/ArrayTabularRow.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.argeo.cms.tabular; - -import java.util.List; - -/** Minimal tabular row wrapping an {@link Object} array */ -public class ArrayTabularRow implements TabularRow { - private final Object[] arr; - - public ArrayTabularRow(List objs) { - this.arr = objs.toArray(); - } - - public Object get(Integer col) { - return arr[col]; - } - - public int size() { - return arr.length; - } - - public Object[] toArray() { - return arr; - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularColumn.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularColumn.java deleted file mode 100644 index 7f7ac1e61..000000000 --- a/org.argeo.cms/src/org/argeo/cms/tabular/TabularColumn.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.argeo.cms.tabular; - -/** The column in a tabular content */ -public class TabularColumn { - private String name; - /** - * JCR types, see - * http://www.day.com/maven/javax.jcr/javadocs/jcr-2.0/index.html - * ?javax/jcr/PropertyType.html - */ - private Integer type; - - /** column with default type */ - public TabularColumn(String name) { - super(); - this.name = name; - } - - public TabularColumn(String name, Integer type) { - super(); - this.name = name; - this.type = type; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getType() { - return type; - } - - public void setType(Integer type) { - this.type = type; - } - -} diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularContent.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularContent.java deleted file mode 100644 index c6d2ab88d..000000000 --- a/org.argeo.cms/src/org/argeo/cms/tabular/TabularContent.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.argeo.cms.tabular; - -import java.util.List; - -/** - * Content organized as a table, possibly with headers. Only JCR types are - * supported even though there is not direct dependency on JCR. - */ -public interface TabularContent { - /** The headers of this table or null is none available. */ - public List getColumns(); - - public TabularRowIterator read(); -} diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularRow.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularRow.java deleted file mode 100644 index 69b973252..000000000 --- a/org.argeo.cms/src/org/argeo/cms/tabular/TabularRow.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.argeo.cms.tabular; - -/** A row of tabular data */ -public interface TabularRow { - /** The value at this column index */ - public Object get(Integer col); - - /** The raw objects (direct references) */ - public Object[] toArray(); - - /** Number of columns */ - public int size(); -} diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularRowIterator.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularRowIterator.java deleted file mode 100644 index 7ad8719e5..000000000 --- a/org.argeo.cms/src/org/argeo/cms/tabular/TabularRowIterator.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.argeo.cms.tabular; - -import java.util.Iterator; - -/** Navigation of rows */ -public interface TabularRowIterator extends Iterator { - /** - * Current row number, has to be incremented by each call to next() ; starts at 0, will - * therefore be 1 for the first row returned. - */ - public Long getCurrentRowNumber(); -} diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularWriter.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularWriter.java deleted file mode 100644 index 34fc85b7f..000000000 --- a/org.argeo.cms/src/org/argeo/cms/tabular/TabularWriter.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.argeo.cms.tabular; - - -/** Write to a tabular content */ -public interface TabularWriter { - /** Append a new row of data */ - public void appendRow(Object[] row); - - /** Finish persisting data and release resources */ - public void close(); -} diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/package-info.java b/org.argeo.cms/src/org/argeo/cms/tabular/package-info.java deleted file mode 100644 index 6cb48d07f..000000000 --- a/org.argeo.cms/src/org/argeo/cms/tabular/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Tabular format API. */ -package org.argeo.cms.tabular; \ No newline at end of file diff --git a/org.argeo.cms/src/org/argeo/cms/util/CompositeString.java b/org.argeo.cms/src/org/argeo/cms/util/CompositeString.java new file mode 100644 index 000000000..8ea16f7f3 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/CompositeString.java @@ -0,0 +1,164 @@ +package org.argeo.cms.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.StringTokenizer; + +/** A name that can be expressed with various conventions. */ +public class CompositeString { + public final static Character UNDERSCORE = Character.valueOf('_'); + public final static Character SPACE = Character.valueOf(' '); + public final static Character DASH = Character.valueOf('-'); + + private final String[] parts; + + // optimisation + private final int hashCode; + + public CompositeString(String str) { + Objects.requireNonNull(str, "String cannot be null"); + if ("".equals(str.trim())) + throw new IllegalArgumentException("String cannot be empty"); + if (!str.equals(str.trim())) + throw new IllegalArgumentException("String must be trimmed"); + this.parts = toParts(str); + hashCode = hashCode(this.parts); + } + + public String toString(char separator, boolean upperCase) { + StringBuilder sb = null; + for (String part : parts) { + if (sb == null) { + sb = new StringBuilder(); + } else { + sb.append(separator); + } + sb.append(upperCase ? part.toUpperCase() : part); + } + return sb.toString(); + } + + public String toStringCaml(boolean firstCharUpperCase) { + StringBuilder sb = null; + for (String part : parts) { + if (sb == null) {// first + sb = new StringBuilder(); + sb.append(firstCharUpperCase ? Character.toUpperCase(part.charAt(0)) : part.charAt(0)); + } else { + sb.append(Character.toUpperCase(part.charAt(0))); + } + + if (part.length() > 1) + sb.append(part.substring(1)); + } + return sb.toString(); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || !(obj instanceof CompositeString)) + return false; + + CompositeString other = (CompositeString) obj; + return Arrays.equals(parts, other.parts); + } + + @Override + public String toString() { + return toString(DASH, false); + } + + public static String[] toParts(String str) { + Character separator = null; + if (str.indexOf(UNDERSCORE) >= 0) { + checkNo(str, SPACE); + checkNo(str, DASH); + separator = UNDERSCORE; + } else if (str.indexOf(DASH) >= 0) { + checkNo(str, SPACE); + checkNo(str, UNDERSCORE); + separator = DASH; + } else if (str.indexOf(SPACE) >= 0) { + checkNo(str, DASH); + checkNo(str, UNDERSCORE); + separator = SPACE; + } + + List res = new ArrayList<>(); + if (separator != null) { + StringTokenizer st = new StringTokenizer(str, separator.toString()); + while (st.hasMoreTokens()) { + res.add(st.nextToken().toLowerCase()); + } + } else { + // single + String strLowerCase = str.toLowerCase(); + if (str.toUpperCase().equals(str) || strLowerCase.equals(str)) + return new String[] { strLowerCase }; + + // CAML + StringBuilder current = null; + for (char c : str.toCharArray()) { + if (Character.isUpperCase(c)) { + if (current != null) + res.add(current.toString()); + current = new StringBuilder(); + } + if (current == null)// first char is lower case + current = new StringBuilder(); + current.append(Character.toLowerCase(c)); + } + res.add(current.toString()); + } + return res.toArray(new String[res.size()]); + } + + private static void checkNo(String str, Character c) { + if (str.indexOf(c) >= 0) { + throw new IllegalArgumentException("Only one kind of sperator is allowed"); + } + } + + private static int hashCode(String[] parts) { + int hashCode = 0; + for (String part : parts) { + hashCode = hashCode + part.hashCode(); + } + return hashCode; + } + + static boolean smokeTests() { + CompositeString plainName = new CompositeString("NAME"); + assert "name".equals(plainName.toString()); + assert "NAME".equals(plainName.toString(UNDERSCORE, true)); + assert "name".equals(plainName.toString(UNDERSCORE, false)); + assert "name".equals(plainName.toStringCaml(false)); + assert "Name".equals(plainName.toStringCaml(true)); + + CompositeString camlName = new CompositeString("myComplexName"); + + assert new CompositeString("my-complex-name").equals(camlName); + assert new CompositeString("MY_COMPLEX_NAME").equals(camlName); + assert new CompositeString("My complex Name").equals(camlName); + assert new CompositeString("MyComplexName").equals(camlName); + + assert "my-complex-name".equals(camlName.toString()); + assert "MY_COMPLEX_NAME".equals(camlName.toString(UNDERSCORE, true)); + assert "my_complex_name".equals(camlName.toString(UNDERSCORE, false)); + assert "myComplexName".equals(camlName.toStringCaml(false)); + assert "MyComplexName".equals(camlName.toStringCaml(true)); + + return CompositeString.class.desiredAssertionStatus(); + } + + public static void main(String[] args) { + System.out.println(smokeTests()); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/CsvParser.java b/org.argeo.cms/src/org/argeo/cms/util/CsvParser.java new file mode 100644 index 000000000..f22a1e45f --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/CsvParser.java @@ -0,0 +1,242 @@ +package org.argeo.cms.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Parses a CSV file interpreting the first line as a header. The + * {@link #parse(InputStream)} method and the setters are synchronized so that + * the object cannot be modified when parsing. + */ +public abstract class CsvParser { + private char separator = ','; + private char quote = '\"'; + + private Boolean noHeader = false; + private Boolean strictLineAsLongAsHeader = true; + + /** + * Actually process a parsed line. If + * {@link #setStrictLineAsLongAsHeader(Boolean)} is true (default) the header + * and the tokens are guaranteed to have the same size. + * + * @param lineNumber the current line number, starts at 1 (the header, if header + * processing is enabled, the first line otherwise) + * @param header the read-only header or null if + * {@link #setNoHeader(Boolean)} is true (default is false) + * @param tokens the parsed tokens + */ + protected abstract void processLine(Integer lineNumber, List header, List tokens); + + /** + * Parses the CSV file (stream is closed at the end) + * + * @param in the stream to parse + * + * @deprecated Use {@link #parse(InputStream, Charset)} instead. + */ + @Deprecated + public synchronized void parse(InputStream in) { + parse(in, (Charset) null); + } + + /** + * Parses the CSV file (stream is closed at the end) + * + * @param in the stream to parse + * @param encoding the encoding to use. + * + * @deprecated Use {@link #parse(InputStream, Charset)} instead. + */ + @Deprecated + public synchronized void parse(InputStream in, String encoding) { + Reader reader; + if (encoding == null) + reader = new InputStreamReader(in); + else + try { + reader = new InputStreamReader(in, encoding); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + parse(reader); + } + + /** + * Parses the CSV file (stream is closed at the end) + * + * @param in the stream to parse + * @param charset the charset to use + */ + public synchronized void parse(InputStream in, Charset charset) { + Reader reader; + if (charset == null) + reader = new InputStreamReader(in); + else + reader = new InputStreamReader(in, charset); + parse(reader); + } + + /** + * Parses the CSV file (stream is closed at the end) + * + * @param reader the reader to use (it will be buffered) + */ + public synchronized void parse(Reader reader) { + Integer lineCount = 0; + try (BufferedReader bufferedReader = new BufferedReader(reader)) { + List header = null; + if (!noHeader) { + String headerStr = bufferedReader.readLine(); + if (headerStr == null)// empty file + return; + lineCount++; + header = new ArrayList(); + StringBuffer currStr = new StringBuffer(""); + Boolean wasInquote = false; + while (parseLine(headerStr, header, currStr, wasInquote)) { + headerStr = bufferedReader.readLine(); + if (headerStr == null) + break; + wasInquote = true; + } + header = Collections.unmodifiableList(header); + } + + String line = null; + lines: while ((line = bufferedReader.readLine()) != null) { + line = preProcessLine(line); + if (line == null) { + // skip line + continue lines; + } + lineCount++; + List tokens = new ArrayList(); + StringBuffer currStr = new StringBuffer(""); + Boolean wasInquote = false; + sublines: while (parseLine(line, tokens, currStr, wasInquote)) { + line = bufferedReader.readLine(); + if (line == null) + break sublines; + wasInquote = true; + } + if (!noHeader && strictLineAsLongAsHeader) { + int headerSize = header.size(); + int tokenSize = tokens.size(); + if (tokenSize == 1 && line.trim().equals("")) + continue lines;// empty line + if (headerSize != tokenSize) { + throw new IllegalStateException("Token size " + tokenSize + " is different from header size " + + headerSize + " at line " + lineCount + ", line: " + line + ", header: " + header + + ", tokens: " + tokens); + } + } + processLine(lineCount, header, tokens); + } + } catch (IOException e) { + throw new RuntimeException("Cannot parse CSV file (line: " + lineCount + ")", e); + } + } + + /** + * Called before each (logical) line is processed, giving a change to modify it + * (typically for cleaning dirty files). To be overridden, return the line + * unchanged by default. Skip the line if 'null' is returned. + */ + protected String preProcessLine(String line) { + return line; + } + + /** + * Parses a line character by character for performance purpose + * + * @return whether to continue parsing this line + */ + protected Boolean parseLine(String str, List tokens, StringBuffer currStr, Boolean wasInquote) { + if (wasInquote) + currStr.append('\n'); + + char[] arr = str.toCharArray(); + boolean inQuote = wasInquote; + for (int i = 0; i < arr.length; i++) { + char c = arr[i]; + if (c == separator) { + if (!inQuote) { + tokens.add(currStr.toString()); +// currStr.delete(0, currStr.length()); + currStr.setLength(0); + currStr.trimToSize(); + } else { + // we don't remove separator that are in a quoted substring + // System.out + // .println("IN QUOTE, got a separator: [" + c + "]"); + currStr.append(c); + } + } else if (c == quote) { + if (inQuote && (i + 1) < arr.length && arr[i + 1] == quote) { + // case of double quote + currStr.append(quote); + i++; + } else {// standard + inQuote = inQuote ? false : true; + } + } else { + currStr.append(c); + } + } + + if (!inQuote) { + tokens.add(currStr.toString()); + // System.out.println("# TOKEN: " + currStr); + } + // if (inQuote) + // throw new ArgeoException("Missing quote at the end of the line " + // + str + " (parsed: " + tokens + ")"); + if (inQuote) + return true; + else + return false; + // return tokens; + } + + public char getSeparator() { + return separator; + } + + public synchronized void setSeparator(char separator) { + this.separator = separator; + } + + public char getQuote() { + return quote; + } + + public synchronized void setQuote(char quote) { + this.quote = quote; + } + + public Boolean getNoHeader() { + return noHeader; + } + + public synchronized void setNoHeader(Boolean noHeader) { + this.noHeader = noHeader; + } + + public Boolean getStrictLineAsLongAsHeader() { + return strictLineAsLongAsHeader; + } + + public synchronized void setStrictLineAsLongAsHeader(Boolean strictLineAsLongAsHeader) { + this.strictLineAsLongAsHeader = strictLineAsLongAsHeader; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/CsvParserWithLinesAsMap.java b/org.argeo.cms/src/org/argeo/cms/util/CsvParserWithLinesAsMap.java new file mode 100644 index 000000000..0a0382c1d --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/CsvParserWithLinesAsMap.java @@ -0,0 +1,36 @@ +package org.argeo.cms.util; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * CSV parser allowing to process lines as maps whose keys are the header + * fields. + */ +public abstract class CsvParserWithLinesAsMap extends CsvParser { + + /** + * Actually processes a line. + * + * @param lineNumber the current line number, starts at 1 (the header, if header + * processing is enabled, the first lien otherwise) + * @param line the parsed tokens as a map whose keys are the header fields + */ + protected abstract void processLine(Integer lineNumber, Map line); + + protected final void processLine(Integer lineNumber, List header, List tokens) { + if (header == null) + throw new IllegalArgumentException("Only CSV with header is supported"); + Map line = new HashMap(); + for (int i = 0; i < header.size(); i++) { + String key = header.get(i); + String value = null; + if (i < tokens.size()) + value = tokens.get(i); + line.put(key, value); + } + processLine(lineNumber, line); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/CsvWriter.java b/org.argeo.cms/src/org/argeo/cms/util/CsvWriter.java new file mode 100644 index 000000000..915a97fcf --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/CsvWriter.java @@ -0,0 +1,156 @@ +package org.argeo.cms.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.List; + +/** Write in CSV format. */ +public class CsvWriter { + private final Writer out; + + private char separator = ','; + private char quote = '\"'; + + /** + * Creates a CSV writer. + * + * @param out the stream to write to. Caller is responsible for closing it. + * + * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead. + * + */ + @Deprecated + public CsvWriter(OutputStream out) { + this.out = new OutputStreamWriter(out); + } + + /** + * Creates a CSV writer. + * + * @param out the stream to write to. Caller is responsible for closing it. + * @param encoding the encoding to use. + * + * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead. + */ + @Deprecated + public CsvWriter(OutputStream out, String encoding) { + try { + this.out = new OutputStreamWriter(out, encoding); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Creates a CSV writer. + * + * @param out the stream to write to. Caller is responsible for closing it. + * @param charset the charset to use + */ + public CsvWriter(OutputStream out, Charset charset) { + this.out = new OutputStreamWriter(out, charset); + } + + /** + * Creates a CSV writer. + * + * @param out the stream to write to. Caller is responsible for closing it. + */ + public CsvWriter(Writer writer) { + this.out = writer; + } + + /** + * Write a CSV line. Also used to write a header if needed (this is transparent + * for the CSV writer): simply call it first, before writing the lines. + */ + public void writeLine(List tokens) { + try { + Iterator it = tokens.iterator(); + while (it.hasNext()) { + Object obj = it.next(); + writeToken(obj != null ? obj.toString() : null); + if (it.hasNext()) + out.write(separator); + } + out.write('\n'); + out.flush(); + } catch (IOException e) { + throw new RuntimeException("Could not write " + tokens, e); + } + } + + /** + * Write a CSV line. Also used to write a header if needed (this is transparent + * for the CSV writer): simply call it first, before writing the lines. + */ + public void writeLine(Object[] tokens) { + try { + for (int i = 0; i < tokens.length; i++) { + if (tokens[i] == null) { + writeToken(null); + } else { + writeToken(tokens[i].toString()); + } + if (i != (tokens.length - 1)) + out.write(separator); + } + out.write('\n'); + out.flush(); + } catch (IOException e) { + throw new RuntimeException("Could not write " + tokens, e); + } + } + + protected void writeToken(String token) throws IOException { + if (token == null) { + // TODO configure how to deal with null + out.write(""); + return; + } + // +2 for possible quotes, another +2 assuming there would be an already + // quoted string where quotes needs to be duplicated + // another +2 for safety + // we don't want to increase buffer size while writing + StringBuffer buf = new StringBuffer(token.length() + 6); + char[] arr = token.toCharArray(); + boolean shouldQuote = false; + for (char c : arr) { + if (!shouldQuote) { + if (c == separator) + shouldQuote = true; + if (c == '\n') + shouldQuote = true; + } + + if (c == quote) { + shouldQuote = true; + // duplicate quote + buf.append(quote); + } + + // generic case + buf.append(c); + } + + if (shouldQuote == true) + out.write(quote); + out.write(buf.toString()); + if (shouldQuote == true) + out.write(quote); + } + + public void setSeparator(char separator) { + this.separator = separator; + } + + public void setQuote(char quote) { + this.quote = quote; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/CurrentSubject.java b/org.argeo.cms/src/org/argeo/cms/util/CurrentSubject.java new file mode 100644 index 000000000..6a3dcbc62 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/CurrentSubject.java @@ -0,0 +1,65 @@ +package org.argeo.cms.util; + +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; + +import javax.security.auth.Subject; + +/** + * Prepare evolution of Java APIs introduced in JDK 18, as these static methods + * will be added to {@link Subject}. + */ +@SuppressWarnings("removal") +public class CurrentSubject { + + private final static boolean useThreadLocal = Boolean + .parseBoolean(System.getProperty("jdk.security.auth.subject.useTL")); + + private final static InheritableThreadLocal current = new InheritableThreadLocal<>(); + + public static Subject current() { + if (useThreadLocal) { + return current.get(); + } else {// legacy + Subject subject = Subject.getSubject(AccessController.getContext()); + return subject; + } + } + + public static T callAs(Subject subject, Callable action) { + if (useThreadLocal) { + Subject previous = current(); + current.set(subject); + try { + return action.call(); + } catch (Exception e) { + throw new CompletionException("Failed to execute action for " + subject, e); + } finally { + current.set(previous); + } + } else {// legacy + try { + return Subject.doAs(subject, new PrivilegedExceptionAction() { + + @Override + public T run() throws Exception { + return action.call(); + } + + }); + } catch (PrivilegedActionException e) { + throw new CompletionException("Failed to execute action for " + subject, e.getCause()); + } catch (Exception e) { + throw new CompletionException("Failed to execute action for " + subject, e); + } + } + } + + /** Singleton. */ + private CurrentSubject() { + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/DictionaryKeys.java b/org.argeo.cms/src/org/argeo/cms/util/DictionaryKeys.java new file mode 100644 index 000000000..a9f6a318f --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/DictionaryKeys.java @@ -0,0 +1,42 @@ +package org.argeo.cms.util; + +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Iterator; + +/** + * Access the keys of a {@link String}-keyed {@link Dictionary} (common throughout + * the OSGi APIs) as an {@link Iterable} so that they are easily usable in + * for-each loops. + */ +class DictionaryKeys implements Iterable { + private final Dictionary dictionary; + + public DictionaryKeys(Dictionary dictionary) { + this.dictionary = dictionary; + } + + @Override + public Iterator iterator() { + return new KeyIterator(dictionary.keys()); + } + + private static class KeyIterator implements Iterator { + private final Enumeration keys; + + KeyIterator(Enumeration keys) { + this.keys = keys; + } + + @Override + public boolean hasNext() { + return keys.hasMoreElements(); + } + + @Override + public String next() { + return keys.nextElement(); + } + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/DigestUtils.java b/org.argeo.cms/src/org/argeo/cms/util/DigestUtils.java new file mode 100644 index 000000000..047749f70 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/DigestUtils.java @@ -0,0 +1,202 @@ +package org.argeo.cms.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** Utilities around cryptographic digests */ +public class DigestUtils { + public final static String MD5 = "MD5"; + public final static String SHA1 = "SHA1"; + public final static String SHA256 = "SHA-256"; + public final static String SHA512 = "SHA-512"; + + private static Boolean debug = false; + // TODO: make it configurable + private final static Integer byteBufferCapacity = 100 * 1024;// 100 KB + + public static byte[] sha1(byte[]... bytes) { + try { + MessageDigest digest = MessageDigest.getInstance(SHA1); + for (byte[] arr : bytes) + digest.update(arr); + byte[] checksum = digest.digest(); + return checksum; + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("SHA1 is not avalaible", e); + } + } + + public static byte[] digestAsBytes(String algorithm, byte[]... bytes) { + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + for (byte[] arr : bytes) + digest.update(arr); + byte[] checksum = digest.digest(); + return checksum; + } catch (NoSuchAlgorithmException e) { + throw new UnsupportedOperationException("Cannot digest with algorithm " + algorithm, e); + } + } + + public static String digest(String algorithm, byte[]... bytes) { + return toHexString(digestAsBytes(algorithm, bytes)); + } + + public static String digest(String algorithm, InputStream in) { + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + // ReadableByteChannel channel = Channels.newChannel(in); + // ByteBuffer bb = ByteBuffer.allocateDirect(byteBufferCapacity); + // while (channel.read(bb) > 0) + // digest.update(bb); + byte[] buffer = new byte[byteBufferCapacity]; + int read = 0; + while ((read = in.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + + byte[] checksum = digest.digest(); + String res = toHexString(checksum); + return res; + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + StreamUtils.closeQuietly(in); + } + } + + public static String digest(String algorithm, File file) { + FileInputStream fis = null; + FileChannel fc = null; + try { + fis = new FileInputStream(file); + fc = fis.getChannel(); + + // Get the file's size and then map it into memory + int sz = (int) fc.size(); + ByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz); + return digest(algorithm, bb); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e); + } finally { + StreamUtils.closeQuietly(fis); + if (fc.isOpen()) + try { + fc.close(); + } catch (IOException e) { + // silent + } + } + } + + protected static String digest(String algorithm, ByteBuffer bb) { + long begin = System.currentTimeMillis(); + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + digest.update(bb); + byte[] checksum = digest.digest(); + String res = toHexString(checksum); + long end = System.currentTimeMillis(); + if (debug) + System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s"); + return res; + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); + } + } + + public static String sha1hex(Path path) { + return digest(SHA1, path, byteBufferCapacity); + } + + public static String digest(String algorithm, Path path, long bufferSize) { + byte[] digest = digestAsBytes(algorithm, path, bufferSize); + return toHexString(digest); + } + + public static byte[] digestAsBytes(String algorithm, Path file, long bufferSize) { + long begin = System.currentTimeMillis(); + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + FileChannel fc = FileChannel.open(file); + long fileSize = Files.size(file); + if (fileSize <= bufferSize) { + ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, fileSize); + md.update(bb); + } else { + long lastCycle = (fileSize / bufferSize) - 1; + long position = 0; + for (int i = 0; i <= lastCycle; i++) { + ByteBuffer bb; + if (i != lastCycle) { + bb = fc.map(MapMode.READ_ONLY, position, bufferSize); + position = position + bufferSize; + } else { + bb = fc.map(MapMode.READ_ONLY, position, fileSize - position); + position = fileSize; + } + md.update(bb); + } + } + long end = System.currentTimeMillis(); + if (debug) + System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s"); + return md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e); + } catch (IOException e) { + throw new RuntimeException("Cannot digest " + file + " with algorithm " + algorithm, e); + } + } + + public static void main(String[] args) { + File file; + if (args.length > 0) + file = new File(args[0]); + else { + System.err.println("Usage: []" + " (see http://java.sun.com/j2se/1.5.0/" + + "docs/guide/security/CryptoSpec.html#AppA)"); + return; + } + + if (args.length > 1) { + String algorithm = args[1]; + System.out.println(digest(algorithm, file)); + } else { + String algorithm = "MD5"; + System.out.println(algorithm + ": " + digest(algorithm, file)); + algorithm = "SHA"; + System.out.println(algorithm + ": " + digest(algorithm, file)); + System.out.println(algorithm + ": " + sha1hex(file.toPath())); + algorithm = "SHA-256"; + System.out.println(algorithm + ": " + digest(algorithm, file)); + algorithm = "SHA-512"; + System.out.println(algorithm + ": " + digest(algorithm, file)); + } + } + + final private static char[] hexArray = "0123456789abcdef".toCharArray(); + + /** Converts a byte array to an hex String. */ + public static String toHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/DirH.java b/org.argeo.cms/src/org/argeo/cms/util/DirH.java new file mode 100644 index 000000000..2596c61d1 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/DirH.java @@ -0,0 +1,116 @@ +package org.argeo.cms.util; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Hashes the hashes of the files in a directory. */ +public class DirH { + + private final static Charset charset = Charset.forName("UTF-16"); + private final static long bufferSize = 200 * 1024 * 1024; + private final static String algorithm = "SHA"; + + private final static byte EOL = (byte) '\n'; + private final static byte SPACE = (byte) ' '; + + private final int hashSize; + + private final byte[][] hashes; + private final byte[][] fileNames; + private final byte[] digest; + private final byte[] dirName; + + /** + * @param dirName can be null or empty + */ + private DirH(byte[][] hashes, byte[][] fileNames, byte[] dirName) { + if (hashes.length != fileNames.length) + throw new IllegalArgumentException(hashes.length + " hashes and " + fileNames.length + " file names"); + this.hashes = hashes; + this.fileNames = fileNames; + this.dirName = dirName == null ? new byte[0] : dirName; + if (hashes.length == 0) {// empty dir + hashSize = 20; + // FIXME what is the digest of an empty dir? + digest = new byte[hashSize]; + Arrays.fill(digest, SPACE); + return; + } + hashSize = hashes[0].length; + for (int i = 0; i < hashes.length; i++) { + if (hashes[i].length != hashSize) + throw new IllegalArgumentException( + "Hash size for " + new String(fileNames[i], charset) + " is " + hashes[i].length); + } + + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + for (int i = 0; i < hashes.length; i++) { + md.update(this.hashes[i]); + md.update(SPACE); + md.update(this.fileNames[i]); + md.update(EOL); + } + digest = md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest", e); + } + } + + public void print(PrintStream out) { + out.print(DigestUtils.toHexString(digest)); + if (dirName.length > 0) { + out.print(' '); + out.print(new String(dirName, charset)); + } + out.print('\n'); + for (int i = 0; i < hashes.length; i++) { + out.print(DigestUtils.toHexString(hashes[i])); + out.print(' '); + out.print(new String(fileNames[i], charset)); + out.print('\n'); + } + } + + public static DirH digest(Path dir) { + try (DirectoryStream files = Files.newDirectoryStream(dir)) { + List hs = new ArrayList(); + List fNames = new ArrayList<>(); + for (Path file : files) { + if (!Files.isDirectory(file)) { + byte[] digest = DigestUtils.digestAsBytes(algorithm, file, bufferSize); + hs.add(digest); + fNames.add(file.getFileName().toString()); + } + } + + byte[][] fileNames = new byte[fNames.size()][]; + for (int i = 0; i < fNames.size(); i++) { + fileNames[i] = fNames.get(i).getBytes(charset); + } + byte[][] hashes = hs.toArray(new byte[hs.size()][]); + return new DirH(hashes, fileNames, dir.toString().getBytes(charset)); + } catch (IOException e) { + throw new RuntimeException("Cannot digest " + dir, e); + } + } + + public static void main(String[] args) { + try { + DirH dirH = DirH.digest(Paths.get("/home/mbaudier/tmp/")); + dirH.print(System.out); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/ExceptionsChain.java b/org.argeo.cms/src/org/argeo/cms/util/ExceptionsChain.java new file mode 100644 index 000000000..e71cfb3ca --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/ExceptionsChain.java @@ -0,0 +1,90 @@ +package org.argeo.cms.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * Serialisable wrapper of a {@link Throwable}. typically to be written as XML + * or JSON in a server error response. + */ +public class ExceptionsChain { + private List exceptions = new ArrayList<>(); + + public ExceptionsChain() { + } + + public ExceptionsChain(Throwable exception) { + writeException(exception); + } + + /** recursive */ + protected void writeException(Throwable exception) { + SystemException systemException = new SystemException(exception); + exceptions.add(systemException); + Throwable cause = exception.getCause(); + if (cause != null) + writeException(cause); + } + + public List getExceptions() { + return exceptions; + } + + public void setExceptions(List exceptions) { + this.exceptions = exceptions; + } + + /** An exception in the chain. */ + public static class SystemException { + private String type; + private String message; + private List stackTrace; + + public SystemException() { + } + + public SystemException(Throwable exception) { + this.type = exception.getClass().getName(); + this.message = exception.getMessage(); + this.stackTrace = new ArrayList<>(); + StackTraceElement[] elems = exception.getStackTrace(); + for (int i = 0; i < elems.length; i++) + stackTrace.add("at " + elems[i].toString()); + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public List getStackTrace() { + return stackTrace; + } + + public void setStackTrace(List stackTrace) { + this.stackTrace = stackTrace; + } + + @Override + public String toString() { + return "System exception: " + type + ", " + message + ", " + stackTrace; + } + + } + + @Override + public String toString() { + return exceptions.toString(); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/FsUtils.java b/org.argeo.cms/src/org/argeo/cms/util/FsUtils.java new file mode 100644 index 000000000..26c05b60e --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/FsUtils.java @@ -0,0 +1,78 @@ +package org.argeo.cms.util; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** Utilities around the standard Java file abstractions. */ +public class FsUtils { + + /** Deletes this path, recursively if needed. */ + public static void copyDirectory(Path source, Path target) { + if (!Files.exists(source) || !Files.isDirectory(source)) + throw new IllegalArgumentException(source + " is not a directory"); + if (Files.exists(target) && !Files.isDirectory(target)) + throw new IllegalArgumentException(target + " is not a directory"); + try { + Files.createDirectories(target); + Files.walkFileTree(source, new SimpleFileVisitor() { + + @Override + public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs) throws IOException { + Path relativePath = source.relativize(directory); + Path targetDirectory = target.resolve(relativePath); + if (!Files.exists(targetDirectory)) + Files.createDirectory(targetDirectory); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Path relativePath = source.relativize(file); + Path targetFile = target.resolve(relativePath); + Files.copy(file, targetFile); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new RuntimeException("Cannot copy " + source + " to " + target, e); + } + + } + + /** + * Deletes this path, recursively if needed. Does nothing if the path does not + * exist. + */ + public static void delete(Path path) { + try { + if (!Files.exists(path)) + return; + Files.walkFileTree(path, new SimpleFileVisitor() { + @Override + public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException { + if (e != null) + throw e; + Files.delete(directory); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + throw new RuntimeException("Cannot delete " + path, e); + } + } + + /** Singleton. */ + private FsUtils() { + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/LangUtils.java b/org.argeo.cms/src/org/argeo/cms/util/LangUtils.java new file mode 100644 index 000000000..0e214271d --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/LangUtils.java @@ -0,0 +1,331 @@ +package org.argeo.cms.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +/** Utilities around Java basic features. */ +public class LangUtils { + /* + * NON-API OSGi + */ + /** + * Returns an array with the names of the provided classes. Useful when + * registering services with multiple interfaces in OSGi. + */ + public static String[] names(Class... clzz) { + String[] res = new String[clzz.length]; + for (int i = 0; i < clzz.length; i++) + res[i] = clzz[i].getName(); + return res; + } + +// /* +// * MAP +// */ +// /** +// * Creates a new {@link Map} with one key-value pair. Key should not be null, +// * but if the value is null, it returns an empty {@link Map}. +// * +// * @deprecated Use {@link Collections#singletonMap(Object, Object)} instead. +// */ +// @Deprecated +// public static Map map(String key, Object value) { +// assert key != null; +// HashMap props = new HashMap<>(); +// if (value != null) +// props.put(key, value); +// return props; +// } + + /* + * DICTIONARY + */ + + /** + * Creates a new {@link Dictionary} with one key-value pair. Key should not be + * null, but if the value is null, it returns an empty {@link Dictionary}. + */ + public static Dictionary dict(String key, Object value) { + assert key != null; + Hashtable props = new Hashtable<>(); + if (value != null) + props.put(key, value); + return props; + } + + /** @deprecated Use {@link #dict(String, Object)} instead. */ + @Deprecated + public static Dictionary dico(String key, Object value) { + return dict(key, value); + } + + /** Converts a {@link Dictionary} to a {@link Map} of strings. */ + public static Map dictToStringMap(Dictionary properties) { + if (properties == null) { + return null; + } + Map res = new HashMap<>(properties.size()); + Enumeration keys = properties.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + res.put(key, properties.get(key).toString()); + } + return res; + } + + /** Converts a {@link Dictionary} to a {@link Map}. */ + public static Map dictToMap(Dictionary properties) { + if (properties == null) { + return null; + } + Map res = new HashMap<>(properties.size()); + Enumeration keys = properties.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + res.put(key, properties.get(key)); + } + return res; + } + + /** + * Get a string property from this map, expecting to find it, or + * null if not found. + */ + public static String get(Map map, String key) { + Object res = map.get(key); + if (res == null) + return null; + return res.toString(); + } + + /** + * Get a string property from this map, expecting to find it. + * + * @throws IllegalArgumentException if the key was not found + */ + public static String getNotNull(Map map, String key) { + Object res = map.get(key); + if (res == null) + throw new IllegalArgumentException("Map " + map + " should contain key " + key); + return res.toString(); + } + + /** + * Wraps the keys of the provided {@link Dictionary} as an {@link Iterable}. + */ + public static Iterable keys(Dictionary props) { + assert props != null; + return new DictionaryKeys(props); + } + + static String toJson(Dictionary props) { + return toJson(props, false); + } + + static String toJson(Dictionary props, boolean pretty) { + StringBuilder sb = new StringBuilder(); + sb.append('{'); + if (pretty) + sb.append('\n'); + Enumeration keys = props.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + if (pretty) + sb.append(' '); + sb.append('\"').append(key).append('\"'); + if (pretty) + sb.append(" : "); + else + sb.append(':'); + sb.append('\"').append(props.get(key)).append('\"'); + if (keys.hasMoreElements()) + sb.append(", "); + if (pretty) + sb.append('\n'); + } + sb.append('}'); + return sb.toString(); + } + + static void storeAsProperties(Dictionary props, Path path) throws IOException { + if (props == null) + throw new IllegalArgumentException("Props cannot be null"); + Properties toStore = new Properties(); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + toStore.setProperty(key, props.get(key).toString()); + } + try (OutputStream out = Files.newOutputStream(path)) { + toStore.store(out, null); + } + } + + static void appendAsLdif(String dnBase, String dnKey, Dictionary props, Path path) + throws IOException { + if (props == null) + throw new IllegalArgumentException("Props cannot be null"); + Object dnValue = props.get(dnKey); + String dnStr = dnKey + '=' + dnValue + ',' + dnBase; + LdapName dn; + try { + dn = new LdapName(dnStr); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot interpret DN " + dnStr, e); + } + if (dnValue == null) + throw new IllegalArgumentException("DN key " + dnKey + " must have a value"); + try (Writer writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) { + writer.append("\ndn: "); + writer.append(dn.toString()); + writer.append('\n'); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + Object value = props.get(key); + writer.append(key); + writer.append(": "); + // FIXME deal with binary and multiple values + writer.append(value.toString()); + writer.append('\n'); + } + } + } + + static Dictionary loadFromProperties(Path path) throws IOException { + Properties toLoad = new Properties(); + try (InputStream in = Files.newInputStream(path)) { + toLoad.load(in); + } + Dictionary res = new Hashtable(); + for (Object key : toLoad.keySet()) + res.put(key.toString(), toLoad.get(key)); + return res; + } + + /* + * COLLECTIONS + */ + /** + * Convert a comma-separated separated {@link String} or a {@link String} array + * to a {@link List} of {@link String}, trimming them. Useful to quickly + * interpret OSGi services properties. + * + * @return a {@link List} containing the trimmed {@link String}s, or an empty + * {@link List} if the argument was null. + */ + public static List toStringList(Object value) { + List values = new ArrayList<>(); + if (value == null) + return values; + String[] arr; + if (value instanceof String) { + arr = ((String) value).split(","); + } else if (value instanceof String[]) { + arr = (String[]) value; + } else { + throw new IllegalArgumentException("Unsupported value type " + value.getClass()); + } + for (String str : arr) { + values.add(str.trim()); + } + return values; + } + + /** Size of an {@link Iterable}, optimised if it is a {@link Collection}. */ + public static int size(Iterable iterable) { + if (iterable instanceof Collection) + return ((Collection) iterable).size(); + + int size = 0; + for (Iterator it = iterable.iterator(); it.hasNext(); size++) + it.next(); + return size; + } + + public static T getAt(Iterable iterable, int index) { + if (iterable instanceof List) { + List lst = ((List) iterable); + if (index >= lst.size()) + throw new IllegalArgumentException("Index " + index + " is not available (size is " + lst.size() + ")"); + return lst.get(index); + } + int i = 0; + for (Iterator it = iterable.iterator(); it.hasNext(); i++) { + if (i == index) + return it.next(); + else + it.next(); + } + throw new IllegalArgumentException("Index " + index + " is not available (size is " + i + ")"); + } + + /* + * EXCEPTIONS + */ + /** + * Chain the messages of all causes (one per line, starts with a line + * return) without all the stack + */ + public static String chainCausesMessages(Throwable t) { + StringBuffer buf = new StringBuffer(); + chainCauseMessage(buf, t); + return buf.toString(); + } + + /** Recursive chaining of messages */ + private static void chainCauseMessage(StringBuffer buf, Throwable t) { + buf.append('\n').append(' ').append(t.getClass().getCanonicalName()).append(": ").append(t.getMessage()); + if (t.getCause() != null) + chainCauseMessage(buf, t.getCause()); + } + + /* + * TIME + */ + /** Formats time elapsed since start. */ + public static String since(ZonedDateTime start) { + ZonedDateTime now = ZonedDateTime.now(); + return duration(start, now); + } + + /** Formats a duration. */ + public static String duration(Temporal start, Temporal end) { + long count = ChronoUnit.DAYS.between(start, end); + if (count != 0) + return count > 1 ? count + " days" : count + " day"; + count = ChronoUnit.HOURS.between(start, end); + if (count != 0) + return count > 1 ? count + " hours" : count + " hours"; + count = ChronoUnit.MINUTES.between(start, end); + if (count != 0) + return count > 1 ? count + " minutes" : count + " minute"; + count = ChronoUnit.SECONDS.between(start, end); + return count > 1 ? count + " seconds" : count + " second"; + } + + /** Singleton constructor. */ + private LangUtils() { + + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/OS.java b/org.argeo.cms/src/org/argeo/cms/util/OS.java new file mode 100644 index 000000000..c63d7a190 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/OS.java @@ -0,0 +1,65 @@ +package org.argeo.cms.util; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** When OS specific informations are needed. */ +public class OS { + public final static OS LOCAL = new OS(); + + private final String arch, name, version; + + /** The OS of the running JVM */ + protected OS() { + arch = System.getProperty("os.arch"); + name = System.getProperty("os.name"); + version = System.getProperty("os.version"); + } + + public String getArch() { + return arch; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public boolean isMSWindows() { + // only MS Windows would use such an horrendous separator... + return File.separatorChar == '\\'; + } + + public String[] getDefaultShellCommand() { + if (!isMSWindows()) + return new String[] { "/bin/bash", "-l", "-i" }; + else + return new String[] { "cmd.exe", "/C" }; + } + + public static long getJvmPid() { + return ProcessHandle.current().pid(); +// String pidAndHost = ManagementFactory.getRuntimeMXBean().getName(); +// return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@'))); + } + + /** + * Get the runtime directory. It will be the environment variable + * XDG_RUNTIME_DIR if it is set, or ~/.cache/argeo if not. + */ + public static Path getRunDir() { + Path runDir; + String xdgRunDir = System.getenv("XDG_RUNTIME_DIR"); + if (xdgRunDir != null) { + // TODO support multiple names + runDir = Paths.get(xdgRunDir); + } else { + runDir = Paths.get(System.getProperty("user.home"), ".cache/argeo"); + } + return runDir; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/PasswordEncryption.java b/org.argeo.cms/src/org/argeo/cms/util/PasswordEncryption.java new file mode 100644 index 000000000..c50f415e3 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/PasswordEncryption.java @@ -0,0 +1,216 @@ +package org.argeo.cms.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public class PasswordEncryption { + public final static Integer DEFAULT_ITERATION_COUNT = 1024; + /** Stronger with 256, but causes problem with Oracle JVM */ + public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256; + public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128; + public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1"; + public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES"; + public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding"; +// public final static String DEFAULT_CHARSET = "UTF-8"; + public final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + private Integer iterationCount = DEFAULT_ITERATION_COUNT; + private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH; + private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY; + private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION; + private String cipherName = DEFAULT_CIPHER_NAME; + + private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, + (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; + private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, + (byte) 0x35, (byte) 0xE3, (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, + (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; + + private Key key; + private Cipher ecipher; + private Cipher dcipher; + + private String securityProviderName = null; + + /** + * This is up to the caller to clear the passed array. Neither copy of nor + * reference to the passed array is kept + */ + public PasswordEncryption(char[] password) { + this(password, DEFAULT_SALT_8, DEFAULT_IV_16); + } + + /** + * This is up to the caller to clear the passed array. Neither copies of nor + * references to the passed arrays are kept + */ + public PasswordEncryption(char[] password, byte[] passwordSalt, byte[] initializationVector) { + try { + initKeyAndCiphers(password, passwordSalt, initializationVector); + } catch (InvalidKeyException e) { + Integer previousSecreteKeyLength = secreteKeyLength; + secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED; + System.err.println("'" + e.getMessage() + "', will use " + secreteKeyLength + + " secrete key length instead of " + previousSecreteKeyLength); + try { + initKeyAndCiphers(password, passwordSalt, initializationVector); + } catch (GeneralSecurityException e1) { + throw new IllegalStateException("Cannot get secret key (with restricted length)", e1); + } + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Cannot get secret key", e); + } + } + + protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, byte[] initializationVector) + throws GeneralSecurityException { + byte[] salt = new byte[8]; + System.arraycopy(passwordSalt, 0, salt, 0, salt.length); + // for (int i = 0; i < password.length && i < salt.length; i++) + // salt[i] = (byte) password[i]; + byte[] iv = new byte[16]; + System.arraycopy(initializationVector, 0, iv, 0, iv.length); + + SecretKeyFactory keyFac = SecretKeyFactory.getInstance(getSecretKeyFactoryName()); + PBEKeySpec keySpec = new PBEKeySpec(password, salt, getIterationCount(), getKeyLength()); + String secKeyEncryption = getSecretKeyEncryption(); + if (secKeyEncryption != null) { + SecretKey tmp = keyFac.generateSecret(keySpec); + key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption()); + } else { + key = keyFac.generateSecret(keySpec); + } + if (securityProviderName != null) + ecipher = Cipher.getInstance(getCipherName(), securityProviderName); + else + ecipher = Cipher.getInstance(getCipherName()); + ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); + dcipher = Cipher.getInstance(getCipherName()); + dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + } + + public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) throws IOException { + try { + CipherOutputStream out = new CipherOutputStream(encryptedOut, ecipher); + StreamUtils.copy(decryptedIn, out); + StreamUtils.closeQuietly(out); + } catch (IOException e) { + throw e; + } finally { + StreamUtils.closeQuietly(decryptedIn); + } + } + + public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) throws IOException { + try { + CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, dcipher); + StreamUtils.copy(decryptedIn, decryptedOut); + } catch (IOException e) { + throw e; + } finally { + StreamUtils.closeQuietly(encryptedIn); + } + } + + public byte[] encryptString(String str) { + ByteArrayOutputStream out = null; + ByteArrayInputStream in = null; + try { + out = new ByteArrayOutputStream(); + in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET)); + encrypt(in, out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + StreamUtils.closeQuietly(out); + } + } + + /** Closes the input stream */ + public String decryptAsString(InputStream in) { + ByteArrayOutputStream out = null; + try { + out = new ByteArrayOutputStream(); + decrypt(in, out); + return new String(out.toByteArray(), DEFAULT_CHARSET); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + StreamUtils.closeQuietly(out); + } + } + + protected Key getKey() { + return key; + } + + protected Cipher getEcipher() { + return ecipher; + } + + protected Cipher getDcipher() { + return dcipher; + } + + protected Integer getIterationCount() { + return iterationCount; + } + + protected Integer getKeyLength() { + return secreteKeyLength; + } + + protected String getSecretKeyFactoryName() { + return secreteKeyFactoryName; + } + + protected String getSecretKeyEncryption() { + return secreteKeyEncryption; + } + + protected String getCipherName() { + return cipherName; + } + + public void setIterationCount(Integer iterationCount) { + this.iterationCount = iterationCount; + } + + public void setSecreteKeyLength(Integer keyLength) { + this.secreteKeyLength = keyLength; + } + + public void setSecreteKeyFactoryName(String secreteKeyFactoryName) { + this.secreteKeyFactoryName = secreteKeyFactoryName; + } + + public void setSecreteKeyEncryption(String secreteKeyEncryption) { + this.secreteKeyEncryption = secreteKeyEncryption; + } + + public void setCipherName(String cipherName) { + this.cipherName = cipherName; + } + + public void setSecurityProviderName(String securityProviderName) { + this.securityProviderName = securityProviderName; + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/ServiceChannel.java b/org.argeo.cms/src/org/argeo/cms/util/ServiceChannel.java new file mode 100644 index 000000000..8cdbcadc4 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/ServiceChannel.java @@ -0,0 +1,78 @@ +package org.argeo.cms.util; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousByteChannel; +import java.nio.channels.CompletionHandler; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +/** An {@link AsynchronousByteChannel} based on an {@link ExecutorService}. */ +public class ServiceChannel implements AsynchronousByteChannel { + private final ReadableByteChannel in; + private final WritableByteChannel out; + + private boolean open = true; + + private ExecutorService executor; + + public ServiceChannel(ReadableByteChannel in, WritableByteChannel out, ExecutorService executor) { + this.in = in; + this.out = out; + this.executor = executor; + } + + @Override + public Future read(ByteBuffer dst) { + return executor.submit(() -> in.read(dst)); + } + + @Override + public void read(ByteBuffer dst, A attachment, CompletionHandler handler) { + try { + Future res = read(dst); + handler.completed(res.get(), attachment); + } catch (Exception e) { + handler.failed(e, attachment); + } + } + + @Override + public Future write(ByteBuffer src) { + return executor.submit(() -> out.write(src)); + } + + @Override + public void write(ByteBuffer src, A attachment, CompletionHandler handler) { + try { + Future res = write(src); + handler.completed(res.get(), attachment); + } catch (Exception e) { + handler.failed(e, attachment); + } + } + + @Override + public synchronized void close() throws IOException { + try { + in.close(); + } catch (Exception e) { + e.printStackTrace(); + } + try { + out.close(); + } catch (Exception e) { + e.printStackTrace(); + } + open = false; + notifyAll(); + } + + @Override + public synchronized boolean isOpen() { + return open; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java b/org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java new file mode 100644 index 000000000..a589e739a --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java @@ -0,0 +1,98 @@ +package org.argeo.cms.util; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.StringJoiner; + +/** Stream utilities to be used when Apache Commons IO is not available. */ +public class StreamUtils { + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /* + * APACHE COMMONS IO (inspired) + */ + + /** @return the number of bytes */ + public static Long copy(InputStream in, OutputStream out) throws IOException { + Long count = 0l; + byte[] buf = new byte[DEFAULT_BUFFER_SIZE]; + while (true) { + int length = in.read(buf); + if (length < 0) + break; + out.write(buf, 0, length); + count = count + length; + } + return count; + } + + /** @return the number of chars */ + public static Long copy(Reader in, Writer out) throws IOException { + Long count = 0l; + char[] buf = new char[DEFAULT_BUFFER_SIZE]; + while (true) { + int length = in.read(buf); + if (length < 0) + break; + out.write(buf, 0, length); + count = count + length; + } + return count; + } + + public static byte[] toByteArray(InputStream in) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + copy(in, out); + return out.toByteArray(); + } + } + + public static void closeQuietly(InputStream in) { + if (in != null) + try { + in.close(); + } catch (Exception e) { + // + } + } + + public static void closeQuietly(OutputStream out) { + if (out != null) + try { + out.close(); + } catch (Exception e) { + // + } + } + + public static void closeQuietly(Reader in) { + if (in != null) + try { + in.close(); + } catch (Exception e) { + // + } + } + + public static void closeQuietly(Writer out) { + if (out != null) + try { + out.close(); + } catch (Exception e) { + // + } + } + + public static String toString(BufferedReader reader) throws IOException { + StringJoiner sn = new StringJoiner("\n"); + String line = null; + while ((line = reader.readLine()) != null) + sn.add(line); + return sn.toString(); + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/Tester.java b/org.argeo.cms/src/org/argeo/cms/util/Tester.java new file mode 100644 index 000000000..fa62cd796 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/Tester.java @@ -0,0 +1,126 @@ +package org.argeo.cms.util; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** A generic tester based on Java assertions and functional programming. */ +public class Tester { + private Map results = Collections.synchronizedSortedMap(new TreeMap<>()); + + private ClassLoader classLoader; + + /** Use {@link Thread#getContextClassLoader()} by default. */ + public Tester() { + this(Thread.currentThread().getContextClassLoader()); + } + + public Tester(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + public void execute(String className) { + Class clss; + try { + clss = classLoader.loadClass(className); + boolean assertionsEnabled = clss.desiredAssertionStatus(); + if (!assertionsEnabled) + throw new IllegalStateException("Test runner " + getClass().getName() + + " requires Java assertions to be enabled. Call the JVM with the -ea argument."); + } catch (Exception e1) { + throw new IllegalArgumentException("Cannot initalise test for " + className, e1); + + } + List methods = findMethods(clss); + if (methods.size() == 0) + throw new IllegalArgumentException("No test method found in " + clss); + // TODO make order more predictable? + for (Method method : methods) { + String uid = method.getDeclaringClass().getName() + "#" + method.getName(); + TesterStatus testStatus = new TesterStatus(uid); + Object obj = null; + try { + beforeTest(uid, method); + obj = clss.getDeclaredConstructor().newInstance(); + method.invoke(obj); + testStatus.setPassed(); + afterTestPassed(uid, method, obj); + } catch (Exception e) { + testStatus.setFailed(e); + afterTestFailed(uid, method, obj, e); + } finally { + results.put(uid, testStatus); + } + } + } + + protected void beforeTest(String uid, Method method) { + // System.out.println(uid + ": STARTING"); + } + + protected void afterTestPassed(String uid, Method method, Object obj) { + System.out.println(uid + ": PASSED"); + } + + protected void afterTestFailed(String uid, Method method, Object obj, Throwable e) { + System.out.println(uid + ": FAILED"); + e.printStackTrace(); + } + + protected List findMethods(Class clss) { + List methods = new ArrayList(); +// Method call = getMethod(clss, "call"); +// if (call != null) +// methods.add(call); +// + for (Method method : clss.getMethods()) { + if (method.getName().startsWith("test")) { + methods.add(method); + } + } + return methods; + } + + protected Method getMethod(Class clss, String name, Class... parameterTypes) { + try { + return clss.getMethod(name, parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } catch (SecurityException e) { + throw new IllegalStateException(e); + } + } + + public static void main(String[] args) { + // deal with arguments + String className; + if (args.length < 1) { + System.err.println(usage()); + System.exit(1); + throw new IllegalArgumentException(); + } else { + className = args[0]; + } + + Tester test = new Tester(); + try { + test.execute(className); + } catch (Throwable e) { + e.printStackTrace(); + } + + Map r = test.results; + for (String uid : r.keySet()) { + TesterStatus testStatus = r.get(uid); + System.out.println(testStatus); + } + } + + public static String usage() { + return "java " + Tester.class.getName() + " [test class name]"; + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/TesterStatus.java b/org.argeo.cms/src/org/argeo/cms/util/TesterStatus.java new file mode 100644 index 000000000..09ab432b2 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/TesterStatus.java @@ -0,0 +1,98 @@ +package org.argeo.cms.util; + +import java.io.Serializable; + +/** The status of a test. */ +public class TesterStatus implements Serializable { + private static final long serialVersionUID = 6272975746885487000L; + + private Boolean passed = null; + private final String uid; + private Throwable throwable = null; + + public TesterStatus(String uid) { + this.uid = uid; + } + + /** For cloning. */ + public TesterStatus(String uid, Boolean passed, Throwable throwable) { + this(uid); + this.passed = passed; + this.throwable = throwable; + } + + public synchronized Boolean isRunning() { + return passed == null; + } + + public synchronized Boolean isPassed() { + assert passed != null; + return passed; + } + + public synchronized Boolean isFailed() { + assert passed != null; + return !passed; + } + + public synchronized void setPassed() { + setStatus(true); + } + + public synchronized void setFailed() { + setStatus(false); + } + + public synchronized void setFailed(Throwable throwable) { + setStatus(false); + setThrowable(throwable); + } + + protected void setStatus(Boolean passed) { + if (this.passed != null) + throw new IllegalStateException("Passed status of test " + uid + " is already set (to " + passed + ")"); + this.passed = passed; + } + + protected void setThrowable(Throwable throwable) { + if (this.throwable != null) + throw new IllegalStateException("Throwable of test " + uid + " is already set (to " + passed + ")"); + this.throwable = throwable; + } + + public String getUid() { + return uid; + } + + public Throwable getThrowable() { + return throwable; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + // TODO Auto-generated method stub + return super.clone(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof TesterStatus) { + TesterStatus other = (TesterStatus) o; + // we don't check consistency for performance purposes + // this equals() is supposed to be used in collections or for transfer + return other.uid.equals(uid); + } + return false; + } + + @Override + public int hashCode() { + return uid.hashCode(); + } + + @Override + public String toString() { + return uid + "\t" + (passed ? "passed" : "failed"); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/Throughput.java b/org.argeo.cms/src/org/argeo/cms/util/Throughput.java new file mode 100644 index 000000000..4fc15f960 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/Throughput.java @@ -0,0 +1,82 @@ +package org.argeo.cms.util; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; + +/** A throughput, that is, a value per unit of time. */ +public class Throughput { + private final static NumberFormat usNumberFormat = NumberFormat.getInstance(Locale.US); + + public enum Unit { + s, m, h, d + } + + private final Double value; + private final Unit unit; + + public Throughput(Double value, Unit unit) { + this.value = value; + this.unit = unit; + } + + public Throughput(Long periodMs, Long count, Unit unit) { + if (unit.equals(Unit.s)) + value = ((double) count * 1000d) / periodMs; + else if (unit.equals(Unit.m)) + value = ((double) count * 60d * 1000d) / periodMs; + else if (unit.equals(Unit.h)) + value = ((double) count * 60d * 60d * 1000d) / periodMs; + else if (unit.equals(Unit.d)) + value = ((double) count * 24d * 60d * 60d * 1000d) / periodMs; + else + throw new IllegalArgumentException("Unsupported unit " + unit); + this.unit = unit; + } + + public Throughput(Double value, String unitStr) { + this(value, Unit.valueOf(unitStr)); + } + + public Throughput(String def) { + int index = def.indexOf('/'); + if (def.length() < 3 || index <= 0 || index != def.length() - 2) + throw new IllegalArgumentException( + def + " no a proper throughput definition" + " (should be /, e.g. 3.54/s or 1500/h"); + String valueStr = def.substring(0, index); + String unitStr = def.substring(index + 1); + try { + this.value = usNumberFormat.parse(valueStr).doubleValue(); + } catch (ParseException e) { + throw new IllegalArgumentException("Cannot parse " + valueStr + " as a number.", e); + } + this.unit = Unit.valueOf(unitStr); + } + + public Long asMsPeriod() { + if (unit.equals(Unit.s)) + return Math.round(1000d / value); + else if (unit.equals(Unit.m)) + return Math.round((60d * 1000d) / value); + else if (unit.equals(Unit.h)) + return Math.round((60d * 60d * 1000d) / value); + else if (unit.equals(Unit.d)) + return Math.round((24d * 60d * 60d * 1000d) / value); + else + throw new IllegalArgumentException("Unsupported unit " + unit); + } + + @Override + public String toString() { + return usNumberFormat.format(value) + '/' + unit; + } + + public Double getValue() { + return value; + } + + public Unit getUnit() { + return unit; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/util/package-info.java b/org.argeo.cms/src/org/argeo/cms/util/package-info.java new file mode 100644 index 000000000..5efc68afb --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/util/package-info.java @@ -0,0 +1,2 @@ +/** Generic Java utilities. */ +package org.argeo.cms.util; \ No newline at end of file diff --git a/org.argeo.util/.classpath b/org.argeo.util/.classpath deleted file mode 100644 index 4199cd3a3..000000000 --- a/org.argeo.util/.classpath +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/org.argeo.util/.project b/org.argeo.util/.project deleted file mode 100644 index 171ff88dc..000000000 --- a/org.argeo.util/.project +++ /dev/null @@ -1,28 +0,0 @@ - - - org.argeo.util - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.pde.ManifestBuilder - - - - - org.eclipse.pde.SchemaBuilder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.pde.PluginNature - - diff --git a/org.argeo.util/META-INF/.gitignore b/org.argeo.util/META-INF/.gitignore deleted file mode 100644 index 4854a41b9..000000000 --- a/org.argeo.util/META-INF/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/MANIFEST.MF diff --git a/org.argeo.util/bnd.bnd b/org.argeo.util/bnd.bnd deleted file mode 100644 index 5f42f7786..000000000 --- a/org.argeo.util/bnd.bnd +++ /dev/null @@ -1,6 +0,0 @@ -Bundle-Activator: org.argeo.osgi.internal.EnterpriseActivator -Bundle-ActivationPolicy: lazy - -Import-Package: org.osgi.*;version=0.0.0,\ -!org.apache.commons.logging,\ -* diff --git a/org.argeo.util/build.properties b/org.argeo.util/build.properties deleted file mode 100644 index ae2abc5ff..000000000 --- a/org.argeo.util/build.properties +++ /dev/null @@ -1 +0,0 @@ -source.. = src/ \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/osgi/internal/EnterpriseActivator.java b/org.argeo.util/src/org/argeo/osgi/internal/EnterpriseActivator.java deleted file mode 100644 index bb495dd12..000000000 --- a/org.argeo.util/src/org/argeo/osgi/internal/EnterpriseActivator.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.argeo.osgi.internal; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -/** - * Called to gather information about the OSGi runtime. Should not activate - * anything else that canonical monitoring services (not creating implicit - * APIs), which is the responsibility of higher levels. - */ -public class EnterpriseActivator implements BundleActivator { - - @Override - public void start(BundleContext context) throws Exception { - } - - @Override - public void stop(BundleContext context) throws Exception { - } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java b/org.argeo.util/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java deleted file mode 100644 index f5e858999..000000000 --- a/org.argeo.util/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java +++ /dev/null @@ -1,122 +0,0 @@ -package org.argeo.osgi.provisioning; - -import java.io.IOException; -import java.util.Collections; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.Map; -import java.util.TreeMap; -import java.util.zip.ZipInputStream; - -import org.osgi.service.provisioning.ProvisioningService; - -public class SimpleProvisioningService implements ProvisioningService { - private Map map = Collections.synchronizedSortedMap(new TreeMap<>()); - - public SimpleProvisioningService() { - // update count - map.put(PROVISIONING_UPDATE_COUNT, 0); - } - - @Override - public Dictionary getInformation() { - return new Information(); - } - - @SuppressWarnings("rawtypes") - @Override - public synchronized void setInformation(Dictionary info) { - map.clear(); - addInformation(info); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public synchronized void addInformation(Dictionary info) { - Enumeration e = info.keys(); - while (e.hasMoreElements()) { - String key = e.nextElement(); - map.put(key, info.get(key)); - } - incrementProvisioningUpdateCount(); - } - - protected synchronized void incrementProvisioningUpdateCount() { - Integer current = (Integer) map.get(PROVISIONING_UPDATE_COUNT); - Integer newValue = current + 1; - map.put(PROVISIONING_UPDATE_COUNT, newValue); - } - - @Override - public synchronized void addInformation(ZipInputStream zis) throws IOException { - throw new UnsupportedOperationException(); - } - - - - class Information extends Dictionary { - - @Override - public int size() { - return map.size(); - } - - @Override - public boolean isEmpty() { - return map.isEmpty(); - } - - @Override - public Enumeration keys() { - Iterator it = map.keySet().iterator(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return it.hasNext(); - } - - @Override - public String nextElement() { - return it.next(); - } - - }; - } - - @Override - public Enumeration elements() { - Iterator it = map.values().iterator(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return it.hasNext(); - } - - @Override - public Object nextElement() { - return it.next(); - } - - }; - } - - @Override - public Object get(Object key) { - return map.get(key); - } - - @Override - public Object put(String key, Object value) { - throw new UnsupportedOperationException(); - } - - @Override - public Object remove(Object key) { - throw new UnsupportedOperationException(); - } - - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/provisioning/package-info.java b/org.argeo.util/src/org/argeo/osgi/provisioning/package-info.java deleted file mode 100644 index 1859887e2..000000000 --- a/org.argeo.util/src/org/argeo/osgi/provisioning/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** OSGi provisioning support. */ -package org.argeo.osgi.provisioning; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java deleted file mode 100644 index 05ba94889..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.security.auth.x500.X500Principal; - -import org.osgi.service.useradmin.Authorization; - -/** An {@link Authorization} which combines roles form various auth sources. */ -class AggregatingAuthorization implements Authorization { - private final String name; - private final String displayName; - private final Set systemRoles; - private final Set roles; - - public AggregatingAuthorization(String name, String displayName, Set systemRoles, String[] roles) { - this.name = new X500Principal(name).getName(); - this.displayName = displayName; - this.systemRoles = Collections.unmodifiableSet(new HashSet<>(systemRoles)); - Set temp = new HashSet<>(); - for (String role : roles) { - if (!temp.contains(role)) - temp.add(role); - } - this.roles = Collections.unmodifiableSet(temp); - } - - @Override - public String getName() { - return name; - } - - @Override - public boolean hasRole(String name) { - if (systemRoles.contains(name)) - return true; - if (roles.contains(name)) - return true; - return false; - } - - @Override - public String[] getRoles() { - int size = systemRoles.size() + roles.size(); - List res = new ArrayList(size); - res.addAll(systemRoles); - res.addAll(roles); - return res.toArray(new String[size]); - } - - @Override - public int hashCode() { - if (name == null) - return super.hashCode(); - return name.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Authorization)) - return false; - Authorization that = (Authorization) obj; - if (name == null) - return that.getName() == null; - return name.equals(that.getName()); - } - - @Override - public String toString() { - return displayName; - } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java deleted file mode 100644 index c9479d51c..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java +++ /dev/null @@ -1,328 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.osgi.useradmin.DirectoryUserAdmin.toLdapName; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.DirectoryConf; -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; - -/** - * Aggregates multiple {@link UserDirectory} and integrates them with system - * roles. - */ -public class AggregatingUserAdmin implements UserAdmin { - private final LdapName systemRolesBaseDn; - private final LdapName tokensBaseDn; - - // DAOs - private DirectoryUserAdmin systemRoles = null; - private DirectoryUserAdmin tokens = null; - private Map businessRoles = new HashMap(); - - // TODO rather use an empty constructor and an init method - public AggregatingUserAdmin(String systemRolesBaseDn, String tokensBaseDn) { - try { - this.systemRolesBaseDn = new LdapName(systemRolesBaseDn); - if (tokensBaseDn != null) - this.tokensBaseDn = new LdapName(tokensBaseDn); - else - this.tokensBaseDn = null; - } catch (InvalidNameException e) { - throw new IllegalStateException("Cannot initialize " + AggregatingUserAdmin.class, e); - } - } - - @Override - public Role createRole(String name, int type) { - return findUserAdmin(name).createRole(name, type); - } - - @Override - public boolean removeRole(String name) { - boolean actuallyDeleted = findUserAdmin(name).removeRole(name); - systemRoles.removeRole(name); - return actuallyDeleted; - } - - @Override - public Role getRole(String name) { - return findUserAdmin(name).getRole(name); - } - - @Override - public Role[] getRoles(String filter) throws InvalidSyntaxException { - List res = new ArrayList(); - for (UserAdmin userAdmin : businessRoles.values()) { - res.addAll(Arrays.asList(userAdmin.getRoles(filter))); - } - res.addAll(Arrays.asList(systemRoles.getRoles(filter))); - return res.toArray(new Role[res.size()]); - } - - @Override - public User getUser(String key, String value) { - List res = new ArrayList(); - for (UserAdmin userAdmin : businessRoles.values()) { - User u = userAdmin.getUser(key, value); - if (u != null) - res.add(u); - } - // Note: node roles cannot contain users, so it is not searched - return res.size() == 1 ? res.get(0) : null; - } - - /** Builds an authorisation by scanning all referentials. */ - @Override - public Authorization getAuthorization(User user) { - if (user == null) {// anonymous - return systemRoles.getAuthorization(null); - } - DirectoryUserAdmin userReferentialOfThisUser = findUserAdmin(user.getName()); - Authorization rawAuthorization = userReferentialOfThisUser.getAuthorization(user); - User retrievedUser = (User) userReferentialOfThisUser.getRole(user.getName()); - String usernameToUse; - String displayNameToUse; - if (user instanceof Group) { - // TODO check whether this is still working - String ownerDn = TokenUtils.userDn((Group) user); - if (ownerDn != null) {// tokens - UserAdmin ownerUserAdmin = findUserAdmin(ownerDn); - User ownerUser = (User) ownerUserAdmin.getRole(ownerDn); - usernameToUse = ownerDn; - displayNameToUse = LdifAuthorization.extractDisplayName(ownerUser); - } else { - usernameToUse = rawAuthorization.getName(); - displayNameToUse = rawAuthorization.toString(); - } - } else {// regular users - usernameToUse = rawAuthorization.getName(); - displayNameToUse = rawAuthorization.toString(); - } - - // gather roles from other referentials - List rawRoles = Arrays.asList(rawAuthorization.getRoles()); - List allRoles = new ArrayList<>(rawRoles); - for (LdapName otherBaseDn : businessRoles.keySet()) { - if (otherBaseDn.equals(userReferentialOfThisUser.getBaseDn())) - continue; - DirectoryUserAdmin otherUserAdmin = userAdminToUse(user, businessRoles.get(otherBaseDn)); - if (otherUserAdmin == null) - continue; - for (String roleStr : rawRoles) { - User role = (User) findUserAdmin(roleStr).getRole(roleStr); - Authorization auth = otherUserAdmin.getAuthorization(role); - allRoles.addAll(Arrays.asList(auth.getRoles())); - } - - } - - // integrate system roles - final DirectoryUserAdmin userAdminToUse = userAdminToUse(retrievedUser, userReferentialOfThisUser); - Objects.requireNonNull(userAdminToUse); - - try { - Set sysRoles = new HashSet(); - for (String role : rawAuthorization.getRoles()) { - User userOrGroup = (User) userAdminToUse.getRole(role); - Authorization auth = systemRoles.getAuthorization(userOrGroup); - systemRoles: for (String systemRole : auth.getRoles()) { - if (role.equals(systemRole)) - continue systemRoles; - sysRoles.add(systemRole); - } -// sysRoles.addAll(Arrays.asList(auth.getRoles())); - } - addAbstractSystemRoles(rawAuthorization, sysRoles); - Authorization authorization = new AggregatingAuthorization(usernameToUse, displayNameToUse, sysRoles, - allRoles.toArray(new String[allRoles.size()])); - return authorization; - } finally { - if (userAdminToUse != null && userAdminToUse.isScoped()) { - userAdminToUse.destroy(); - } - } - } - - /** Decide whether to scope or not */ - private DirectoryUserAdmin userAdminToUse(User user, DirectoryUserAdmin userAdmin) { - if (userAdmin.isAuthenticated()) - return userAdmin; - if (user instanceof DirectoryUser) { - return userAdmin; - } else if (user instanceof AuthenticatingUser) { - return userAdmin.scope(user).orElse(null); - } else { - throw new IllegalArgumentException("Unsupported user type " + user.getClass()); - } - - } - - /** - * Enrich with application-specific roles which are strictly programmatic, such - * as anonymous/user semantics. - */ - protected void addAbstractSystemRoles(Authorization rawAuthorization, Set sysRoles) { - - } - - // - // USER ADMIN AGGREGATOR - // - protected void addUserDirectory(UserDirectory ud) { - if (!(ud instanceof DirectoryUserAdmin)) - throw new IllegalArgumentException("Only " + DirectoryUserAdmin.class.getName() + " is supported"); - DirectoryUserAdmin userDirectory = (DirectoryUserAdmin) ud; - String basePath = userDirectory.getBase(); - if (isSystemRolesBaseDn(basePath)) { - this.systemRoles = userDirectory; - systemRoles.setExternalRoles(this); - } else if (isTokensBaseDn(basePath)) { - this.tokens = userDirectory; - tokens.setExternalRoles(this); - } else { - LdapName baseDn = toLdapName(basePath); - if (businessRoles.containsKey(baseDn)) - throw new IllegalStateException("There is already a user admin for " + baseDn); - businessRoles.put(baseDn, userDirectory); - } - userDirectory.init(); - postAdd(userDirectory); - } - - /** Called after a new user directory has been added */ - protected void postAdd(UserDirectory userDirectory) { - } - - private DirectoryUserAdmin findUserAdmin(String name) { - try { - return findUserAdmin(new LdapName(name)); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Badly formatted name " + name, e); - } - } - - private DirectoryUserAdmin findUserAdmin(LdapName name) { - if (name.startsWith(systemRolesBaseDn)) - return systemRoles; - if (tokensBaseDn != null && name.startsWith(tokensBaseDn)) - return tokens; - List res = new ArrayList<>(1); - userDirectories: for (LdapName baseDn : businessRoles.keySet()) { - DirectoryUserAdmin userDirectory = businessRoles.get(baseDn); - if (name.startsWith(baseDn)) { - if (userDirectory.isDisabled()) - continue userDirectories; -// if (res.isEmpty()) { - res.add(userDirectory); -// } else { -// for (AbstractUserDirectory ud : res) { -// LdapName bd = ud.getBaseDn(); -// if (userDirectory.getBaseDn().startsWith(bd)) { -// // child user directory -// } -// } -// } - } - } - if (res.size() == 0) - throw new IllegalStateException("Cannot find user admin for " + name); - if (res.size() > 1) - throw new IllegalStateException("Multiple user admin found for " + name); - return res.get(0); - } - - protected boolean isSystemRolesBaseDn(String basePath) { - return toLdapName(basePath).equals(systemRolesBaseDn); - } - - protected boolean isTokensBaseDn(String basePath) { - return tokensBaseDn != null && toLdapName(basePath).equals(tokensBaseDn); - } - -// protected Dictionary currentState() { -// Dictionary res = new Hashtable(); -// // res.put(NodeConstants.CN, NodeConstants.DEFAULT); -// for (LdapName name : businessRoles.keySet()) { -// AbstractUserDirectory userDirectory = businessRoles.get(name); -// String uri = UserAdminConf.propertiesAsUri(userDirectory.getProperties()).toString(); -// res.put(uri, ""); -// } -// return res; -// } - - public void start() { - if (systemRoles == null) { - // TODO do we really need separate system roles? - Hashtable properties = new Hashtable<>(); - properties.put(DirectoryConf.baseDn.name(), "ou=roles,ou=system"); - systemRoles = new DirectoryUserAdmin(properties); - } - } - - public void stop() { - for (LdapName name : businessRoles.keySet()) { - DirectoryUserAdmin userDirectory = businessRoles.get(name); - destroy(userDirectory); - } - businessRoles.clear(); - businessRoles = null; - destroy(systemRoles); - systemRoles = null; - } - - private void destroy(DirectoryUserAdmin userDirectory) { - preDestroy(userDirectory); - userDirectory.destroy(); - } - -// protected void removeUserDirectory(UserDirectory userDirectory) { -// LdapName baseDn = toLdapName(userDirectory.getContext()); -// businessRoles.remove(baseDn); -// if (userDirectory instanceof DirectoryUserAdmin) -// destroy((DirectoryUserAdmin) userDirectory); -// } - - @Deprecated - protected void removeUserDirectory(String basePath) { - if (isSystemRolesBaseDn(basePath)) - throw new IllegalArgumentException("System roles cannot be removed "); - LdapName baseDn = toLdapName(basePath); - if (!businessRoles.containsKey(baseDn)) - throw new IllegalStateException("No user directory registered for " + baseDn); - DirectoryUserAdmin userDirectory = businessRoles.remove(baseDn); - destroy(userDirectory); - } - - /** - * Called before each user directory is destroyed, so that additional actions - * can be performed. - */ - protected void preDestroy(UserDirectory userDirectory) { - } - - public Set getUserDirectories() { - TreeSet res = new TreeSet<>((o1, o2) -> o1.getBase().compareTo(o2.getBase())); - res.addAll(businessRoles.values()); - res.add(systemRoles); - return res; - } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java deleted file mode 100644 index ba1f3f753..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.Dictionary; -import java.util.Hashtable; - -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.DirectoryDigestUtils; -import org.osgi.service.useradmin.User; - -/** - * A special user type used during authentication in order to provide the - * credentials required for scoping the user admin. - */ -public class AuthenticatingUser implements User { - /** From com.sun.security.auth.module.*LoginModule */ - public final static String SHARED_STATE_NAME = "javax.security.auth.login.name"; - /** From com.sun.security.auth.module.*LoginModule */ - public final static String SHARED_STATE_PWD = "javax.security.auth.login.password"; - - private final String name; - private final Dictionary credentials; - - public AuthenticatingUser(LdapName name) { - if (name == null) - throw new NullPointerException("Provided name cannot be null."); - this.name = name.toString(); - this.credentials = new Hashtable<>(); - } - - public AuthenticatingUser(String name, Dictionary credentials) { - this.name = name; - this.credentials = credentials; - } - - public AuthenticatingUser(String name, char[] password) { - if (name == null) - throw new NullPointerException("Provided name cannot be null."); - this.name = name; - credentials = new Hashtable<>(); - credentials.put(SHARED_STATE_NAME, name); - byte[] pwd = DirectoryDigestUtils.charsToBytes(password); - credentials.put(SHARED_STATE_PWD, pwd); - } - - @Override - public String getName() { - return name; - } - - @Override - public int getType() { - return User.USER; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public Dictionary getProperties() { - throw new UnsupportedOperationException(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public Dictionary getCredentials() { - return credentials; - } - - @Override - public boolean hasCredential(String key, Object value) { - throw new UnsupportedOperationException(); - } - - @Override - public int hashCode() { - return name.hashCode(); - } - - @Override - public String toString() { - return "Authenticating user " + name; - } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java deleted file mode 100644 index 1d58a2dae..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.argeo.osgi.useradmin; - -import org.osgi.service.useradmin.Group; - -/** A group in a user directroy. */ -interface DirectoryGroup extends Group, DirectoryUser { -// List getMemberNames(); -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java deleted file mode 100644 index 18b28a288..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.argeo.osgi.useradmin; - -import org.osgi.service.useradmin.User; - -/** A user in a user directory. */ -interface DirectoryUser extends User { -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java deleted file mode 100644 index fbcff484c..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java +++ /dev/null @@ -1,400 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.util.naming.LdapAttrs.objectClass; -import static org.argeo.util.naming.LdapObjs.extensibleObject; -import static org.argeo.util.naming.LdapObjs.inetOrgPerson; -import static org.argeo.util.naming.LdapObjs.organizationalPerson; -import static org.argeo.util.naming.LdapObjs.person; -import static org.argeo.util.naming.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.util.CurrentSubject; -import org.argeo.util.directory.DirectoryConf; -import org.argeo.util.directory.DirectoryDigestUtils; -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.directory.ldap.AbstractLdapDirectory; -import org.argeo.util.directory.ldap.LdapDao; -import org.argeo.util.directory.ldap.LdapEntry; -import org.argeo.util.directory.ldap.LdapEntryWorkingCopy; -import org.argeo.util.directory.ldap.LdapNameUtils; -import org.argeo.util.directory.ldap.LdifDao; -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); - } - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifAuthorization.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifAuthorization.java deleted file mode 100644 index d7f6ad960..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifAuthorization.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Dictionary; -import java.util.List; - -import org.argeo.util.naming.LdapAttrs; -import org.osgi.service.useradmin.Authorization; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.User; - -/** Basic authorization. */ -class LdifAuthorization implements Authorization { - private final String name; - private final String displayName; - private final List allRoles; - - public LdifAuthorization(User user, List allRoles) { - if (user == null) { - this.name = null; - this.displayName = "anonymous"; - } else { - this.name = user.getName(); - this.displayName = extractDisplayName(user); - } - // roles - String[] roles = new String[allRoles.size()]; - for (int i = 0; i < allRoles.size(); i++) { - roles[i] = allRoles.get(i).getName(); - } - this.allRoles = Collections.unmodifiableList(Arrays.asList(roles)); - } - - @Override - public String getName() { - return name; - } - - @Override - public boolean hasRole(String name) { - return allRoles.contains(name); - } - - @Override - public String[] getRoles() { - return allRoles.toArray(new String[allRoles.size()]); - } - - @Override - public int hashCode() { - if (name == null) - return super.hashCode(); - return name.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Authorization)) - return false; - Authorization that = (Authorization) obj; - if (name == null) - return that.getName() == null; - return name.equals(that.getName()); - } - - @Override - public String toString() { - return displayName; - } - - final static String extractDisplayName(User user) { - Dictionary props = user.getProperties(); - Object displayName = props.get(LdapAttrs.displayName.name()); - if (displayName == null) - displayName = props.get(LdapAttrs.cn.name()); - if (displayName == null) - displayName = props.get(LdapAttrs.uid.name()); - if (displayName == null) - displayName = user.getName(); - if (displayName == null) - throw new IllegalStateException("Cannot set display name for " + user); - return displayName.toString(); - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java deleted file mode 100644 index bdf34aa91..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java +++ /dev/null @@ -1,127 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.ArrayList; -import java.util.List; - -import javax.naming.InvalidNameException; -import javax.naming.directory.Attribute; -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.ldap.AbstractLdapDirectory; -import org.osgi.service.useradmin.Role; - -/** Directory group implementation */ -class LdifGroup extends LdifUser implements DirectoryGroup { - private final String memberAttributeId; - - LdifGroup(AbstractLdapDirectory userAdmin, LdapName dn) { - super(userAdmin, dn); - memberAttributeId = userAdmin.getMemberAttributeId(); - } - - @Override - public boolean addMember(Role role) { - try { - Role foundRole = findRole(new LdapName(role.getName())); - if (foundRole == null) - throw new UnsupportedOperationException( - "Adding role " + role.getName() + " is unsupported within this context."); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Role name" + role.getName() + " is badly formatted"); - } - - getUserAdmin().checkEdit(); - if (!isEditing()) - startEditing(); - - Attribute member = getAttributes().get(memberAttributeId); - if (member != null) { - if (member.contains(role.getName())) - return false; - else - member.add(role.getName()); - } else - getAttributes().put(memberAttributeId, role.getName()); - return true; - } - - @Override - public boolean addRequiredMember(Role role) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeMember(Role role) { - getUserAdmin().checkEdit(); - if (!isEditing()) - startEditing(); - - Attribute member = getAttributes().get(memberAttributeId); - if (member != null) { - if (!member.contains(role.getName())) - return false; - member.remove(role.getName()); - return true; - } else - return false; - } - - @Override - public Role[] getMembers() { - List directMembers = new ArrayList(); - for (LdapName ldapName : getReferences(memberAttributeId)) { - Role role = findRole(ldapName); - if (role == null) { - throw new IllegalStateException("Role " + ldapName + " not found."); - } - directMembers.add(role); - } - return directMembers.toArray(new Role[directMembers.size()]); - } - - /** - * Whether a role with this name can be found from this context. - * - * @return The related {@link Role} or null. - */ - protected Role findRole(LdapName ldapName) { - Role role = getUserAdmin().getRole(ldapName.toString()); - if (role == null) { - if (getUserAdmin().getExternalRoles() != null) - role = getUserAdmin().getExternalRoles().getRole(ldapName.toString()); - } - return role; - } - -// @Override -// public List getMemberNames() { -// Attribute memberAttribute = getAttributes().get(memberAttributeId); -// if (memberAttribute == null) -// return new ArrayList(); -// try { -// List roles = new ArrayList(); -// NamingEnumeration values = memberAttribute.getAll(); -// while (values.hasMore()) { -// LdapName dn = new LdapName(values.next().toString()); -// roles.add(dn); -// } -// return roles; -// } catch (NamingException e) { -// throw new IllegalStateException("Cannot get members", e); -// } -// } - - @Override - public Role[] getRequiredMembers() { - throw new UnsupportedOperationException(); - } - - @Override - public int getType() { - return GROUP; - } - - protected DirectoryUserAdmin getUserAdmin() { - return (DirectoryUserAdmin) getDirectory(); - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java deleted file mode 100644 index 0b07c7565..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.argeo.osgi.useradmin; - -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.ldap.AbstractLdapDirectory; -import org.argeo.util.directory.ldap.DefaultLdapEntry; - -/** Directory user implementation */ -class LdifUser extends DefaultLdapEntry implements DirectoryUser { - LdifUser(AbstractLdapDirectory userAdmin, LdapName dn) { - super(userAdmin, dn); - } - - @Override - public String getName() { - return getDn().toString(); - } - - @Override - public int getType() { - return USER; - } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java deleted file mode 100644 index 5d7e97dde..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.ArrayList; -import java.util.List; - -import javax.naming.NameNotFoundException; -import javax.naming.NamingException; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.directory.ldap.AbstractLdapDirectory; -import org.argeo.util.directory.ldap.AbstractLdapDirectoryDao; -import org.argeo.util.directory.ldap.LdapEntry; -import org.argeo.util.directory.ldap.LdapEntryWorkingCopy; -import org.argeo.util.naming.LdapAttrs; - -/** Pseudo user directory to be used when logging in as OS user. */ -public class OsUserDirectory extends AbstractLdapDirectoryDao { - private final String osUsername = System.getProperty("user.name"); - private final LdapName osUserDn; - private final LdapEntry osUser; - - public OsUserDirectory(AbstractLdapDirectory directory) { - super(directory); - try { - osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + directory.getUserBaseRdn() + "," - + directory.getBaseDn()); -// Attributes attributes = new BasicAttributes(); -// attributes.put(LdapAttrs.uid.name(), osUsername); - osUser = newUser(osUserDn); - } catch (NamingException e) { - throw new IllegalStateException("Cannot create system user", e); - } - } - - @Override - public List getDirectGroups(LdapName dn) { - return new ArrayList<>(); - } - - @Override - public boolean entryExists(LdapName dn) { - return osUserDn.equals(dn); - } - - @Override - public boolean checkConnection() { - return true; - } - - @Override - public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException { - if (osUserDn.equals(key)) - return osUser; - else - throw new NameNotFoundException("Not an OS role"); - } - - @Override - public List doGetEntries(LdapName searchBase, String f, boolean deep) { - List res = new ArrayList<>(); -// if (f == null || f.match(osUser.getProperties())) - res.add(osUser); - return res; - } - - @Override - public HierarchyUnit doGetHierarchyUnit(LdapName dn) { - return null; - } - - @Override - public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { - return new ArrayList<>(); - } - - public void prepare(LdapEntryWorkingCopy wc) { - - } - - public void commit(LdapEntryWorkingCopy wc) { - - } - - public void rollback(LdapEntryWorkingCopy wc) { - - } - - @Override - public void init() { - // TODO Auto-generated method stub - - } - - @Override - public void destroy() { - // TODO Auto-generated method stub - - } - - @Override - public Attributes doGetAttributes(LdapName name) { - try { - return doGetEntry(name).getAttributes(); - } catch (NameNotFoundException e) { - throw new IllegalStateException(name + " doe not exist in " + getDirectory().getBaseDn(), e); - } - } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserUtils.java deleted file mode 100644 index 5d0cbf687..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserUtils.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.net.URISyntaxException; -import java.net.URL; -import java.security.NoSuchAlgorithmException; -import java.security.URIParameter; - -import javax.security.auth.Subject; -import javax.security.auth.login.Configuration; -import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; - -/** Log in based on JDK-provided OS integration. */ -public class OsUserUtils { - private final static String LOGIN_CONTEXT_USER_NIX = "USER_NIX"; - private final static String LOGIN_CONTEXT_USER_NT = "USER_NT"; - - public static String getOsUsername() { - return System.getProperty("user.name"); - } - - public static LoginContext loginAsSystemUser(Subject subject) { - try { - URL jaasConfigurationUrl = OsUserUtils.class.getClassLoader() - .getResource("org/argeo/osgi/useradmin/jaas-os.cfg"); - URIParameter uriParameter = new URIParameter(jaasConfigurationUrl.toURI()); - Configuration jaasConfiguration = Configuration.getInstance("JavaLoginConfig", uriParameter); - LoginContext lc = new LoginContext(isWindows() ? LOGIN_CONTEXT_USER_NT : LOGIN_CONTEXT_USER_NIX, subject, - null, jaasConfiguration); - lc.login(); - return lc; - } catch (URISyntaxException | NoSuchAlgorithmException | LoginException e) { - throw new RuntimeException("Cannot login as system user", e); - } - } - - public static void main(String args[]) { - Subject subject = new Subject(); - LoginContext loginContext = loginAsSystemUser(subject); - System.out.println(subject); - try { - loginContext.logout(); - } catch (LoginException e) { - // silent - } - } - - private static boolean isWindows() { - return System.getProperty("os.name").startsWith("Windows"); - } - - private OsUserUtils() { - } -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/TokenUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/TokenUtils.java deleted file mode 100644 index 178b4ae82..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/TokenUtils.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.util.naming.LdapAttrs.description; -import static org.argeo.util.naming.LdapAttrs.owner; - -import java.security.Principal; -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.security.auth.Subject; - -import org.argeo.util.naming.NamingUtils; -import org.osgi.service.useradmin.Group; - -/** - * Canonically implements the Argeo token conventions. - */ -public class TokenUtils { - public static Set tokensUsed(Subject subject, String tokensBaseDn) { - Set res = new HashSet<>(); - for (Principal principal : subject.getPrincipals()) { - String name = principal.getName(); - if (name.endsWith(tokensBaseDn)) { - try { - LdapName ldapName = new LdapName(name); - String token = ldapName.getRdn(ldapName.size()).getValue().toString(); - res.add(token); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Invalid principal " + principal, e); - } - } - } - return res; - } - - /** The user related to this token group */ - public static String userDn(Group tokenGroup) { - return (String) tokenGroup.getProperties().get(owner.name()); - } - - public static boolean isExpired(Group tokenGroup) { - return isExpired(tokenGroup, Instant.now()); - - } - - public static boolean isExpired(Group tokenGroup, Instant instant) { - String expiryDateStr = (String) tokenGroup.getProperties().get(description.name()); - if (expiryDateStr != null) { - Instant expiryDate = NamingUtils.ldapDateToInstant(expiryDateStr); - if (expiryDate.isBefore(instant)) { - return true; - } - } - return false; - } - -// private final String token; -// -// public TokenUtils(String token) { -// this.token = token; -// } -// -// public String getToken() { -// return token; -// } -// -// @Override -// public int hashCode() { -// return token.hashCode(); -// } -// -// @Override -// public boolean equals(Object obj) { -// if ((obj instanceof TokenUtils) && ((TokenUtils) obj).token.equals(token)) -// return true; -// return false; -// } -// -// @Override -// public String toString() { -// return "Token #" + hashCode(); -// } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java deleted file mode 100644 index 05ed7cf7c..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.argeo.osgi.useradmin; - -import org.argeo.util.directory.Directory; -import org.argeo.util.directory.HierarchyUnit; -import org.osgi.service.useradmin.Role; - -/** Information about a user directory. */ -public interface UserDirectory extends Directory { - - HierarchyUnit getHierarchyUnit(Role role); - - Iterable getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep); - - String getRolePath(Role role); - - String getRoleSimpleName(Role role); - - Role getRoleByPath(String path); -} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/jaas-os.cfg b/org.argeo.util/src/org/argeo/osgi/useradmin/jaas-os.cfg deleted file mode 100644 index da04505a7..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/jaas-os.cfg +++ /dev/null @@ -1,8 +0,0 @@ -USER_NIX { - com.sun.security.auth.module.UnixLoginModule requisite; -}; - -USER_NT { - com.sun.security.auth.module.NTLoginModule requisite; -}; - diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/package-info.java b/org.argeo.util/src/org/argeo/osgi/useradmin/package-info.java deleted file mode 100644 index c108d2c55..000000000 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** LDAP and LDIF based OSGi useradmin implementation. */ -package org.argeo.osgi.useradmin; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/osgi/util/FilterRequirement.java b/org.argeo.util/src/org/argeo/osgi/util/FilterRequirement.java deleted file mode 100644 index 31f1d4de6..000000000 --- a/org.argeo.util/src/org/argeo/osgi/util/FilterRequirement.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.argeo.osgi.util; - -import java.util.HashMap; -import java.util.Map; - -import org.osgi.resource.Namespace; -import org.osgi.resource.Requirement; -import org.osgi.resource.Resource; - -/** Simplify filtering resources. */ -public class FilterRequirement implements Requirement { - private String namespace; - private String filter; - - public FilterRequirement(String namespace, String filter) { - this.namespace = namespace; - this.filter = filter; - } - - @Override - public Resource getResource() { - return null; - } - - @Override - public String getNamespace() { - return namespace; - } - - @Override - public Map getDirectives() { - Map directives = new HashMap<>(); - directives.put(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter); - return directives; - } - - @Override - public Map getAttributes() { - return new HashMap<>(); - } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/util/OnServiceRegistration.java b/org.argeo.util/src/org/argeo/osgi/util/OnServiceRegistration.java deleted file mode 100644 index 5a6760e0f..000000000 --- a/org.argeo.util/src/org/argeo/osgi/util/OnServiceRegistration.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.argeo.osgi.util; - -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Function; - -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.ServiceReference; -import org.osgi.util.tracker.ServiceTracker; - -public class OnServiceRegistration implements Future { - private BundleContext ownBundleContext = FrameworkUtil.getBundle(OnServiceRegistration.class).getBundleContext(); - - private ServiceTracker st; - - private R result; - private boolean cancelled = false; - private Throwable exception; - - public OnServiceRegistration(Class clss, Function function) { - this(null, clss, function); - } - - public OnServiceRegistration(BundleContext bundleContext, Class clss, Function function) { - st = new ServiceTracker(bundleContext != null ? bundleContext : ownBundleContext, clss, null) { - - @Override - public T addingService(ServiceReference reference) { - T service = super.addingService(reference); - try { - if (result != null)// we only want the first one - return service; - result = function.apply(service); - return service; - } catch (Exception e) { - exception = e; - return service; - } finally { - close(); - } - } - }; - st.open(bundleContext == null); - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - if (result != null || exception != null || cancelled) - return false; - st.close(); - cancelled = true; - return true; - } - - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public boolean isDone() { - return result != null || cancelled; - } - - @Override - public R get() throws InterruptedException, ExecutionException { - st.waitForService(0); - return tryGetResult(); - } - - @Override - public R get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - st.waitForService(TimeUnit.MILLISECONDS.convert(timeout, unit)); - if (result == null) - throw new TimeoutException("No result after " + timeout + " " + unit); - return tryGetResult(); - } - - protected R tryGetResult() throws ExecutionException, CancellationException { - if (cancelled) - throw new CancellationException(); - if (exception != null) - throw new ExecutionException(exception); - if (result == null)// this should not happen - try { - throw new IllegalStateException("No result available"); - } catch (Exception e) { - exception = e; - throw new ExecutionException(e); - } - return result; - } - -} diff --git a/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java b/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java deleted file mode 100644 index 5728b90db..000000000 --- a/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.argeo.osgi.util; - -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; -import java.util.concurrent.ForkJoinPool; - -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceRegistration; - -public class OsgiRegister { - private final BundleContext bundleContext; - private Executor executor; - - private CompletableFuture shutdownStarting = new CompletableFuture(); - - public OsgiRegister(BundleContext bundleContext) { - this.bundleContext = bundleContext; - // TODO experiment with dedicated executors - this.executor = ForkJoinPool.commonPool(); - } - - public void set(T obj, Class clss, Map attributes, Class... classes) { - CompletableFuture> srf = new CompletableFuture>(); - CompletableFuture postRegistration = CompletableFuture.supplyAsync(() -> { - List lst = new ArrayList<>(); - lst.add(clss.getName()); - for (Class c : classes) { - lst.add(c.getName()); - } - ServiceRegistration sr = bundleContext.registerService(lst.toArray(new String[lst.size()]), obj, - new Hashtable(attributes)); - srf.complete(sr); - return obj; - }, executor); -// Singleton singleton = new Singleton(clss, postRegistration); - -// shutdownStarting. // -// thenCompose(singleton::prepareUnregistration). // -// thenRunAsync(() -> { -// try { -// srf.get().unregister(); -// } catch (InterruptedException | ExecutionException e) { -// e.printStackTrace(); -// } -// }, executor); -// return singleton; - } - - public void shutdown() { - shutdownStarting.complete(null); - } -} diff --git a/org.argeo.util/src/org/argeo/util/CompositeString.java b/org.argeo.util/src/org/argeo/util/CompositeString.java deleted file mode 100644 index 2f8587dec..000000000 --- a/org.argeo.util/src/org/argeo/util/CompositeString.java +++ /dev/null @@ -1,164 +0,0 @@ -package org.argeo.util; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.StringTokenizer; - -/** A name that can be expressed with various conventions. */ -public class CompositeString { - public final static Character UNDERSCORE = Character.valueOf('_'); - public final static Character SPACE = Character.valueOf(' '); - public final static Character DASH = Character.valueOf('-'); - - private final String[] parts; - - // optimisation - private final int hashCode; - - public CompositeString(String str) { - Objects.requireNonNull(str, "String cannot be null"); - if ("".equals(str.trim())) - throw new IllegalArgumentException("String cannot be empty"); - if (!str.equals(str.trim())) - throw new IllegalArgumentException("String must be trimmed"); - this.parts = toParts(str); - hashCode = hashCode(this.parts); - } - - public String toString(char separator, boolean upperCase) { - StringBuilder sb = null; - for (String part : parts) { - if (sb == null) { - sb = new StringBuilder(); - } else { - sb.append(separator); - } - sb.append(upperCase ? part.toUpperCase() : part); - } - return sb.toString(); - } - - public String toStringCaml(boolean firstCharUpperCase) { - StringBuilder sb = null; - for (String part : parts) { - if (sb == null) {// first - sb = new StringBuilder(); - sb.append(firstCharUpperCase ? Character.toUpperCase(part.charAt(0)) : part.charAt(0)); - } else { - sb.append(Character.toUpperCase(part.charAt(0))); - } - - if (part.length() > 1) - sb.append(part.substring(1)); - } - return sb.toString(); - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public boolean equals(Object obj) { - if (obj == null || !(obj instanceof CompositeString)) - return false; - - CompositeString other = (CompositeString) obj; - return Arrays.equals(parts, other.parts); - } - - @Override - public String toString() { - return toString(DASH, false); - } - - public static String[] toParts(String str) { - Character separator = null; - if (str.indexOf(UNDERSCORE) >= 0) { - checkNo(str, SPACE); - checkNo(str, DASH); - separator = UNDERSCORE; - } else if (str.indexOf(DASH) >= 0) { - checkNo(str, SPACE); - checkNo(str, UNDERSCORE); - separator = DASH; - } else if (str.indexOf(SPACE) >= 0) { - checkNo(str, DASH); - checkNo(str, UNDERSCORE); - separator = SPACE; - } - - List res = new ArrayList<>(); - if (separator != null) { - StringTokenizer st = new StringTokenizer(str, separator.toString()); - while (st.hasMoreTokens()) { - res.add(st.nextToken().toLowerCase()); - } - } else { - // single - String strLowerCase = str.toLowerCase(); - if (str.toUpperCase().equals(str) || strLowerCase.equals(str)) - return new String[] { strLowerCase }; - - // CAML - StringBuilder current = null; - for (char c : str.toCharArray()) { - if (Character.isUpperCase(c)) { - if (current != null) - res.add(current.toString()); - current = new StringBuilder(); - } - if (current == null)// first char is lower case - current = new StringBuilder(); - current.append(Character.toLowerCase(c)); - } - res.add(current.toString()); - } - return res.toArray(new String[res.size()]); - } - - private static void checkNo(String str, Character c) { - if (str.indexOf(c) >= 0) { - throw new IllegalArgumentException("Only one kind of sperator is allowed"); - } - } - - private static int hashCode(String[] parts) { - int hashCode = 0; - for (String part : parts) { - hashCode = hashCode + part.hashCode(); - } - return hashCode; - } - - static boolean smokeTests() { - CompositeString plainName = new CompositeString("NAME"); - assert "name".equals(plainName.toString()); - assert "NAME".equals(plainName.toString(UNDERSCORE, true)); - assert "name".equals(plainName.toString(UNDERSCORE, false)); - assert "name".equals(plainName.toStringCaml(false)); - assert "Name".equals(plainName.toStringCaml(true)); - - CompositeString camlName = new CompositeString("myComplexName"); - - assert new CompositeString("my-complex-name").equals(camlName); - assert new CompositeString("MY_COMPLEX_NAME").equals(camlName); - assert new CompositeString("My complex Name").equals(camlName); - assert new CompositeString("MyComplexName").equals(camlName); - - assert "my-complex-name".equals(camlName.toString()); - assert "MY_COMPLEX_NAME".equals(camlName.toString(UNDERSCORE, true)); - assert "my_complex_name".equals(camlName.toString(UNDERSCORE, false)); - assert "myComplexName".equals(camlName.toStringCaml(false)); - assert "MyComplexName".equals(camlName.toStringCaml(true)); - - return CompositeString.class.desiredAssertionStatus(); - } - - public static void main(String[] args) { - System.out.println(smokeTests()); - } -} diff --git a/org.argeo.util/src/org/argeo/util/CsvParser.java b/org.argeo.util/src/org/argeo/util/CsvParser.java deleted file mode 100644 index b903f7722..000000000 --- a/org.argeo.util/src/org/argeo/util/CsvParser.java +++ /dev/null @@ -1,242 +0,0 @@ -package org.argeo.util; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Parses a CSV file interpreting the first line as a header. The - * {@link #parse(InputStream)} method and the setters are synchronized so that - * the object cannot be modified when parsing. - */ -public abstract class CsvParser { - private char separator = ','; - private char quote = '\"'; - - private Boolean noHeader = false; - private Boolean strictLineAsLongAsHeader = true; - - /** - * Actually process a parsed line. If - * {@link #setStrictLineAsLongAsHeader(Boolean)} is true (default) the header - * and the tokens are guaranteed to have the same size. - * - * @param lineNumber the current line number, starts at 1 (the header, if header - * processing is enabled, the first line otherwise) - * @param header the read-only header or null if - * {@link #setNoHeader(Boolean)} is true (default is false) - * @param tokens the parsed tokens - */ - protected abstract void processLine(Integer lineNumber, List header, List tokens); - - /** - * Parses the CSV file (stream is closed at the end) - * - * @param in the stream to parse - * - * @deprecated Use {@link #parse(InputStream, Charset)} instead. - */ - @Deprecated - public synchronized void parse(InputStream in) { - parse(in, (Charset) null); - } - - /** - * Parses the CSV file (stream is closed at the end) - * - * @param in the stream to parse - * @param encoding the encoding to use. - * - * @deprecated Use {@link #parse(InputStream, Charset)} instead. - */ - @Deprecated - public synchronized void parse(InputStream in, String encoding) { - Reader reader; - if (encoding == null) - reader = new InputStreamReader(in); - else - try { - reader = new InputStreamReader(in, encoding); - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException(e); - } - parse(reader); - } - - /** - * Parses the CSV file (stream is closed at the end) - * - * @param in the stream to parse - * @param charset the charset to use - */ - public synchronized void parse(InputStream in, Charset charset) { - Reader reader; - if (charset == null) - reader = new InputStreamReader(in); - else - reader = new InputStreamReader(in, charset); - parse(reader); - } - - /** - * Parses the CSV file (stream is closed at the end) - * - * @param reader the reader to use (it will be buffered) - */ - public synchronized void parse(Reader reader) { - Integer lineCount = 0; - try (BufferedReader bufferedReader = new BufferedReader(reader)) { - List header = null; - if (!noHeader) { - String headerStr = bufferedReader.readLine(); - if (headerStr == null)// empty file - return; - lineCount++; - header = new ArrayList(); - StringBuffer currStr = new StringBuffer(""); - Boolean wasInquote = false; - while (parseLine(headerStr, header, currStr, wasInquote)) { - headerStr = bufferedReader.readLine(); - if (headerStr == null) - break; - wasInquote = true; - } - header = Collections.unmodifiableList(header); - } - - String line = null; - lines: while ((line = bufferedReader.readLine()) != null) { - line = preProcessLine(line); - if (line == null) { - // skip line - continue lines; - } - lineCount++; - List tokens = new ArrayList(); - StringBuffer currStr = new StringBuffer(""); - Boolean wasInquote = false; - sublines: while (parseLine(line, tokens, currStr, wasInquote)) { - line = bufferedReader.readLine(); - if (line == null) - break sublines; - wasInquote = true; - } - if (!noHeader && strictLineAsLongAsHeader) { - int headerSize = header.size(); - int tokenSize = tokens.size(); - if (tokenSize == 1 && line.trim().equals("")) - continue lines;// empty line - if (headerSize != tokenSize) { - throw new IllegalStateException("Token size " + tokenSize + " is different from header size " - + headerSize + " at line " + lineCount + ", line: " + line + ", header: " + header - + ", tokens: " + tokens); - } - } - processLine(lineCount, header, tokens); - } - } catch (IOException e) { - throw new RuntimeException("Cannot parse CSV file (line: " + lineCount + ")", e); - } - } - - /** - * Called before each (logical) line is processed, giving a change to modify it - * (typically for cleaning dirty files). To be overridden, return the line - * unchanged by default. Skip the line if 'null' is returned. - */ - protected String preProcessLine(String line) { - return line; - } - - /** - * Parses a line character by character for performance purpose - * - * @return whether to continue parsing this line - */ - protected Boolean parseLine(String str, List tokens, StringBuffer currStr, Boolean wasInquote) { - if (wasInquote) - currStr.append('\n'); - - char[] arr = str.toCharArray(); - boolean inQuote = wasInquote; - for (int i = 0; i < arr.length; i++) { - char c = arr[i]; - if (c == separator) { - if (!inQuote) { - tokens.add(currStr.toString()); -// currStr.delete(0, currStr.length()); - currStr.setLength(0); - currStr.trimToSize(); - } else { - // we don't remove separator that are in a quoted substring - // System.out - // .println("IN QUOTE, got a separator: [" + c + "]"); - currStr.append(c); - } - } else if (c == quote) { - if (inQuote && (i + 1) < arr.length && arr[i + 1] == quote) { - // case of double quote - currStr.append(quote); - i++; - } else {// standard - inQuote = inQuote ? false : true; - } - } else { - currStr.append(c); - } - } - - if (!inQuote) { - tokens.add(currStr.toString()); - // System.out.println("# TOKEN: " + currStr); - } - // if (inQuote) - // throw new ArgeoException("Missing quote at the end of the line " - // + str + " (parsed: " + tokens + ")"); - if (inQuote) - return true; - else - return false; - // return tokens; - } - - public char getSeparator() { - return separator; - } - - public synchronized void setSeparator(char separator) { - this.separator = separator; - } - - public char getQuote() { - return quote; - } - - public synchronized void setQuote(char quote) { - this.quote = quote; - } - - public Boolean getNoHeader() { - return noHeader; - } - - public synchronized void setNoHeader(Boolean noHeader) { - this.noHeader = noHeader; - } - - public Boolean getStrictLineAsLongAsHeader() { - return strictLineAsLongAsHeader; - } - - public synchronized void setStrictLineAsLongAsHeader(Boolean strictLineAsLongAsHeader) { - this.strictLineAsLongAsHeader = strictLineAsLongAsHeader; - } - -} diff --git a/org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java b/org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java deleted file mode 100644 index 8eb6e9463..000000000 --- a/org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.argeo.util; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * CSV parser allowing to process lines as maps whose keys are the header - * fields. - */ -public abstract class CsvParserWithLinesAsMap extends CsvParser { - - /** - * Actually processes a line. - * - * @param lineNumber the current line number, starts at 1 (the header, if header - * processing is enabled, the first lien otherwise) - * @param line the parsed tokens as a map whose keys are the header fields - */ - protected abstract void processLine(Integer lineNumber, Map line); - - protected final void processLine(Integer lineNumber, List header, List tokens) { - if (header == null) - throw new IllegalArgumentException("Only CSV with header is supported"); - Map line = new HashMap(); - for (int i = 0; i < header.size(); i++) { - String key = header.get(i); - String value = null; - if (i < tokens.size()) - value = tokens.get(i); - line.put(key, value); - } - processLine(lineNumber, line); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/CsvWriter.java b/org.argeo.util/src/org/argeo/util/CsvWriter.java deleted file mode 100644 index c3b3a3ad7..000000000 --- a/org.argeo.util/src/org/argeo/util/CsvWriter.java +++ /dev/null @@ -1,156 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.util.Iterator; -import java.util.List; - -/** Write in CSV format. */ -public class CsvWriter { - private final Writer out; - - private char separator = ','; - private char quote = '\"'; - - /** - * Creates a CSV writer. - * - * @param out the stream to write to. Caller is responsible for closing it. - * - * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead. - * - */ - @Deprecated - public CsvWriter(OutputStream out) { - this.out = new OutputStreamWriter(out); - } - - /** - * Creates a CSV writer. - * - * @param out the stream to write to. Caller is responsible for closing it. - * @param encoding the encoding to use. - * - * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead. - */ - @Deprecated - public CsvWriter(OutputStream out, String encoding) { - try { - this.out = new OutputStreamWriter(out, encoding); - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException(e); - } - } - - /** - * Creates a CSV writer. - * - * @param out the stream to write to. Caller is responsible for closing it. - * @param charset the charset to use - */ - public CsvWriter(OutputStream out, Charset charset) { - this.out = new OutputStreamWriter(out, charset); - } - - /** - * Creates a CSV writer. - * - * @param out the stream to write to. Caller is responsible for closing it. - */ - public CsvWriter(Writer writer) { - this.out = writer; - } - - /** - * Write a CSV line. Also used to write a header if needed (this is transparent - * for the CSV writer): simply call it first, before writing the lines. - */ - public void writeLine(List tokens) { - try { - Iterator it = tokens.iterator(); - while (it.hasNext()) { - Object obj = it.next(); - writeToken(obj != null ? obj.toString() : null); - if (it.hasNext()) - out.write(separator); - } - out.write('\n'); - out.flush(); - } catch (IOException e) { - throw new RuntimeException("Could not write " + tokens, e); - } - } - - /** - * Write a CSV line. Also used to write a header if needed (this is transparent - * for the CSV writer): simply call it first, before writing the lines. - */ - public void writeLine(Object[] tokens) { - try { - for (int i = 0; i < tokens.length; i++) { - if (tokens[i] == null) { - writeToken(null); - } else { - writeToken(tokens[i].toString()); - } - if (i != (tokens.length - 1)) - out.write(separator); - } - out.write('\n'); - out.flush(); - } catch (IOException e) { - throw new RuntimeException("Could not write " + tokens, e); - } - } - - protected void writeToken(String token) throws IOException { - if (token == null) { - // TODO configure how to deal with null - out.write(""); - return; - } - // +2 for possible quotes, another +2 assuming there would be an already - // quoted string where quotes needs to be duplicated - // another +2 for safety - // we don't want to increase buffer size while writing - StringBuffer buf = new StringBuffer(token.length() + 6); - char[] arr = token.toCharArray(); - boolean shouldQuote = false; - for (char c : arr) { - if (!shouldQuote) { - if (c == separator) - shouldQuote = true; - if (c == '\n') - shouldQuote = true; - } - - if (c == quote) { - shouldQuote = true; - // duplicate quote - buf.append(quote); - } - - // generic case - buf.append(c); - } - - if (shouldQuote == true) - out.write(quote); - out.write(buf.toString()); - if (shouldQuote == true) - out.write(quote); - } - - public void setSeparator(char separator) { - this.separator = separator; - } - - public void setQuote(char quote) { - this.quote = quote; - } - -} diff --git a/org.argeo.util/src/org/argeo/util/CurrentSubject.java b/org.argeo.util/src/org/argeo/util/CurrentSubject.java deleted file mode 100644 index 60ce3cf26..000000000 --- a/org.argeo.util/src/org/argeo/util/CurrentSubject.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.argeo.util; - -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletionException; - -import javax.security.auth.Subject; - -/** - * Prepare evolution of Java APIs introduced in JDK 18, as these static methods - * will be added to {@link Subject}. - */ -@SuppressWarnings("removal") -public class CurrentSubject { - - private final static boolean useThreadLocal = Boolean - .parseBoolean(System.getProperty("jdk.security.auth.subject.useTL")); - - private final static InheritableThreadLocal current = new InheritableThreadLocal<>(); - - public static Subject current() { - if (useThreadLocal) { - return current.get(); - } else {// legacy - Subject subject = Subject.getSubject(AccessController.getContext()); - return subject; - } - } - - public static T callAs(Subject subject, Callable action) { - if (useThreadLocal) { - Subject previous = current(); - current.set(subject); - try { - return action.call(); - } catch (Exception e) { - throw new CompletionException("Failed to execute action for " + subject, e); - } finally { - current.set(previous); - } - } else {// legacy - try { - return Subject.doAs(subject, new PrivilegedExceptionAction() { - - @Override - public T run() throws Exception { - return action.call(); - } - - }); - } catch (PrivilegedActionException e) { - throw new CompletionException("Failed to execute action for " + subject, e.getCause()); - } catch (Exception e) { - throw new CompletionException("Failed to execute action for " + subject, e); - } - } - } - - /** Singleton. */ - private CurrentSubject() { - } - -} diff --git a/org.argeo.util/src/org/argeo/util/DictionaryKeys.java b/org.argeo.util/src/org/argeo/util/DictionaryKeys.java deleted file mode 100644 index d17c86f96..000000000 --- a/org.argeo.util/src/org/argeo/util/DictionaryKeys.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.argeo.util; - -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.Iterator; - -/** - * Access the keys of a {@link String}-keyed {@link Dictionary} (common throughout - * the OSGi APIs) as an {@link Iterable} so that they are easily usable in - * for-each loops. - */ -class DictionaryKeys implements Iterable { - private final Dictionary dictionary; - - public DictionaryKeys(Dictionary dictionary) { - this.dictionary = dictionary; - } - - @Override - public Iterator iterator() { - return new KeyIterator(dictionary.keys()); - } - - private static class KeyIterator implements Iterator { - private final Enumeration keys; - - KeyIterator(Enumeration keys) { - this.keys = keys; - } - - @Override - public boolean hasNext() { - return keys.hasMoreElements(); - } - - @Override - public String next() { - return keys.nextElement(); - } - - } -} diff --git a/org.argeo.util/src/org/argeo/util/DigestUtils.java b/org.argeo.util/src/org/argeo/util/DigestUtils.java deleted file mode 100644 index 38b4e7032..000000000 --- a/org.argeo.util/src/org/argeo/util/DigestUtils.java +++ /dev/null @@ -1,202 +0,0 @@ -package org.argeo.util; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileChannel.MapMode; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -/** Utilities around cryptographic digests */ -public class DigestUtils { - public final static String MD5 = "MD5"; - public final static String SHA1 = "SHA1"; - public final static String SHA256 = "SHA-256"; - public final static String SHA512 = "SHA-512"; - - private static Boolean debug = false; - // TODO: make it configurable - private final static Integer byteBufferCapacity = 100 * 1024;// 100 KB - - public static byte[] sha1(byte[]... bytes) { - try { - MessageDigest digest = MessageDigest.getInstance(SHA1); - for (byte[] arr : bytes) - digest.update(arr); - byte[] checksum = digest.digest(); - return checksum; - } catch (NoSuchAlgorithmException e) { - throw new UnsupportedOperationException("SHA1 is not avalaible", e); - } - } - - public static byte[] digestAsBytes(String algorithm, byte[]... bytes) { - try { - MessageDigest digest = MessageDigest.getInstance(algorithm); - for (byte[] arr : bytes) - digest.update(arr); - byte[] checksum = digest.digest(); - return checksum; - } catch (NoSuchAlgorithmException e) { - throw new UnsupportedOperationException("Cannot digest with algorithm " + algorithm, e); - } - } - - public static String digest(String algorithm, byte[]... bytes) { - return toHexString(digestAsBytes(algorithm, bytes)); - } - - public static String digest(String algorithm, InputStream in) { - try { - MessageDigest digest = MessageDigest.getInstance(algorithm); - // ReadableByteChannel channel = Channels.newChannel(in); - // ByteBuffer bb = ByteBuffer.allocateDirect(byteBufferCapacity); - // while (channel.read(bb) > 0) - // digest.update(bb); - byte[] buffer = new byte[byteBufferCapacity]; - int read = 0; - while ((read = in.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } - - byte[] checksum = digest.digest(); - String res = toHexString(checksum); - return res; - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - StreamUtils.closeQuietly(in); - } - } - - public static String digest(String algorithm, File file) { - FileInputStream fis = null; - FileChannel fc = null; - try { - fis = new FileInputStream(file); - fc = fis.getChannel(); - - // Get the file's size and then map it into memory - int sz = (int) fc.size(); - ByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz); - return digest(algorithm, bb); - } catch (IOException e) { - throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e); - } finally { - StreamUtils.closeQuietly(fis); - if (fc.isOpen()) - try { - fc.close(); - } catch (IOException e) { - // silent - } - } - } - - protected static String digest(String algorithm, ByteBuffer bb) { - long begin = System.currentTimeMillis(); - try { - MessageDigest digest = MessageDigest.getInstance(algorithm); - digest.update(bb); - byte[] checksum = digest.digest(); - String res = toHexString(checksum); - long end = System.currentTimeMillis(); - if (debug) - System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s"); - return res; - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); - } - } - - public static String sha1hex(Path path) { - return digest(SHA1, path, byteBufferCapacity); - } - - public static String digest(String algorithm, Path path, long bufferSize) { - byte[] digest = digestAsBytes(algorithm, path, bufferSize); - return toHexString(digest); - } - - public static byte[] digestAsBytes(String algorithm, Path file, long bufferSize) { - long begin = System.currentTimeMillis(); - try { - MessageDigest md = MessageDigest.getInstance(algorithm); - FileChannel fc = FileChannel.open(file); - long fileSize = Files.size(file); - if (fileSize <= bufferSize) { - ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, fileSize); - md.update(bb); - } else { - long lastCycle = (fileSize / bufferSize) - 1; - long position = 0; - for (int i = 0; i <= lastCycle; i++) { - ByteBuffer bb; - if (i != lastCycle) { - bb = fc.map(MapMode.READ_ONLY, position, bufferSize); - position = position + bufferSize; - } else { - bb = fc.map(MapMode.READ_ONLY, position, fileSize - position); - position = fileSize; - } - md.update(bb); - } - } - long end = System.currentTimeMillis(); - if (debug) - System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s"); - return md.digest(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e); - } catch (IOException e) { - throw new RuntimeException("Cannot digest " + file + " with algorithm " + algorithm, e); - } - } - - public static void main(String[] args) { - File file; - if (args.length > 0) - file = new File(args[0]); - else { - System.err.println("Usage: []" + " (see http://java.sun.com/j2se/1.5.0/" - + "docs/guide/security/CryptoSpec.html#AppA)"); - return; - } - - if (args.length > 1) { - String algorithm = args[1]; - System.out.println(digest(algorithm, file)); - } else { - String algorithm = "MD5"; - System.out.println(algorithm + ": " + digest(algorithm, file)); - algorithm = "SHA"; - System.out.println(algorithm + ": " + digest(algorithm, file)); - System.out.println(algorithm + ": " + sha1hex(file.toPath())); - algorithm = "SHA-256"; - System.out.println(algorithm + ": " + digest(algorithm, file)); - algorithm = "SHA-512"; - System.out.println(algorithm + ": " + digest(algorithm, file)); - } - } - - final private static char[] hexArray = "0123456789abcdef".toCharArray(); - - /** Converts a byte array to an hex String. */ - public static String toHexString(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/DirH.java b/org.argeo.util/src/org/argeo/util/DirH.java deleted file mode 100644 index 013897d23..000000000 --- a/org.argeo.util/src/org/argeo/util/DirH.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.Charset; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** Hashes the hashes of the files in a directory. */ -public class DirH { - - private final static Charset charset = Charset.forName("UTF-16"); - private final static long bufferSize = 200 * 1024 * 1024; - private final static String algorithm = "SHA"; - - private final static byte EOL = (byte) '\n'; - private final static byte SPACE = (byte) ' '; - - private final int hashSize; - - private final byte[][] hashes; - private final byte[][] fileNames; - private final byte[] digest; - private final byte[] dirName; - - /** - * @param dirName can be null or empty - */ - private DirH(byte[][] hashes, byte[][] fileNames, byte[] dirName) { - if (hashes.length != fileNames.length) - throw new IllegalArgumentException(hashes.length + " hashes and " + fileNames.length + " file names"); - this.hashes = hashes; - this.fileNames = fileNames; - this.dirName = dirName == null ? new byte[0] : dirName; - if (hashes.length == 0) {// empty dir - hashSize = 20; - // FIXME what is the digest of an empty dir? - digest = new byte[hashSize]; - Arrays.fill(digest, SPACE); - return; - } - hashSize = hashes[0].length; - for (int i = 0; i < hashes.length; i++) { - if (hashes[i].length != hashSize) - throw new IllegalArgumentException( - "Hash size for " + new String(fileNames[i], charset) + " is " + hashes[i].length); - } - - try { - MessageDigest md = MessageDigest.getInstance(algorithm); - for (int i = 0; i < hashes.length; i++) { - md.update(this.hashes[i]); - md.update(SPACE); - md.update(this.fileNames[i]); - md.update(EOL); - } - digest = md.digest(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest", e); - } - } - - public void print(PrintStream out) { - out.print(DigestUtils.toHexString(digest)); - if (dirName.length > 0) { - out.print(' '); - out.print(new String(dirName, charset)); - } - out.print('\n'); - for (int i = 0; i < hashes.length; i++) { - out.print(DigestUtils.toHexString(hashes[i])); - out.print(' '); - out.print(new String(fileNames[i], charset)); - out.print('\n'); - } - } - - public static DirH digest(Path dir) { - try (DirectoryStream files = Files.newDirectoryStream(dir)) { - List hs = new ArrayList(); - List fNames = new ArrayList<>(); - for (Path file : files) { - if (!Files.isDirectory(file)) { - byte[] digest = DigestUtils.digestAsBytes(algorithm, file, bufferSize); - hs.add(digest); - fNames.add(file.getFileName().toString()); - } - } - - byte[][] fileNames = new byte[fNames.size()][]; - for (int i = 0; i < fNames.size(); i++) { - fileNames[i] = fNames.get(i).getBytes(charset); - } - byte[][] hashes = hs.toArray(new byte[hs.size()][]); - return new DirH(hashes, fileNames, dir.toString().getBytes(charset)); - } catch (IOException e) { - throw new RuntimeException("Cannot digest " + dir, e); - } - } - - public static void main(String[] args) { - try { - DirH dirH = DirH.digest(Paths.get("/home/mbaudier/tmp/")); - dirH.print(System.out); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/org.argeo.util/src/org/argeo/util/ExceptionsChain.java b/org.argeo.util/src/org/argeo/util/ExceptionsChain.java deleted file mode 100644 index 9f824213d..000000000 --- a/org.argeo.util/src/org/argeo/util/ExceptionsChain.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.argeo.util; - -import java.util.ArrayList; -import java.util.List; - -/** - * Serialisable wrapper of a {@link Throwable}. typically to be written as XML - * or JSON in a server error response. - */ -public class ExceptionsChain { - private List exceptions = new ArrayList<>(); - - public ExceptionsChain() { - } - - public ExceptionsChain(Throwable exception) { - writeException(exception); - } - - /** recursive */ - protected void writeException(Throwable exception) { - SystemException systemException = new SystemException(exception); - exceptions.add(systemException); - Throwable cause = exception.getCause(); - if (cause != null) - writeException(cause); - } - - public List getExceptions() { - return exceptions; - } - - public void setExceptions(List exceptions) { - this.exceptions = exceptions; - } - - /** An exception in the chain. */ - public static class SystemException { - private String type; - private String message; - private List stackTrace; - - public SystemException() { - } - - public SystemException(Throwable exception) { - this.type = exception.getClass().getName(); - this.message = exception.getMessage(); - this.stackTrace = new ArrayList<>(); - StackTraceElement[] elems = exception.getStackTrace(); - for (int i = 0; i < elems.length; i++) - stackTrace.add("at " + elems[i].toString()); - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getMessage() { - return message; - } - - public void setMessage(String message) { - this.message = message; - } - - public List getStackTrace() { - return stackTrace; - } - - public void setStackTrace(List stackTrace) { - this.stackTrace = stackTrace; - } - - @Override - public String toString() { - return "System exception: " + type + ", " + message + ", " + stackTrace; - } - - } - - @Override - public String toString() { - return exceptions.toString(); - } -} diff --git a/org.argeo.util/src/org/argeo/util/FsUtils.java b/org.argeo.util/src/org/argeo/util/FsUtils.java deleted file mode 100644 index cd61b5619..000000000 --- a/org.argeo.util/src/org/argeo/util/FsUtils.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; - -/** Utilities around the standard Java file abstractions. */ -public class FsUtils { - - /** Deletes this path, recursively if needed. */ - public static void copyDirectory(Path source, Path target) { - if (!Files.exists(source) || !Files.isDirectory(source)) - throw new IllegalArgumentException(source + " is not a directory"); - if (Files.exists(target) && !Files.isDirectory(target)) - throw new IllegalArgumentException(target + " is not a directory"); - try { - Files.createDirectories(target); - Files.walkFileTree(source, new SimpleFileVisitor() { - - @Override - public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs) throws IOException { - Path relativePath = source.relativize(directory); - Path targetDirectory = target.resolve(relativePath); - if (!Files.exists(targetDirectory)) - Files.createDirectory(targetDirectory); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Path relativePath = source.relativize(file); - Path targetFile = target.resolve(relativePath); - Files.copy(file, targetFile); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - throw new RuntimeException("Cannot copy " + source + " to " + target, e); - } - - } - - /** - * Deletes this path, recursively if needed. Does nothing if the path does not - * exist. - */ - public static void delete(Path path) { - try { - if (!Files.exists(path)) - return; - Files.walkFileTree(path, new SimpleFileVisitor() { - @Override - public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException { - if (e != null) - throw e; - Files.delete(directory); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - }); - } catch (IOException e) { - throw new RuntimeException("Cannot delete " + path, e); - } - } - - /** Singleton. */ - private FsUtils() { - } - -} diff --git a/org.argeo.util/src/org/argeo/util/LangUtils.java b/org.argeo.util/src/org/argeo/util/LangUtils.java deleted file mode 100644 index 1aee28c03..000000000 --- a/org.argeo.util/src/org/argeo/util/LangUtils.java +++ /dev/null @@ -1,331 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Writer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.time.temporal.Temporal; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -/** Utilities around Java basic features. */ -public class LangUtils { - /* - * NON-API OSGi - */ - /** - * Returns an array with the names of the provided classes. Useful when - * registering services with multiple interfaces in OSGi. - */ - public static String[] names(Class... clzz) { - String[] res = new String[clzz.length]; - for (int i = 0; i < clzz.length; i++) - res[i] = clzz[i].getName(); - return res; - } - -// /* -// * MAP -// */ -// /** -// * Creates a new {@link Map} with one key-value pair. Key should not be null, -// * but if the value is null, it returns an empty {@link Map}. -// * -// * @deprecated Use {@link Collections#singletonMap(Object, Object)} instead. -// */ -// @Deprecated -// public static Map map(String key, Object value) { -// assert key != null; -// HashMap props = new HashMap<>(); -// if (value != null) -// props.put(key, value); -// return props; -// } - - /* - * DICTIONARY - */ - - /** - * Creates a new {@link Dictionary} with one key-value pair. Key should not be - * null, but if the value is null, it returns an empty {@link Dictionary}. - */ - public static Dictionary dict(String key, Object value) { - assert key != null; - Hashtable props = new Hashtable<>(); - if (value != null) - props.put(key, value); - return props; - } - - /** @deprecated Use {@link #dict(String, Object)} instead. */ - @Deprecated - public static Dictionary dico(String key, Object value) { - return dict(key, value); - } - - /** Converts a {@link Dictionary} to a {@link Map} of strings. */ - public static Map dictToStringMap(Dictionary properties) { - if (properties == null) { - return null; - } - Map res = new HashMap<>(properties.size()); - Enumeration keys = properties.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - res.put(key, properties.get(key).toString()); - } - return res; - } - - /** Converts a {@link Dictionary} to a {@link Map}. */ - public static Map dictToMap(Dictionary properties) { - if (properties == null) { - return null; - } - Map res = new HashMap<>(properties.size()); - Enumeration keys = properties.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - res.put(key, properties.get(key)); - } - return res; - } - - /** - * Get a string property from this map, expecting to find it, or - * null if not found. - */ - public static String get(Map map, String key) { - Object res = map.get(key); - if (res == null) - return null; - return res.toString(); - } - - /** - * Get a string property from this map, expecting to find it. - * - * @throws IllegalArgumentException if the key was not found - */ - public static String getNotNull(Map map, String key) { - Object res = map.get(key); - if (res == null) - throw new IllegalArgumentException("Map " + map + " should contain key " + key); - return res.toString(); - } - - /** - * Wraps the keys of the provided {@link Dictionary} as an {@link Iterable}. - */ - public static Iterable keys(Dictionary props) { - assert props != null; - return new DictionaryKeys(props); - } - - static String toJson(Dictionary props) { - return toJson(props, false); - } - - static String toJson(Dictionary props, boolean pretty) { - StringBuilder sb = new StringBuilder(); - sb.append('{'); - if (pretty) - sb.append('\n'); - Enumeration keys = props.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - if (pretty) - sb.append(' '); - sb.append('\"').append(key).append('\"'); - if (pretty) - sb.append(" : "); - else - sb.append(':'); - sb.append('\"').append(props.get(key)).append('\"'); - if (keys.hasMoreElements()) - sb.append(", "); - if (pretty) - sb.append('\n'); - } - sb.append('}'); - return sb.toString(); - } - - static void storeAsProperties(Dictionary props, Path path) throws IOException { - if (props == null) - throw new IllegalArgumentException("Props cannot be null"); - Properties toStore = new Properties(); - for (Enumeration keys = props.keys(); keys.hasMoreElements();) { - String key = keys.nextElement(); - toStore.setProperty(key, props.get(key).toString()); - } - try (OutputStream out = Files.newOutputStream(path)) { - toStore.store(out, null); - } - } - - static void appendAsLdif(String dnBase, String dnKey, Dictionary props, Path path) - throws IOException { - if (props == null) - throw new IllegalArgumentException("Props cannot be null"); - Object dnValue = props.get(dnKey); - String dnStr = dnKey + '=' + dnValue + ',' + dnBase; - LdapName dn; - try { - dn = new LdapName(dnStr); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Cannot interpret DN " + dnStr, e); - } - if (dnValue == null) - throw new IllegalArgumentException("DN key " + dnKey + " must have a value"); - try (Writer writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) { - writer.append("\ndn: "); - writer.append(dn.toString()); - writer.append('\n'); - for (Enumeration keys = props.keys(); keys.hasMoreElements();) { - String key = keys.nextElement(); - Object value = props.get(key); - writer.append(key); - writer.append(": "); - // FIXME deal with binary and multiple values - writer.append(value.toString()); - writer.append('\n'); - } - } - } - - static Dictionary loadFromProperties(Path path) throws IOException { - Properties toLoad = new Properties(); - try (InputStream in = Files.newInputStream(path)) { - toLoad.load(in); - } - Dictionary res = new Hashtable(); - for (Object key : toLoad.keySet()) - res.put(key.toString(), toLoad.get(key)); - return res; - } - - /* - * COLLECTIONS - */ - /** - * Convert a comma-separated separated {@link String} or a {@link String} array - * to a {@link List} of {@link String}, trimming them. Useful to quickly - * interpret OSGi services properties. - * - * @return a {@link List} containing the trimmed {@link String}s, or an empty - * {@link List} if the argument was null. - */ - public static List toStringList(Object value) { - List values = new ArrayList<>(); - if (value == null) - return values; - String[] arr; - if (value instanceof String) { - arr = ((String) value).split(","); - } else if (value instanceof String[]) { - arr = (String[]) value; - } else { - throw new IllegalArgumentException("Unsupported value type " + value.getClass()); - } - for (String str : arr) { - values.add(str.trim()); - } - return values; - } - - /** Size of an {@link Iterable}, optimised if it is a {@link Collection}. */ - public static int size(Iterable iterable) { - if (iterable instanceof Collection) - return ((Collection) iterable).size(); - - int size = 0; - for (Iterator it = iterable.iterator(); it.hasNext(); size++) - it.next(); - return size; - } - - public static T getAt(Iterable iterable, int index) { - if (iterable instanceof List) { - List lst = ((List) iterable); - if (index >= lst.size()) - throw new IllegalArgumentException("Index " + index + " is not available (size is " + lst.size() + ")"); - return lst.get(index); - } - int i = 0; - for (Iterator it = iterable.iterator(); it.hasNext(); i++) { - if (i == index) - return it.next(); - else - it.next(); - } - throw new IllegalArgumentException("Index " + index + " is not available (size is " + i + ")"); - } - - /* - * EXCEPTIONS - */ - /** - * Chain the messages of all causes (one per line, starts with a line - * return) without all the stack - */ - public static String chainCausesMessages(Throwable t) { - StringBuffer buf = new StringBuffer(); - chainCauseMessage(buf, t); - return buf.toString(); - } - - /** Recursive chaining of messages */ - private static void chainCauseMessage(StringBuffer buf, Throwable t) { - buf.append('\n').append(' ').append(t.getClass().getCanonicalName()).append(": ").append(t.getMessage()); - if (t.getCause() != null) - chainCauseMessage(buf, t.getCause()); - } - - /* - * TIME - */ - /** Formats time elapsed since start. */ - public static String since(ZonedDateTime start) { - ZonedDateTime now = ZonedDateTime.now(); - return duration(start, now); - } - - /** Formats a duration. */ - public static String duration(Temporal start, Temporal end) { - long count = ChronoUnit.DAYS.between(start, end); - if (count != 0) - return count > 1 ? count + " days" : count + " day"; - count = ChronoUnit.HOURS.between(start, end); - if (count != 0) - return count > 1 ? count + " hours" : count + " hours"; - count = ChronoUnit.MINUTES.between(start, end); - if (count != 0) - return count > 1 ? count + " minutes" : count + " minute"; - count = ChronoUnit.SECONDS.between(start, end); - return count > 1 ? count + " seconds" : count + " second"; - } - - /** Singleton constructor. */ - private LangUtils() { - - } - -} diff --git a/org.argeo.util/src/org/argeo/util/OS.java b/org.argeo.util/src/org/argeo/util/OS.java deleted file mode 100644 index 174f45b78..000000000 --- a/org.argeo.util/src/org/argeo/util/OS.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.argeo.util; - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; - -/** When OS specific informations are needed. */ -public class OS { - public final static OS LOCAL = new OS(); - - private final String arch, name, version; - - /** The OS of the running JVM */ - protected OS() { - arch = System.getProperty("os.arch"); - name = System.getProperty("os.name"); - version = System.getProperty("os.version"); - } - - public String getArch() { - return arch; - } - - public String getName() { - return name; - } - - public String getVersion() { - return version; - } - - public boolean isMSWindows() { - // only MS Windows would use such an horrendous separator... - return File.separatorChar == '\\'; - } - - public String[] getDefaultShellCommand() { - if (!isMSWindows()) - return new String[] { "/bin/bash", "-l", "-i" }; - else - return new String[] { "cmd.exe", "/C" }; - } - - public static long getJvmPid() { - return ProcessHandle.current().pid(); -// String pidAndHost = ManagementFactory.getRuntimeMXBean().getName(); -// return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@'))); - } - - /** - * Get the runtime directory. It will be the environment variable - * XDG_RUNTIME_DIR if it is set, or ~/.cache/argeo if not. - */ - public static Path getRunDir() { - Path runDir; - String xdgRunDir = System.getenv("XDG_RUNTIME_DIR"); - if (xdgRunDir != null) { - // TODO support multiple names - runDir = Paths.get(xdgRunDir); - } else { - runDir = Paths.get(System.getProperty("user.home"), ".cache/argeo"); - } - return runDir; - } -} diff --git a/org.argeo.util/src/org/argeo/util/PasswordEncryption.java b/org.argeo.util/src/org/argeo/util/PasswordEncryption.java deleted file mode 100644 index c95c7879e..000000000 --- a/org.argeo.util/src/org/argeo/util/PasswordEncryption.java +++ /dev/null @@ -1,216 +0,0 @@ -package org.argeo.util; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.Key; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -public class PasswordEncryption { - public final static Integer DEFAULT_ITERATION_COUNT = 1024; - /** Stronger with 256, but causes problem with Oracle JVM */ - public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256; - public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128; - public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1"; - public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES"; - public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding"; -// public final static String DEFAULT_CHARSET = "UTF-8"; - public final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - private Integer iterationCount = DEFAULT_ITERATION_COUNT; - private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH; - private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY; - private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION; - private String cipherName = DEFAULT_CIPHER_NAME; - - private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, - (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; - private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, - (byte) 0x35, (byte) 0xE3, (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, - (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; - - private Key key; - private Cipher ecipher; - private Cipher dcipher; - - private String securityProviderName = null; - - /** - * This is up to the caller to clear the passed array. Neither copy of nor - * reference to the passed array is kept - */ - public PasswordEncryption(char[] password) { - this(password, DEFAULT_SALT_8, DEFAULT_IV_16); - } - - /** - * This is up to the caller to clear the passed array. Neither copies of nor - * references to the passed arrays are kept - */ - public PasswordEncryption(char[] password, byte[] passwordSalt, byte[] initializationVector) { - try { - initKeyAndCiphers(password, passwordSalt, initializationVector); - } catch (InvalidKeyException e) { - Integer previousSecreteKeyLength = secreteKeyLength; - secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED; - System.err.println("'" + e.getMessage() + "', will use " + secreteKeyLength - + " secrete key length instead of " + previousSecreteKeyLength); - try { - initKeyAndCiphers(password, passwordSalt, initializationVector); - } catch (GeneralSecurityException e1) { - throw new IllegalStateException("Cannot get secret key (with restricted length)", e1); - } - } catch (GeneralSecurityException e) { - throw new IllegalStateException("Cannot get secret key", e); - } - } - - protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, byte[] initializationVector) - throws GeneralSecurityException { - byte[] salt = new byte[8]; - System.arraycopy(passwordSalt, 0, salt, 0, salt.length); - // for (int i = 0; i < password.length && i < salt.length; i++) - // salt[i] = (byte) password[i]; - byte[] iv = new byte[16]; - System.arraycopy(initializationVector, 0, iv, 0, iv.length); - - SecretKeyFactory keyFac = SecretKeyFactory.getInstance(getSecretKeyFactoryName()); - PBEKeySpec keySpec = new PBEKeySpec(password, salt, getIterationCount(), getKeyLength()); - String secKeyEncryption = getSecretKeyEncryption(); - if (secKeyEncryption != null) { - SecretKey tmp = keyFac.generateSecret(keySpec); - key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption()); - } else { - key = keyFac.generateSecret(keySpec); - } - if (securityProviderName != null) - ecipher = Cipher.getInstance(getCipherName(), securityProviderName); - else - ecipher = Cipher.getInstance(getCipherName()); - ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); - dcipher = Cipher.getInstance(getCipherName()); - dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); - } - - public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) throws IOException { - try { - CipherOutputStream out = new CipherOutputStream(encryptedOut, ecipher); - StreamUtils.copy(decryptedIn, out); - StreamUtils.closeQuietly(out); - } catch (IOException e) { - throw e; - } finally { - StreamUtils.closeQuietly(decryptedIn); - } - } - - public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) throws IOException { - try { - CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, dcipher); - StreamUtils.copy(decryptedIn, decryptedOut); - } catch (IOException e) { - throw e; - } finally { - StreamUtils.closeQuietly(encryptedIn); - } - } - - public byte[] encryptString(String str) { - ByteArrayOutputStream out = null; - ByteArrayInputStream in = null; - try { - out = new ByteArrayOutputStream(); - in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET)); - encrypt(in, out); - return out.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - StreamUtils.closeQuietly(out); - } - } - - /** Closes the input stream */ - public String decryptAsString(InputStream in) { - ByteArrayOutputStream out = null; - try { - out = new ByteArrayOutputStream(); - decrypt(in, out); - return new String(out.toByteArray(), DEFAULT_CHARSET); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - StreamUtils.closeQuietly(out); - } - } - - protected Key getKey() { - return key; - } - - protected Cipher getEcipher() { - return ecipher; - } - - protected Cipher getDcipher() { - return dcipher; - } - - protected Integer getIterationCount() { - return iterationCount; - } - - protected Integer getKeyLength() { - return secreteKeyLength; - } - - protected String getSecretKeyFactoryName() { - return secreteKeyFactoryName; - } - - protected String getSecretKeyEncryption() { - return secreteKeyEncryption; - } - - protected String getCipherName() { - return cipherName; - } - - public void setIterationCount(Integer iterationCount) { - this.iterationCount = iterationCount; - } - - public void setSecreteKeyLength(Integer keyLength) { - this.secreteKeyLength = keyLength; - } - - public void setSecreteKeyFactoryName(String secreteKeyFactoryName) { - this.secreteKeyFactoryName = secreteKeyFactoryName; - } - - public void setSecreteKeyEncryption(String secreteKeyEncryption) { - this.secreteKeyEncryption = secreteKeyEncryption; - } - - public void setCipherName(String cipherName) { - this.cipherName = cipherName; - } - - public void setSecurityProviderName(String securityProviderName) { - this.securityProviderName = securityProviderName; - } -} diff --git a/org.argeo.util/src/org/argeo/util/ServiceChannel.java b/org.argeo.util/src/org/argeo/util/ServiceChannel.java deleted file mode 100644 index 799738414..000000000 --- a/org.argeo.util/src/org/argeo/util/ServiceChannel.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.AsynchronousByteChannel; -import java.nio.channels.CompletionHandler; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; - -/** An {@link AsynchronousByteChannel} based on an {@link ExecutorService}. */ -public class ServiceChannel implements AsynchronousByteChannel { - private final ReadableByteChannel in; - private final WritableByteChannel out; - - private boolean open = true; - - private ExecutorService executor; - - public ServiceChannel(ReadableByteChannel in, WritableByteChannel out, ExecutorService executor) { - this.in = in; - this.out = out; - this.executor = executor; - } - - @Override - public Future read(ByteBuffer dst) { - return executor.submit(() -> in.read(dst)); - } - - @Override - public void read(ByteBuffer dst, A attachment, CompletionHandler handler) { - try { - Future res = read(dst); - handler.completed(res.get(), attachment); - } catch (Exception e) { - handler.failed(e, attachment); - } - } - - @Override - public Future write(ByteBuffer src) { - return executor.submit(() -> out.write(src)); - } - - @Override - public void write(ByteBuffer src, A attachment, CompletionHandler handler) { - try { - Future res = write(src); - handler.completed(res.get(), attachment); - } catch (Exception e) { - handler.failed(e, attachment); - } - } - - @Override - public synchronized void close() throws IOException { - try { - in.close(); - } catch (Exception e) { - e.printStackTrace(); - } - try { - out.close(); - } catch (Exception e) { - e.printStackTrace(); - } - open = false; - notifyAll(); - } - - @Override - public synchronized boolean isOpen() { - return open; - } - -} diff --git a/org.argeo.util/src/org/argeo/util/StreamUtils.java b/org.argeo.util/src/org/argeo/util/StreamUtils.java deleted file mode 100644 index 5e4e636fc..000000000 --- a/org.argeo.util/src/org/argeo/util/StreamUtils.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.argeo.util; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.Writer; -import java.util.StringJoiner; - -/** Stream utilities to be used when Apache Commons IO is not available. */ -public class StreamUtils { - private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; - - /* - * APACHE COMMONS IO (inspired) - */ - - /** @return the number of bytes */ - public static Long copy(InputStream in, OutputStream out) throws IOException { - Long count = 0l; - byte[] buf = new byte[DEFAULT_BUFFER_SIZE]; - while (true) { - int length = in.read(buf); - if (length < 0) - break; - out.write(buf, 0, length); - count = count + length; - } - return count; - } - - /** @return the number of chars */ - public static Long copy(Reader in, Writer out) throws IOException { - Long count = 0l; - char[] buf = new char[DEFAULT_BUFFER_SIZE]; - while (true) { - int length = in.read(buf); - if (length < 0) - break; - out.write(buf, 0, length); - count = count + length; - } - return count; - } - - public static byte[] toByteArray(InputStream in) throws IOException { - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - copy(in, out); - return out.toByteArray(); - } - } - - public static void closeQuietly(InputStream in) { - if (in != null) - try { - in.close(); - } catch (Exception e) { - // - } - } - - public static void closeQuietly(OutputStream out) { - if (out != null) - try { - out.close(); - } catch (Exception e) { - // - } - } - - public static void closeQuietly(Reader in) { - if (in != null) - try { - in.close(); - } catch (Exception e) { - // - } - } - - public static void closeQuietly(Writer out) { - if (out != null) - try { - out.close(); - } catch (Exception e) { - // - } - } - - public static String toString(BufferedReader reader) throws IOException { - StringJoiner sn = new StringJoiner("\n"); - String line = null; - while ((line = reader.readLine()) != null) - sn.add(line); - return sn.toString(); - } -} diff --git a/org.argeo.util/src/org/argeo/util/Tester.java b/org.argeo.util/src/org/argeo/util/Tester.java deleted file mode 100644 index 31a2be4ec..000000000 --- a/org.argeo.util/src/org/argeo/util/Tester.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.argeo.util; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -/** A generic tester based on Java assertions and functional programming. */ -public class Tester { - private Map results = Collections.synchronizedSortedMap(new TreeMap<>()); - - private ClassLoader classLoader; - - /** Use {@link Thread#getContextClassLoader()} by default. */ - public Tester() { - this(Thread.currentThread().getContextClassLoader()); - } - - public Tester(ClassLoader classLoader) { - this.classLoader = classLoader; - } - - public void execute(String className) { - Class clss; - try { - clss = classLoader.loadClass(className); - boolean assertionsEnabled = clss.desiredAssertionStatus(); - if (!assertionsEnabled) - throw new IllegalStateException("Test runner " + getClass().getName() - + " requires Java assertions to be enabled. Call the JVM with the -ea argument."); - } catch (Exception e1) { - throw new IllegalArgumentException("Cannot initalise test for " + className, e1); - - } - List methods = findMethods(clss); - if (methods.size() == 0) - throw new IllegalArgumentException("No test method found in " + clss); - // TODO make order more predictable? - for (Method method : methods) { - String uid = method.getDeclaringClass().getName() + "#" + method.getName(); - TesterStatus testStatus = new TesterStatus(uid); - Object obj = null; - try { - beforeTest(uid, method); - obj = clss.getDeclaredConstructor().newInstance(); - method.invoke(obj); - testStatus.setPassed(); - afterTestPassed(uid, method, obj); - } catch (Exception e) { - testStatus.setFailed(e); - afterTestFailed(uid, method, obj, e); - } finally { - results.put(uid, testStatus); - } - } - } - - protected void beforeTest(String uid, Method method) { - // System.out.println(uid + ": STARTING"); - } - - protected void afterTestPassed(String uid, Method method, Object obj) { - System.out.println(uid + ": PASSED"); - } - - protected void afterTestFailed(String uid, Method method, Object obj, Throwable e) { - System.out.println(uid + ": FAILED"); - e.printStackTrace(); - } - - protected List findMethods(Class clss) { - List methods = new ArrayList(); -// Method call = getMethod(clss, "call"); -// if (call != null) -// methods.add(call); -// - for (Method method : clss.getMethods()) { - if (method.getName().startsWith("test")) { - methods.add(method); - } - } - return methods; - } - - protected Method getMethod(Class clss, String name, Class... parameterTypes) { - try { - return clss.getMethod(name, parameterTypes); - } catch (NoSuchMethodException e) { - return null; - } catch (SecurityException e) { - throw new IllegalStateException(e); - } - } - - public static void main(String[] args) { - // deal with arguments - String className; - if (args.length < 1) { - System.err.println(usage()); - System.exit(1); - throw new IllegalArgumentException(); - } else { - className = args[0]; - } - - Tester test = new Tester(); - try { - test.execute(className); - } catch (Throwable e) { - e.printStackTrace(); - } - - Map r = test.results; - for (String uid : r.keySet()) { - TesterStatus testStatus = r.get(uid); - System.out.println(testStatus); - } - } - - public static String usage() { - return "java " + Tester.class.getName() + " [test class name]"; - - } -} diff --git a/org.argeo.util/src/org/argeo/util/TesterStatus.java b/org.argeo.util/src/org/argeo/util/TesterStatus.java deleted file mode 100644 index d1d14ed06..000000000 --- a/org.argeo.util/src/org/argeo/util/TesterStatus.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.argeo.util; - -import java.io.Serializable; - -/** The status of a test. */ -public class TesterStatus implements Serializable { - private static final long serialVersionUID = 6272975746885487000L; - - private Boolean passed = null; - private final String uid; - private Throwable throwable = null; - - public TesterStatus(String uid) { - this.uid = uid; - } - - /** For cloning. */ - public TesterStatus(String uid, Boolean passed, Throwable throwable) { - this(uid); - this.passed = passed; - this.throwable = throwable; - } - - public synchronized Boolean isRunning() { - return passed == null; - } - - public synchronized Boolean isPassed() { - assert passed != null; - return passed; - } - - public synchronized Boolean isFailed() { - assert passed != null; - return !passed; - } - - public synchronized void setPassed() { - setStatus(true); - } - - public synchronized void setFailed() { - setStatus(false); - } - - public synchronized void setFailed(Throwable throwable) { - setStatus(false); - setThrowable(throwable); - } - - protected void setStatus(Boolean passed) { - if (this.passed != null) - throw new IllegalStateException("Passed status of test " + uid + " is already set (to " + passed + ")"); - this.passed = passed; - } - - protected void setThrowable(Throwable throwable) { - if (this.throwable != null) - throw new IllegalStateException("Throwable of test " + uid + " is already set (to " + passed + ")"); - this.throwable = throwable; - } - - public String getUid() { - return uid; - } - - public Throwable getThrowable() { - return throwable; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - // TODO Auto-generated method stub - return super.clone(); - } - - @Override - public boolean equals(Object o) { - if (o instanceof TesterStatus) { - TesterStatus other = (TesterStatus) o; - // we don't check consistency for performance purposes - // this equals() is supposed to be used in collections or for transfer - return other.uid.equals(uid); - } - return false; - } - - @Override - public int hashCode() { - return uid.hashCode(); - } - - @Override - public String toString() { - return uid + "\t" + (passed ? "passed" : "failed"); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/Throughput.java b/org.argeo.util/src/org/argeo/util/Throughput.java deleted file mode 100644 index 266ddbc58..000000000 --- a/org.argeo.util/src/org/argeo/util/Throughput.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.argeo.util; - -import java.text.NumberFormat; -import java.text.ParseException; -import java.util.Locale; - -/** A throughput, that is, a value per unit of time. */ -public class Throughput { - private final static NumberFormat usNumberFormat = NumberFormat.getInstance(Locale.US); - - public enum Unit { - s, m, h, d - } - - private final Double value; - private final Unit unit; - - public Throughput(Double value, Unit unit) { - this.value = value; - this.unit = unit; - } - - public Throughput(Long periodMs, Long count, Unit unit) { - if (unit.equals(Unit.s)) - value = ((double) count * 1000d) / periodMs; - else if (unit.equals(Unit.m)) - value = ((double) count * 60d * 1000d) / periodMs; - else if (unit.equals(Unit.h)) - value = ((double) count * 60d * 60d * 1000d) / periodMs; - else if (unit.equals(Unit.d)) - value = ((double) count * 24d * 60d * 60d * 1000d) / periodMs; - else - throw new IllegalArgumentException("Unsupported unit " + unit); - this.unit = unit; - } - - public Throughput(Double value, String unitStr) { - this(value, Unit.valueOf(unitStr)); - } - - public Throughput(String def) { - int index = def.indexOf('/'); - if (def.length() < 3 || index <= 0 || index != def.length() - 2) - throw new IllegalArgumentException( - def + " no a proper throughput definition" + " (should be /, e.g. 3.54/s or 1500/h"); - String valueStr = def.substring(0, index); - String unitStr = def.substring(index + 1); - try { - this.value = usNumberFormat.parse(valueStr).doubleValue(); - } catch (ParseException e) { - throw new IllegalArgumentException("Cannot parse " + valueStr + " as a number.", e); - } - this.unit = Unit.valueOf(unitStr); - } - - public Long asMsPeriod() { - if (unit.equals(Unit.s)) - return Math.round(1000d / value); - else if (unit.equals(Unit.m)) - return Math.round((60d * 1000d) / value); - else if (unit.equals(Unit.h)) - return Math.round((60d * 60d * 1000d) / value); - else if (unit.equals(Unit.d)) - return Math.round((24d * 60d * 60d * 1000d) / value); - else - throw new IllegalArgumentException("Unsupported unit " + unit); - } - - @Override - public String toString() { - return usNumberFormat.format(value) + '/' + unit; - } - - public Double getValue() { - return value; - } - - public Unit getUnit() { - return unit; - } - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/Directory.java b/org.argeo.util/src/org/argeo/util/directory/Directory.java deleted file mode 100644 index 988658969..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/Directory.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.argeo.util.directory; - -import java.util.Optional; - -import org.argeo.util.transaction.WorkControl; - -/** An information directory (typicylly LDAP). */ -public interface Directory extends HierarchyUnit { - String getName(); - - /** Whether this directory is read only. */ - boolean isReadOnly(); - - /** Whether this directory is disabled. */ - boolean isDisabled(); - - /** The realm (typically Kerberos) of this directory. */ - Optional getRealm(); - - /** Sets the transaction control used by this directory when editing. */ - void setTransactionControl(WorkControl transactionControl); - - /* - * HIERARCHY - */ - - /** The hierarchy unit at this path. */ - HierarchyUnit getHierarchyUnit(String path); - - /** Create a new hierarchy unit. */ - HierarchyUnit createHierarchyUnit(String path); -} diff --git a/org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java b/org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java deleted file mode 100644 index 4450ca474..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java +++ /dev/null @@ -1,246 +0,0 @@ -package org.argeo.util.directory; - -import java.net.InetAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.UnknownHostException; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; - -import org.argeo.util.directory.ldap.IpaUtils; -import org.argeo.util.naming.NamingUtils; - -/** Properties used to configure user admins. */ -public enum DirectoryConf { - /** Base DN (cannot be configured externally) */ - baseDn(null), - - /** URI of the underlying resource (cannot be configured externally) */ - uri(null), - - /** User objectClass */ - userObjectClass("inetOrgPerson"), - - /** Relative base DN for users */ - userBase("ou=People"), - - /** Groups objectClass */ - groupObjectClass("groupOfNames"), - - /** Relative base DN for users */ - groupBase("ou=Groups"), - - /** Relative base DN for users */ - systemRoleBase("ou=Roles"), - - /** Read-only source */ - readOnly(null), - - /** Disabled source */ - disabled(null), - - /** Authentication realm */ - realm(null), - - /** Override all passwords with this value (typically for testing purposes) */ - forcedPassword(null); - - public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config"; - - public final static String SCHEME_LDAP = "ldap"; - public final static String SCHEME_LDAPS = "ldaps"; - public final static String SCHEME_FILE = "file"; - public final static String SCHEME_OS = "os"; - public final static String SCHEME_IPA = "ipa"; - - private final static String SECURITY_PRINCIPAL = "java.naming.security.principal"; - private final static String SECURITY_CREDENTIALS = "java.naming.security.credentials"; - - /** The default value. */ - private Object def; - - DirectoryConf(Object def) { - this.def = def; - } - - public Object getDefault() { - return def; - } - - /** - * For use as Java property. - * - * @deprecated use {@link #name()} instead - */ - @Deprecated - public String property() { - return name(); - } - - public String getValue(Dictionary properties) { - Object res = getRawValue(properties); - if (res == null) - return null; - return res.toString(); - } - - @SuppressWarnings("unchecked") - public T getRawValue(Dictionary properties) { - Object res = properties.get(name()); - if (res == null) - res = getDefault(); - return (T) res; - } - - /** @deprecated use {@link #valueOf(String)} instead */ - @Deprecated - public static DirectoryConf local(String property) { - return DirectoryConf.valueOf(property); - } - - /** Hides host and credentials. */ - public static URI propertiesAsUri(Dictionary properties) { - StringBuilder query = new StringBuilder(); - - boolean first = true; -// for (Enumeration keys = properties.keys(); keys.hasMoreElements();) { -// String key = keys.nextElement(); -// // TODO clarify which keys are relevant (list only the enum?) -// if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn") -// && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name()) -// && !key.equals(uri.name()) && !key.equals(Constants.OBJECTCLASS) -// && !key.equals(Constants.SERVICE_ID) && !key.equals("bundle.id")) { -// if (first) -// first = false; -// else -// query.append('&'); -// query.append(valueOf(key).name()); -// query.append('=').append(properties.get(key).toString()); -// } -// } - - keys: for (DirectoryConf key : DirectoryConf.values()) { - if (key.equals(baseDn) || key.equals(uri)) - continue keys; - Object value = properties.get(key.name()); - if (value == null) - continue keys; - if (first) - first = false; - else - query.append('&'); - query.append(key.name()); - query.append('=').append(value.toString()); - - } - - Object bDnObj = properties.get(baseDn.name()); - String bDn = bDnObj != null ? bDnObj.toString() : null; - try { - return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null, - null); - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Cannot create URI from properties", e); - } - } - - public static Dictionary uriAsProperties(String uriStr) { - try { - Hashtable res = new Hashtable(); - URI u = new URI(uriStr); - String scheme = u.getScheme(); - if (scheme != null && scheme.equals(SCHEME_IPA)) { - return IpaUtils.convertIpaUri(u); -// scheme = u.getScheme(); - } - String path = u.getPath(); - // base DN - String bDn = path.substring(path.lastIndexOf('/') + 1, path.length()); - if (bDn.equals("") && SCHEME_OS.equals(scheme)) { - bDn = getBaseDnFromHostname(); - } - - if (bDn.endsWith(".ldif")) - bDn = bDn.substring(0, bDn.length() - ".ldif".length()); - - // Normalize base DN as LDAP name -// bDn = new LdapName(bDn).toString(); - - String principal = null; - String credentials = null; - if (scheme != null) - if (scheme.equals(SCHEME_LDAP) || scheme.equals(SCHEME_LDAPS)) { - // TODO additional checks - if (u.getUserInfo() != null) { - String[] userInfo = u.getUserInfo().split(":"); - principal = userInfo.length > 0 ? userInfo[0] : null; - credentials = userInfo.length > 1 ? userInfo[1] : null; - } - } else if (scheme.equals(SCHEME_FILE)) { - } else if (scheme.equals(SCHEME_IPA)) { - } else if (scheme.equals(SCHEME_OS)) { - } else - throw new IllegalArgumentException("Unsupported scheme " + scheme); - Map> query = NamingUtils.queryToMap(u); - for (String key : query.keySet()) { - DirectoryConf ldapProp = DirectoryConf.valueOf(key); - List values = query.get(key); - if (values.size() == 1) { - res.put(ldapProp.name(), values.get(0)); - } else { - throw new IllegalArgumentException("Only single values are supported"); - } - } - res.put(baseDn.name(), bDn); - if (SCHEME_OS.equals(scheme)) - res.put(readOnly.name(), "true"); - if (principal != null) - res.put(SECURITY_PRINCIPAL, principal); - if (credentials != null) - res.put(SECURITY_CREDENTIALS, credentials); - if (scheme != null) {// relative URIs are dealt with externally - if (SCHEME_OS.equals(scheme)) { - res.put(uri.name(), SCHEME_OS + ":///"); - } else { - URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(), - scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null); - res.put(uri.name(), bareUri.toString()); - } - } - return res; - } catch (URISyntaxException e) { - throw new IllegalArgumentException("Cannot convert " + uri + " to properties", e); - } - } - - private static String getBaseDnFromHostname() { - String hostname; - try { - hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - hostname = "localhost.localdomain"; - } - int dotIdx = hostname.indexOf('.'); - if (dotIdx >= 0) { - String domain = hostname.substring(dotIdx + 1, hostname.length()); - String bDn = ("." + domain).replaceAll("\\.", ",dc="); - bDn = bDn.substring(1, bDn.length()); - return bDn; - } else { - return "dc=" + hostname; - } - } - - /** - * Hash the base DN in order to have a deterministic string to be used as a cn - * for the underlying user directory. - */ - public static String baseDnHash(Dictionary properties) { - String bDn = (String) properties.get(baseDn.name()); - if (bDn == null) - throw new IllegalStateException("No baseDn in " + properties); - return DirectoryDigestUtils.sha1str(bDn); - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java b/org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java deleted file mode 100644 index d07d2d2ed..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.argeo.util.directory; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.util.Arrays; - -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; - -/** Utilities around digests, mostly those related to passwords. */ -public class DirectoryDigestUtils { - public final static String PASSWORD_SCHEME_SHA = "SHA"; - public final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256"; - - public static byte[] sha1(byte[] bytes) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - digest.update(bytes); - byte[] checksum = digest.digest(); - return checksum; - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Cannot SHA1 digest", e); - } - } - - public static byte[] toPasswordScheme(String passwordScheme, char[] password, byte[] salt, Integer iterations, - Integer keyLength) { - try { - if (PASSWORD_SCHEME_SHA.equals(passwordScheme)) { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - byte[] bytes = charsToBytes(password); - digest.update(bytes); - return digest.digest(); - } else if (PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { - KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength); - - SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - final int ITERATION_LENGTH = 4; - byte[] key = f.generateSecret(spec).getEncoded(); - byte[] result = new byte[ITERATION_LENGTH + salt.length + key.length]; - byte iterationsArr[] = new BigInteger(iterations.toString()).toByteArray(); - if (iterationsArr.length < ITERATION_LENGTH) { - Arrays.fill(result, 0, ITERATION_LENGTH - iterationsArr.length, (byte) 0); - System.arraycopy(iterationsArr, 0, result, ITERATION_LENGTH - iterationsArr.length, - iterationsArr.length); - } else { - System.arraycopy(iterationsArr, 0, result, 0, ITERATION_LENGTH); - } - System.arraycopy(salt, 0, result, ITERATION_LENGTH, salt.length); - System.arraycopy(key, 0, result, ITERATION_LENGTH + salt.length, key.length); - return result; - } else { - throw new UnsupportedOperationException("Unkown password scheme " + passwordScheme); - } - } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { - throw new IllegalStateException("Cannot digest", e); - } - } - - public static char[] bytesToChars(Object obj) { - if (obj instanceof char[]) - return (char[]) obj; - if (!(obj instanceof byte[])) - throw new IllegalArgumentException(obj.getClass() + " is not a byte array"); - ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj); - CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer); - char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit()); - // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data - // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data - // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data - return res; - } - - public static byte[] charsToBytes(char[] chars) { - CharBuffer charBuffer = CharBuffer.wrap(chars); - ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); - byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); - // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data - // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data - return bytes; - } - - public static String sha1str(String str) { - byte[] hash = sha1(str.getBytes(StandardCharsets.UTF_8)); - return encodeHexString(hash); - } - - final private static char[] hexArray = "0123456789abcdef".toCharArray(); - - /** - * From - * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to - * -a-hex-string-in-java - */ - public static String encodeHexString(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - /** singleton */ - private DirectoryDigestUtils() { - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java b/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java deleted file mode 100644 index 947b6bc85..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.argeo.util.directory; - -import java.util.Dictionary; -import java.util.Locale; - -/** A unit within the high-level organisational structure of a directory. */ -public interface HierarchyUnit { - /** Name to use in paths. */ - String getHierarchyUnitName(); - - /** Name to use in UI. */ - String getHierarchyUnitLabel(Locale locale); - - /** - * The parent {@link HierarchyUnit}, or null if a - * {@link Directory}. - */ - HierarchyUnit getParent(); - - /** Direct children {@link HierarchyUnit}s. */ - Iterable getDirectHierarchyUnits(boolean functionalOnly); - - /** - * Whether this is an arbitrary named and placed {@link HierarchyUnit}. - * - * @return true if functional, false is technical - * (e.g. People, Groups, etc.) - */ - boolean isFunctional(); - - /** - * The base of this organisational unit within the hierarchy. This would - * typically be an LDAP base DN. - */ - String getBase(); - - /** The related {@link Directory}. */ - Directory getDirectory(); - - /** Its metadata (typically LDAP attributes). */ - Dictionary getProperties(); -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java deleted file mode 100644 index 5cd4ac1a5..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java +++ /dev/null @@ -1,572 +0,0 @@ -package org.argeo.util.directory.ldap; - -import static org.argeo.util.directory.ldap.LdapNameUtils.toLdapName; - -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Arrays; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.StringJoiner; - -import javax.naming.Context; -import javax.naming.InvalidNameException; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttributes; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; -import javax.transaction.xa.XAResource; - -import org.argeo.osgi.useradmin.OsUserDirectory; -import org.argeo.util.directory.Directory; -import org.argeo.util.directory.DirectoryConf; -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.LdapObjs; -import org.argeo.util.transaction.WorkControl; -import org.argeo.util.transaction.WorkingCopyXaResource; -import org.argeo.util.transaction.XAResourceProvider; - -/** A {@link Directory} based either on LDAP or LDIF. */ -public abstract class AbstractLdapDirectory implements Directory, XAResourceProvider { - protected static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name"; - protected static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password"; - - private final LdapName baseDn; - private final Hashtable configProperties; - private final Rdn userBaseRdn, groupBaseRdn, systemRoleBaseRdn; - private final String userObjectClass, groupObjectClass; - private String memberAttributeId = "member"; - - private final boolean readOnly; - private final boolean disabled; - private final String uri; - - private String forcedPassword; - - private final boolean scoped; - - private List credentialAttributeIds = Arrays - .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() }); - - private WorkControl transactionControl; - private WorkingCopyXaResource xaResource; - - private LdapDirectoryDao directoryDao; - - /** Whether the the directory has is authenticated via a service user. */ - private boolean authenticated = false; - - public AbstractLdapDirectory(URI uriArg, Dictionary props, boolean scoped) { - this.configProperties = new Hashtable(); - for (Enumeration keys = props.keys(); keys.hasMoreElements();) { - String key = keys.nextElement(); - configProperties.put(key, props.get(key)); - } - - String baseDnStr = DirectoryConf.baseDn.getValue(configProperties); - if (baseDnStr == null) - throw new IllegalArgumentException("Base DN must be specified: " + configProperties); - baseDn = toLdapName(baseDnStr); - this.scoped = scoped; - - if (uriArg != null) { - uri = uriArg.toString(); - // uri from properties is ignored - } else { - String uriStr = DirectoryConf.uri.getValue(configProperties); - if (uriStr == null) - uri = null; - else - uri = uriStr; - } - - forcedPassword = DirectoryConf.forcedPassword.getValue(configProperties); - - userObjectClass = DirectoryConf.userObjectClass.getValue(configProperties); - groupObjectClass = DirectoryConf.groupObjectClass.getValue(configProperties); - - String userBase = DirectoryConf.userBase.getValue(configProperties); - String groupBase = DirectoryConf.groupBase.getValue(configProperties); - String systemRoleBase = DirectoryConf.systemRoleBase.getValue(configProperties); - try { -// baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties)); - userBaseRdn = new Rdn(userBase); -// userBaseDn = new LdapName(userBase + "," + baseDn); - groupBaseRdn = new Rdn(groupBase); -// groupBaseDn = new LdapName(groupBase + "," + baseDn); - systemRoleBaseRdn = new Rdn(systemRoleBase); - } catch (InvalidNameException e) { - throw new IllegalArgumentException( - "Badly formated base DN " + DirectoryConf.baseDn.getValue(configProperties), e); - } - - // read only - String readOnlyStr = DirectoryConf.readOnly.getValue(configProperties); - if (readOnlyStr == null) { - readOnly = readOnlyDefault(uri); - configProperties.put(DirectoryConf.readOnly.name(), Boolean.toString(readOnly)); - } else - readOnly = Boolean.parseBoolean(readOnlyStr); - - // disabled - String disabledStr = DirectoryConf.disabled.getValue(configProperties); - if (disabledStr != null) - disabled = Boolean.parseBoolean(disabledStr); - else - disabled = false; - if (!getRealm().isEmpty()) { - // IPA multiple LDAP causes URI parsing to fail - // TODO manage generic redundant LDAP case - directoryDao = new LdapDao(this); - } else { - if (uri != null) { - URI u = URI.create(uri); - if (DirectoryConf.SCHEME_LDAP.equals(u.getScheme()) - || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) { - directoryDao = new LdapDao(this); - authenticated = configProperties.get(Context.SECURITY_PRINCIPAL) != null; - } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) { - directoryDao = new LdifDao(this); - authenticated = true; - } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) { - directoryDao = new OsUserDirectory(this); - authenticated = true; - // singleUser = true; - } else { - throw new IllegalArgumentException("Unsupported scheme " + u.getScheme()); - } - } else { - // in memory - directoryDao = new LdifDao(this); - } - } - if (directoryDao != null) - xaResource = new WorkingCopyXaResource<>(directoryDao); - } - - /* - * INITIALISATION - */ - - public void init() { - getDirectoryDao().init(); - } - - public void destroy() { - getDirectoryDao().destroy(); - } - - /* - * CREATION - */ - protected abstract LdapEntry newUser(LdapName name); - - protected abstract LdapEntry newGroup(LdapName name); - - /* - * EDITION - */ - - public boolean isEditing() { - return xaResource.wc() != null; - } - - public LdapEntryWorkingCopy getWorkingCopy() { - LdapEntryWorkingCopy wc = xaResource.wc(); - if (wc == null) - return null; - return wc; - } - - public void checkEdit() { - if (xaResource.wc() == null) { - try { - transactionControl.getWorkContext().registerXAResource(xaResource, null); - } catch (Exception e) { - throw new IllegalStateException("Cannot enlist " + xaResource, e); - } - } else { - } - } - - public void setTransactionControl(WorkControl transactionControl) { - this.transactionControl = transactionControl; - } - - public XAResource getXaResource() { - return xaResource; - } - - public boolean removeEntry(LdapName dn) { - checkEdit(); - LdapEntryWorkingCopy wc = getWorkingCopy(); - boolean actuallyDeleted; - if (getDirectoryDao().entryExists(dn) || wc.getNewData().containsKey(dn)) { - LdapEntry user = doGetRole(dn); - wc.getDeletedData().put(dn, user); - actuallyDeleted = true; - } else {// just removing from groups (e.g. system roles) - actuallyDeleted = false; - } - for (LdapName groupDn : getDirectoryDao().getDirectGroups(dn)) { - LdapEntry group = doGetRole(groupDn); - group.getAttributes().get(getMemberAttributeId()).remove(dn.toString()); - } - return actuallyDeleted; - } - - /* - * RETRIEVAL - */ - - protected LdapEntry doGetRole(LdapName dn) { - LdapEntryWorkingCopy wc = getWorkingCopy(); - LdapEntry user; - try { - user = getDirectoryDao().doGetEntry(dn); - } catch (NameNotFoundException e) { - user = null; - } - if (wc != null) { - if (user == null && wc.getNewData().containsKey(dn)) - user = wc.getNewData().get(dn); - else if (wc.getDeletedData().containsKey(dn)) - user = null; - } - return user; - } - - protected void collectGroups(LdapEntry user, List allRoles) { - Attributes attrs = user.getAttributes(); - // TODO centralize attribute name - Attribute memberOf = attrs.get(LdapAttrs.memberOf.name()); - // if user belongs to this directory, we only check memberOf - if (memberOf != null && user.getDn().startsWith(getBaseDn())) { - try { - NamingEnumeration values = memberOf.getAll(); - while (values.hasMore()) { - Object value = values.next(); - LdapName groupDn = new LdapName(value.toString()); - LdapEntry group = doGetRole(groupDn); - if (group != null) { - allRoles.add(group); - } else { - // user doesn't have the right to retrieve role, but we know it exists - // otherwise memberOf would not work - group = newGroup(groupDn); - allRoles.add(group); - } - } - } catch (NamingException e) { - throw new IllegalStateException("Cannot get memberOf groups for " + user, e); - } - } else { - directGroups: for (LdapName groupDn : getDirectoryDao().getDirectGroups(user.getDn())) { - LdapEntry group = doGetRole(groupDn); - if (group != null) { - if (allRoles.contains(group)) { - // important in order to avoi loops - continue directGroups; - } - allRoles.add(group); - collectGroups(group, allRoles); - } - } - } - } - - /* - * HIERARCHY - */ - @Override - public HierarchyUnit getHierarchyUnit(String path) { - LdapName dn = pathToName(path); - return directoryDao.doGetHierarchyUnit(dn); - } - - @Override - public Iterable getDirectHierarchyUnits(boolean functionalOnly) { - return directoryDao.doGetDirectHierarchyUnits(baseDn, functionalOnly); - } - - @Override - public String getHierarchyUnitName() { - return getName(); - } - - @Override - public String getHierarchyUnitLabel(Locale locale) { - String key = LdapNameUtils.getLastRdn(getBaseDn()).getType(); - Object value = LdapEntry.getLocalized(asLdapEntry().getProperties(), key, locale); - if (value == null) - value = getHierarchyUnitName(); - assert value != null; - return value.toString(); - } - - @Override - public HierarchyUnit getParent() { - return null; - } - - @Override - public boolean isFunctional() { - return true; - } - - @Override - public Directory getDirectory() { - return this; - } - - @Override - public HierarchyUnit createHierarchyUnit(String path) { - checkEdit(); - LdapEntryWorkingCopy wc = getWorkingCopy(); - LdapName dn = pathToName(path); - if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn)) - || wc.getNewData().containsKey(dn)) - throw new IllegalArgumentException("Already a hierarchy unit " + path); - BasicAttributes attrs = new BasicAttributes(true); - attrs.put(LdapAttrs.objectClass.name(), LdapObjs.organizationalUnit.name()); - Rdn nameRdn = dn.getRdn(dn.size() - 1); - // TODO deal with multiple attr RDN - attrs.put(nameRdn.getType(), nameRdn.getValue()); - wc.getModifiedData().put(dn, attrs); - LdapHierarchyUnit newHierarchyUnit = new LdapHierarchyUnit(this, dn); - wc.getNewData().put(dn, newHierarchyUnit); - return newHierarchyUnit; - } - - /* - * PATHS - */ - - @Override - public String getBase() { - return getBaseDn().toString(); - } - - @Override - public String getName() { - return nameToSimple(getBaseDn(), "."); - } - - protected String nameToRelativePath(LdapName dn) { - LdapName name = LdapNameUtils.relativeName(getBaseDn(), dn); - return nameToSimple(name, "/"); - } - - protected String nameToSimple(LdapName name, String separator) { - StringJoiner path = new StringJoiner(separator); - for (int i = 0; i < name.size(); i++) { - path.add(name.getRdn(i).getValue().toString()); - } - return path.toString(); - - } - - protected LdapName pathToName(String path) { - try { - LdapName name = (LdapName) getBaseDn().clone(); - String[] segments = path.split("/"); - Rdn parentRdn = null; - // segments[0] is the directory itself - for (int i = 0; i < segments.length; i++) { - String segment = segments[i]; - // TODO make attr names configurable ? - String attr = getDirectory().getRealm().isPresent()/* IPA */ ? LdapAttrs.cn.name() - : LdapAttrs.ou.name(); - if (parentRdn != null) { - if (getUserBaseRdn().equals(parentRdn)) - attr = LdapAttrs.uid.name(); - else if (getGroupBaseRdn().equals(parentRdn)) - attr = LdapAttrs.cn.name(); - else if (getSystemRoleBaseRdn().equals(parentRdn)) - attr = LdapAttrs.cn.name(); - } - Rdn rdn = new Rdn(attr, segment); - name.add(rdn); - parentRdn = rdn; - } - return name; - } catch (InvalidNameException e) { - throw new IllegalStateException("Cannot convert " + path + " to LDAP name", e); - } - - } - - /* - * UTILITIES - */ - protected boolean isExternal(LdapName name) { - return !name.startsWith(baseDn); - } - - protected static boolean hasObjectClass(Attributes attrs, LdapObjs objectClass) { - return hasObjectClass(attrs, objectClass.name()); - } - - protected static boolean hasObjectClass(Attributes attrs, String objectClass) { - try { - Attribute attr = attrs.get(LdapAttrs.objectClass.name()); - NamingEnumeration en = attr.getAll(); - while (en.hasMore()) { - String v = en.next().toString(); - if (v.equalsIgnoreCase(objectClass)) - return true; - - } - return false; - } catch (NamingException e) { - throw new IllegalStateException("Cannot search for objectClass " + objectClass, e); - } - } - - private static boolean readOnlyDefault(String uriStr) { - if (uriStr == null) - return true; - /// TODO make it more generic - URI uri; - try { - uri = new URI(uriStr.split(" ")[0]); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - if (uri.getScheme() == null) - return false;// assume relative file to be writable - if (uri.getScheme().equals(DirectoryConf.SCHEME_FILE)) { - File file = new File(uri); - if (file.exists()) - return !file.canWrite(); - else - return !file.getParentFile().canWrite(); - } else if (uri.getScheme().equals(DirectoryConf.SCHEME_LDAP)) { - if (uri.getAuthority() != null)// assume writable if authenticated - return false; - } else if (uri.getScheme().equals(DirectoryConf.SCHEME_OS)) { - return true; - } - return true;// read only by default - } - - /* - * AS AN ENTRY - */ - public LdapEntry asLdapEntry() { - try { - return directoryDao.doGetEntry(baseDn); - } catch (NameNotFoundException e) { - throw new IllegalStateException("Cannot get " + baseDn + " entry", e); - } - } - - public Dictionary getProperties() { - return asLdapEntry().getProperties(); - } - - /* - * ACCESSORS - */ - @Override - public Optional getRealm() { - Object realm = configProperties.get(DirectoryConf.realm.name()); - if (realm == null) - return Optional.empty(); - return Optional.of(realm.toString()); - } - - public LdapName getBaseDn() { - return (LdapName) baseDn.clone(); - } - - public boolean isReadOnly() { - return readOnly; - } - - public boolean isDisabled() { - return disabled; - } - - public boolean isAuthenticated() { - return authenticated; - } - - public Rdn getUserBaseRdn() { - return userBaseRdn; - } - - public Rdn getGroupBaseRdn() { - return groupBaseRdn; - } - - public Rdn getSystemRoleBaseRdn() { - return systemRoleBaseRdn; - } - -// public Dictionary getConfigProperties() { -// return configProperties; -// } - - public Dictionary cloneConfigProperties() { - return new Hashtable<>(configProperties); - } - - public String getForcedPassword() { - return forcedPassword; - } - - public boolean isScoped() { - return scoped; - } - - public List getCredentialAttributeIds() { - return credentialAttributeIds; - } - - public String getUri() { - return uri; - } - - public LdapDirectoryDao getDirectoryDao() { - return directoryDao; - } - - /** dn can be null, in that case a default should be returned. */ - public String getUserObjectClass() { - return userObjectClass; - } - - public String getGroupObjectClass() { - return groupObjectClass; - } - - public String getMemberAttributeId() { - return memberAttributeId; - } - - /* - * OBJECT METHODS - */ - - @Override - public int hashCode() { - return baseDn.hashCode(); - } - - @Override - public String toString() { - return "Directory " + baseDn.toString(); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectoryDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectoryDao.java deleted file mode 100644 index e6d242fe8..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectoryDao.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.argeo.util.directory.ldap; - -import javax.naming.ldap.LdapName; - -/** Base class for LDAP/LDIF directory DAOs. */ -public abstract class AbstractLdapDirectoryDao implements LdapDirectoryDao { - - private AbstractLdapDirectory directory; - - public AbstractLdapDirectoryDao(AbstractLdapDirectory directory) { - this.directory = directory; - } - - public AbstractLdapDirectory getDirectory() { - return directory; - } - - @Override - public LdapEntryWorkingCopy newWorkingCopy() { - return new LdapEntryWorkingCopy(); - } - - @Override - public LdapEntry newUser(LdapName name) { - return getDirectory().newUser(name); - } - - @Override - public LdapEntry newGroup(LdapName name) { - return getDirectory().newGroup(name); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java deleted file mode 100644 index 7b0095fbe..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.util.Dictionary; -import java.util.Enumeration; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; - -public class AttributesDictionary extends Dictionary { - private final Attributes attributes; - - /** The provided attributes is wrapped, not copied. */ - public AttributesDictionary(Attributes attributes) { - if (attributes == null) - throw new IllegalArgumentException("Attributes cannot be null"); - this.attributes = attributes; - } - - @Override - public int size() { - return attributes.size(); - } - - @Override - public boolean isEmpty() { - return attributes.size() == 0; - } - - @Override - public Enumeration keys() { - NamingEnumeration namingEnumeration = attributes.getIDs(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return namingEnumeration.hasMoreElements(); - } - - @Override - public String nextElement() { - return namingEnumeration.nextElement(); - } - - }; - } - - @Override - public Enumeration elements() { - NamingEnumeration namingEnumeration = attributes.getIDs(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return namingEnumeration.hasMoreElements(); - } - - @Override - public Object nextElement() { - String key = namingEnumeration.nextElement(); - return get(key); - } - - }; - } - - @Override - /** @returns a String or String[] */ - public Object get(Object key) { - try { - if (key == null) - throw new IllegalArgumentException("Key cannot be null"); - Attribute attr = attributes.get(key.toString()); - if (attr == null) - return null; - if (attr.size() == 0) - throw new IllegalStateException("There must be at least one value"); - else if (attr.size() == 1) { - return attr.get().toString(); - } else {// multiple - String[] res = new String[attr.size()]; - for (int i = 0; i < attr.size(); i++) { - Object value = attr.get(); - if (value == null) - throw new RuntimeException("Values cannot be null"); - res[i] = attr.get(i).toString(); - } - return res; - } - } catch (NamingException e) { - throw new RuntimeException("Cannot get value for " + key, e); - } - } - - @Override - public Object put(String key, Object value) { - if (key == null) - throw new IllegalArgumentException("Key cannot be null"); - if (value == null) - throw new IllegalArgumentException("Value cannot be null"); - - Object oldValue = get(key); - Attribute attr = attributes.get(key); - if (attr == null) { - attr = new BasicAttribute(key); - attributes.put(attr); - } - - if (value instanceof String[]) { - String[] values = (String[]) value; - // clean additional values - for (int i = values.length; i < attr.size(); i++) - attr.remove(i); - // set values - for (int i = 0; i < values.length; i++) { - attr.set(i, values[i]); - } - } else { - if (attr.size() > 1) - throw new IllegalArgumentException("Attribute " + key + " is multi-valued"); - if (attr.size() == 1) { - try { - if (!attr.get(0).equals(value)) - attr.set(0, value.toString()); - } catch (NamingException e) { - throw new RuntimeException("Cannot check existing value", e); - } - } else { - attr.add(value.toString()); - } - } - return oldValue; - } - - @Override - public Object remove(Object key) { - if (key == null) - throw new IllegalArgumentException("Key cannot be null"); - Object oldValue = get(key); - if (oldValue == null) - return null; - return attributes.remove(key.toString()); - } - - /** - * Copy the content of an {@link Attributes} to the provided - * {@link Dictionary}. - */ - public static void copy(Attributes attributes, Dictionary dictionary) { - AttributesDictionary ad = new AttributesDictionary(attributes); - Enumeration keys = ad.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - dictionary.put(key, ad.get(key)); - } - } - - /** - * Copy a {@link Dictionary} into an {@link Attributes}. - */ - public static void copy(Dictionary dictionary, Attributes attributes) { - AttributesDictionary ad = new AttributesDictionary(attributes); - Enumeration keys = dictionary.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - ad.put(key, dictionary.get(key)); - } - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java deleted file mode 100644 index e10f45756..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.io.IOException; -import java.util.Arrays; -import java.util.StringTokenizer; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.UnsupportedCallbackException; - -import org.argeo.util.naming.LdapAttrs; - -/** LDAP authPassword field according to RFC 3112 */ -public class AuthPassword implements CallbackHandler { - private final String authScheme; - private final String authInfo; - private final String authValue; - - public AuthPassword(String value) { - StringTokenizer st = new StringTokenizer(value, "$"); - // TODO make it more robust, deal with bad formatting - this.authScheme = st.nextToken().trim(); - this.authInfo = st.nextToken().trim(); - this.authValue = st.nextToken().trim(); - - String expectedAuthScheme = getExpectedAuthScheme(); - if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme)) - throw new IllegalArgumentException( - "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme); - } - - protected AuthPassword(String authInfo, String authValue) { - this.authScheme = getExpectedAuthScheme(); - if (authScheme == null) - throw new IllegalArgumentException("Expected auth scheme cannot be null"); - this.authInfo = authInfo; - this.authValue = authValue; - } - - protected AuthPassword(AuthPassword authPassword) { - this.authScheme = authPassword.getAuthScheme(); - this.authInfo = authPassword.getAuthInfo(); - this.authValue = authPassword.getAuthValue(); - } - - protected String getExpectedAuthScheme() { - return null; - } - - protected boolean matchAuthValue(Object object) { - return authValue.equals(object.toString()); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof AuthPassword)) - return false; - AuthPassword authPassword = (AuthPassword) obj; - return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo) - && authValue.equals(authValue); - } - - public boolean keyEquals(AuthPassword authPassword) { - return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo); - } - - @Override - public int hashCode() { - return authValue.hashCode(); - } - - @Override - public String toString() { - return toAuthPassword(); - } - - public final String toAuthPassword() { - return getAuthScheme() + '$' + authInfo + '$' + authValue; - } - - public String getAuthScheme() { - return authScheme; - } - - public String getAuthInfo() { - return authInfo; - } - - public String getAuthValue() { - return authValue; - } - - public static AuthPassword matchAuthValue(Attributes attributes, char[] value) { - try { - Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); - if (authPassword != null) { - NamingEnumeration values = authPassword.getAll(); - while (values.hasMore()) { - Object val = values.next(); - AuthPassword token = new AuthPassword(val.toString()); - String auth; - if (Arrays.binarySearch(value, '$') >= 0) { - auth = token.authInfo + '$' + token.authValue; - } else { - auth = token.authValue; - } - if (Arrays.equals(auth.toCharArray(), value)) - return token; - // if (token.matchAuthValue(value)) - // return token; - } - } - return null; - } catch (NamingException e) { - throw new IllegalStateException("Cannot check attribute", e); - } - } - - public static boolean remove(Attributes attributes, AuthPassword value) { - Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); - return authPassword.remove(value.toAuthPassword()); - } - - @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof NameCallback) - ((NameCallback) callback).setName(toAuthPassword()); - else if (callback instanceof PasswordCallback) - ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray()); - } - } - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java b/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java deleted file mode 100644 index c01d1c3c1..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java +++ /dev/null @@ -1,481 +0,0 @@ -package org.argeo.util.directory.ldap; - -import static java.nio.charset.StandardCharsets.US_ASCII; - -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collections; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.StringJoiner; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.DirectoryDigestUtils; -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.LdapObjs; - -/** An entry in an LDAP (or LDIF) directory. */ -public class DefaultLdapEntry implements LdapEntry { - private final AbstractLdapDirectory directory; - - private final LdapName dn; - - private AttributeDictionary properties; - private AttributeDictionary credentials; - -// private String primaryObjectClass; -// private List objectClasses = new ArrayList<>(); - - protected DefaultLdapEntry(AbstractLdapDirectory directory, LdapName dn) { - Objects.requireNonNull(directory); - Objects.requireNonNull(dn); - this.directory = directory; - this.dn = dn; - - // Object classes -// Objects.requireNonNull(initialAttributes); -// try { -// NamingEnumeration en = initialAttributes.get(LdapAttrs.objectClass.name()).getAll(); -// String first = null; -// attrs: while (en.hasMore()) { -// String v = en.next().toString(); -// if (v.equalsIgnoreCase(LdapObjs.top.name())) -// continue attrs; -// if (first == null) -// first = v; -// if (v.equalsIgnoreCase(getDirectory().getUserObjectClass())) -// primaryObjectClass = getDirectory().getUserObjectClass(); -// else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass())) -// primaryObjectClass = getDirectory().getGroupObjectClass(); -// objectClasses.add(v); -// } -// if (primaryObjectClass == null) { -// if (first == null) -// throw new IllegalStateException("Could not find primary object class"); -// primaryObjectClass = first; -// } -// } catch (NamingException e) { -// throw new IllegalStateException("Cannot find object classes", e); -// } - - } - - @Override - public LdapName getDn() { - return dn; - } - - public synchronized Attributes getAttributes() { - return isEditing() ? getModifiedAttributes() : getDirectory().getDirectoryDao().doGetAttributes(dn); - } - - @Override - public List getReferences(String attributeId) { - Attribute memberAttribute = getAttributes().get(attributeId); - if (memberAttribute == null) - return new ArrayList(); - try { - List roles = new ArrayList(); - NamingEnumeration values = memberAttribute.getAll(); - while (values.hasMore()) { - LdapName dn = new LdapName(values.next().toString()); - roles.add(dn); - } - return roles; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get members", e); - } - - } - - /** Should only be called from working copy thread. */ - protected synchronized Attributes getModifiedAttributes() { - assert getWc() != null; - return getWc().getModifiedData().get(getDn()); - } - - protected synchronized boolean isEditing() { - return getWc() != null && getModifiedAttributes() != null; - } - - private synchronized LdapEntryWorkingCopy getWc() { - return directory.getWorkingCopy(); - } - - protected synchronized void startEditing() { -// if (frozen) -// throw new IllegalStateException("Cannot edit frozen view"); - if (directory.isReadOnly()) - throw new IllegalStateException("User directory is read-only"); - assert getModifiedAttributes() == null; - getWc().startEditing(this); - // modifiedAttributes = (Attributes) publishedAttributes.clone(); - } - - public synchronized void publishAttributes(Attributes modifiedAttributes) { -// publishedAttributes = modifiedAttributes; - } - - /* - * PROPERTIES - */ - @Override - public Dictionary getProperties() { - if (properties == null) - properties = new AttributeDictionary(false); - return properties; - } - - public Dictionary getCredentials() { - if (credentials == null) - credentials = new AttributeDictionary(true); - return credentials; - } - - /* - * CREDENTIALS - */ - @Override - public boolean hasCredential(String key, Object value) { - if (key == null) { - // TODO check other sources (like PKCS12) - // String pwd = new String((char[]) value); - // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112) - char[] password = DirectoryDigestUtils.bytesToChars(value); - - if (getDirectory().getForcedPassword() != null - && getDirectory().getForcedPassword().equals(new String(password))) - return true; - - AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password); - if (authPassword != null) { - if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) { - SharedSecret onceToken = new SharedSecret(authPassword); - if (onceToken.isExpired()) { - // AuthPassword.remove(getAttributes(), onceToken); - return false; - } else { - // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken); - return true; - } - // TODO delete expired tokens? - } else { - // TODO implement SHA - throw new UnsupportedOperationException( - "Unsupported authPassword scheme " + authPassword.getAuthScheme()); - } - } - - // Regular password -// byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256); - if (hasCredential(LdapAttrs.userPassword.name(), DirectoryDigestUtils.charsToBytes(password))) - return true; - return false; - } - - Object storedValue = getCredentials().get(key); - if (storedValue == null || value == null) - return false; - if (!(value instanceof String || value instanceof byte[])) - return false; - if (storedValue instanceof String && value instanceof String) - return storedValue.equals(value); - if (storedValue instanceof byte[] && value instanceof byte[]) { - String storedBase64 = new String((byte[]) storedValue, US_ASCII); - String passwordScheme = null; - if (storedBase64.charAt(0) == '{') { - int index = storedBase64.indexOf('}'); - if (index > 0) { - passwordScheme = storedBase64.substring(1, index); - String storedValueBase64 = storedBase64.substring(index + 1); - byte[] storedValueBytes = Base64.getDecoder().decode(storedValueBase64); - char[] passwordValue = DirectoryDigestUtils.bytesToChars((byte[]) value); - byte[] valueBytes; - if (DirectoryDigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) { - valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, - null); - } else if (DirectoryDigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { - // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/ - byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4); - BigInteger iterations = new BigInteger(iterationsArr); - byte[] salt = Arrays.copyOfRange(storedValueBytes, iterationsArr.length, - iterationsArr.length + 64); - byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length, - storedValueBytes.length); - int keyLengthBits = keyArr.length * 8; - valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt, - iterations.intValue(), keyLengthBits); - } else { - throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme); - } - return Arrays.equals(storedValueBytes, valueBytes); - } - } - } -// if (storedValue instanceof byte[] && value instanceof byte[]) { -// return Arrays.equals((byte[]) storedValue, (byte[]) value); -// } - return false; - } - - /** Hash the password */ - private static byte[] sha1hash(char[] password) { - byte[] hashedPassword = ("{SHA}" + Base64.getEncoder() - .encodeToString(DirectoryDigestUtils.sha1(DirectoryDigestUtils.charsToBytes(password)))) - .getBytes(StandardCharsets.UTF_8); - return hashedPassword; - } - - public AbstractLdapDirectory getDirectory() { - return directory; - } - - public LdapDirectoryDao getDirectoryDao() { - return directory.getDirectoryDao(); - } - - @Override - public int hashCode() { - return dn.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj instanceof LdapEntry) { - LdapEntry that = (LdapEntry) obj; - return this.dn.equals(that.getDn()); - } - return false; - } - - @Override - public String toString() { - return dn.toString(); - } - - private static boolean isAsciiPrintable(String str) { - if (str == null) { - return false; - } - int sz = str.length(); - for (int i = 0; i < sz; i++) { - if (isAsciiPrintable(str.charAt(i)) == false) { - return false; - } - } - return true; - } - - private static boolean isAsciiPrintable(char ch) { - return ch >= 32 && ch < 127; - } - - protected class AttributeDictionary extends Dictionary { - private final List effectiveKeys = new ArrayList(); - private final List attrFilter; - private final Boolean includeFilter; - - public AttributeDictionary(Boolean credentials) { - this.attrFilter = getDirectory().getCredentialAttributeIds(); - this.includeFilter = credentials; - try { - NamingEnumeration ids = getAttributes().getIDs(); - while (ids.hasMore()) { - String id = ids.next(); - if (credentials && attrFilter.contains(id)) - effectiveKeys.add(id); - else if (!credentials && !attrFilter.contains(id)) - effectiveKeys.add(id); - } - } catch (NamingException e) { - throw new IllegalStateException("Cannot initialise attribute dictionary", e); - } - if (!credentials) - effectiveKeys.add(LdapAttrs.objectClasses.name()); - } - - @Override - public int size() { - return effectiveKeys.size(); - } - - @Override - public boolean isEmpty() { - return effectiveKeys.size() == 0; - } - - @Override - public Enumeration keys() { - return Collections.enumeration(effectiveKeys); - } - - @Override - public Enumeration elements() { - final Iterator it = effectiveKeys.iterator(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return it.hasNext(); - } - - @Override - public Object nextElement() { - String key = it.next(); - return get(key); - } - - }; - } - - @Override - public Object get(Object key) { - try { - Attribute attr = !key.equals(LdapAttrs.objectClasses.name()) ? getAttributes().get(key.toString()) - : getAttributes().get(LdapAttrs.objectClass.name()); - if (attr == null) - return null; - Object value = attr.get(); - if (value instanceof byte[]) { - if (key.equals(LdapAttrs.userPassword.name())) - // TODO other cases (certificates, images) - return value; - value = new String((byte[]) value, StandardCharsets.UTF_8); - } - if (attr.size() == 1) - return value; - // special case for object class - if (key.equals(LdapAttrs.objectClass.name())) { - // TODO support multiple object classes - NamingEnumeration en = attr.getAll(); - String first = null; - attrs: while (en.hasMore()) { - String v = en.next().toString(); - if (v.equalsIgnoreCase(LdapObjs.top.name())) - continue attrs; - if (first == null) - first = v; - if (v.equalsIgnoreCase(getDirectory().getUserObjectClass())) - return getDirectory().getUserObjectClass(); - else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass())) - return getDirectory().getGroupObjectClass(); - } - if (first != null) - return first; - throw new IllegalStateException("Cannot find objectClass in " + value); - } else { - NamingEnumeration en = attr.getAll(); - StringJoiner values = new StringJoiner("\n"); - while (en.hasMore()) { - String v = en.next().toString(); - values.add(v); - } - return values.toString(); - } -// else -// return value; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get value for attribute " + key, e); - } - } - - @Override - public Object put(String key, Object value) { - Objects.requireNonNull(value, "Value for key " + key + " is null"); - try { - if (key == null) { - // FIXME remove this "feature", a key should be specified - // TODO persist to other sources (like PKCS12) - char[] password = DirectoryDigestUtils.bytesToChars(value); - byte[] hashedPassword = sha1hash(password); - return put(LdapAttrs.userPassword.name(), hashedPassword); - } - if (key.startsWith("X-")) { - return put(LdapAttrs.authPassword.name(), value); - } - - // start editing - getDirectory().checkEdit(); - if (!isEditing()) - startEditing(); - - // object classes special case. - if (key.equals(LdapAttrs.objectClasses.name())) { - Attribute attribute = new BasicAttribute(LdapAttrs.objectClass.name()); - String[] objectClasses = value.toString().split("\n"); - for (String objectClass : objectClasses) { - if (objectClass.trim().equals("")) - continue; - attribute.add(objectClass); - } - Attribute previousAttribute = getModifiedAttributes().put(attribute); - if (previousAttribute != null) - return previousAttribute.get(); - else - return null; - } - - if (!(value instanceof String || value instanceof byte[])) - throw new IllegalArgumentException("Value must be String or byte[]"); - - if (includeFilter && !attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " not included"); - else if (!includeFilter && attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " excluded"); - - Attribute attribute = getModifiedAttributes().get(key.toString()); - // if (attribute == null) // block unit tests - attribute = new BasicAttribute(key.toString()); - if (value instanceof String && !isAsciiPrintable(((String) value))) - attribute.add(((String) value).getBytes(StandardCharsets.UTF_8)); - else - attribute.add(value); - Attribute previousAttribute = getModifiedAttributes().put(attribute); - if (previousAttribute != null) - return previousAttribute.get(); - else - return null; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get value for attribute " + key, e); - } - } - - @Override - public Object remove(Object key) { - getDirectory().checkEdit(); - if (!isEditing()) - startEditing(); - - if (includeFilter && !attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " not included"); - else if (!includeFilter && attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " excluded"); - - try { - Attribute attr = getModifiedAttributes().remove(key.toString()); - if (attr != null) - return attr.get(); - else - return null; - } catch (NamingException e) { - throw new IllegalStateException("Cannot remove attribute " + key, e); - } - } - - } - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java b/org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java deleted file mode 100644 index 99ad6dbe8..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.List; -import java.util.StringJoiner; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.DirectoryConf; -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.dns.DnsBrowser; - -/** Free IPA specific conventions. */ -public class IpaUtils { - public final static String IPA_USER_BASE = "cn=users"; - public final static String IPA_GROUP_BASE = "cn=groups"; - public final static String IPA_ROLE_BASE = "cn=roles"; - public final static String IPA_SERVICE_BASE = "cn=services"; - - public final static String IPA_ACCOUNTS_BASE = "cn=accounts"; - - private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase(); - - public final static String IPA_USER_DIRECTORY_CONFIG = DirectoryConf.userBase + "=" + IPA_USER_BASE + "&" - + DirectoryConf.groupBase + "=" + IPA_GROUP_BASE + "&" + DirectoryConf.systemRoleBase + "=" + IPA_ROLE_BASE - + "&" + DirectoryConf.readOnly + "=true"; - - @Deprecated - static String domainToUserDirectoryConfigPath(String realm) { - return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + DirectoryConf.realm.name() + "=" + realm; - } - - public static void addIpaConfig(String realm, Dictionary properties) { - properties.put(DirectoryConf.baseDn.name(), domainToBaseDn(realm)); - properties.put(DirectoryConf.realm.name(), realm); - properties.put(DirectoryConf.userBase.name(), IPA_USER_BASE); - properties.put(DirectoryConf.groupBase.name(), IPA_GROUP_BASE); - properties.put(DirectoryConf.systemRoleBase.name(), IPA_ROLE_BASE); - properties.put(DirectoryConf.readOnly.name(), Boolean.TRUE.toString()); - } - - public static String domainToBaseDn(String domain) { - String[] dcs = domain.split("\\."); - StringJoiner sj = new StringJoiner(","); - for (int i = 0; i < dcs.length; i++) { - String dc = dcs[i]; - sj.add(LdapAttrs.dc.name() + '=' + dc.toLowerCase()); - } - return IPA_ACCOUNTS_BASE + ',' + sj.toString(); - } - - public static LdapName kerberosToDn(String kerberosName) { - String[] kname = kerberosName.split("@"); - String username = kname[0]; - String baseDn = domainToBaseDn(kname[1]); - String dn; - if (!username.contains("/")) - dn = LdapAttrs.uid + "=" + username + "," + IPA_USER_BASE + "," + baseDn; - else - dn = KRB_PRINCIPAL_NAME + "=" + kerberosName + "," + IPA_SERVICE_BASE + "," + baseDn; - try { - return new LdapName(dn); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Badly formatted name for " + kerberosName + ": " + dn); - } - } - - private IpaUtils() { - - } - - public static String kerberosDomainFromDns() { - String kerberosDomain; - try (DnsBrowser dnsBrowser = new DnsBrowser()) { - InetAddress localhost = InetAddress.getLocalHost(); - String hostname = localhost.getHostName(); - String dnsZone = hostname.substring(hostname.indexOf('.') + 1); - kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT"); - return kerberosDomain; - } catch (IOException e) { - throw new IllegalStateException("Cannot determine Kerberos domain from DNS", e); - } - - } - - public static Dictionary convertIpaUri(URI uri) { - String path = uri.getPath(); - String kerberosRealm; - if (path == null || path.length() <= 1) { - kerberosRealm = kerberosDomainFromDns(); - } else { - kerberosRealm = path.substring(1); - } - - if (kerberosRealm == null) - throw new IllegalStateException("No Kerberos domain available for " + uri); - // TODO intergrate CA certificate in truststore - // String schemeToUse = SCHEME_LDAPS; - String schemeToUse = DirectoryConf.SCHEME_LDAP; - List ldapHosts; - String ldapHostsStr = uri.getHost(); - if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) { - try (DnsBrowser dnsBrowser = new DnsBrowser()) { - ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase(), - schemeToUse.equals(DirectoryConf.SCHEME_LDAP) ? true : false); - if (ldapHosts == null || ldapHosts.size() == 0) { - throw new IllegalStateException("Cannot configure LDAP for IPA " + uri); - } else { - ldapHostsStr = ldapHosts.get(0); - } - } catch (IOException e) { - throw new IllegalStateException("Cannot convert IPA uri " + uri, e); - } - } else { - ldapHosts = new ArrayList<>(); - ldapHosts.add(ldapHostsStr); - } - - StringBuilder uriStr = new StringBuilder(); - try { - for (String host : ldapHosts) { - URI convertedUri = new URI(schemeToUse + "://" + host + "/"); - uriStr.append(convertedUri).append(' '); - } - } catch (URISyntaxException e) { - throw new IllegalStateException("Cannot convert IPA uri " + uri, e); - } - - Hashtable res = new Hashtable<>(); - res.put(DirectoryConf.uri.name(), uriStr.toString()); - addIpaConfig(kerberosRealm, res); - return res; - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java deleted file mode 100644 index 748efe350..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.util.Dictionary; -import java.util.Hashtable; - -import javax.naming.CommunicationException; -import javax.naming.Context; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.InitialLdapContext; -import javax.naming.ldap.LdapName; - -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.transaction.WorkingCopy; - -/** A synchronized wrapper for a single {@link InitialLdapContext}. */ -// TODO implement multiple contexts and connection pooling. -public class LdapConnection { - private InitialLdapContext initialLdapContext = null; - - public LdapConnection(String url, Dictionary properties) { - try { - Hashtable connEnv = new Hashtable(); - connEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - connEnv.put(Context.PROVIDER_URL, url); - connEnv.put("java.naming.ldap.attributes.binary", LdapAttrs.userPassword.name()); - // use pooling in order to avoid connection timeout -// connEnv.put("com.sun.jndi.ldap.connect.pool", "true"); -// connEnv.put("com.sun.jndi.ldap.connect.pool.timeout", 300000); - - initialLdapContext = new InitialLdapContext(connEnv, null); - // StartTlsResponse tls = (StartTlsResponse) ctx - // .extendedOperation(new StartTlsRequest()); - // tls.negotiate(); - Object securityAuthentication = properties.get(Context.SECURITY_AUTHENTICATION); - if (securityAuthentication != null) - initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, securityAuthentication); - else - initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); - Object principal = properties.get(Context.SECURITY_PRINCIPAL); - if (principal != null) { - initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString()); - Object creds = properties.get(Context.SECURITY_CREDENTIALS); - if (creds != null) { - initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString()); - } - } - } catch (NamingException e) { - throw new IllegalStateException("Cannot connect to LDAP", e); - } - - } - - public void init() { - - } - - public void destroy() { - try { - // tls.close(); - initialLdapContext.close(); - initialLdapContext = null; - } catch (NamingException e) { - e.printStackTrace(); - } - } - - protected InitialLdapContext getLdapContext() { - return initialLdapContext; - } - - protected void reconnect() throws NamingException { - initialLdapContext.reconnect(initialLdapContext.getConnectControls()); - } - - public synchronized NamingEnumeration search(LdapName searchBase, String searchFilter, - SearchControls searchControls) throws NamingException { - NamingEnumeration results; - try { - results = getLdapContext().search(searchBase, searchFilter, searchControls); - } catch (CommunicationException e) { - reconnect(); - results = getLdapContext().search(searchBase, searchFilter, searchControls); - } - return results; - } - - public synchronized Attributes getAttributes(LdapName name) throws NamingException { - try { - return getLdapContext().getAttributes(name); - } catch (CommunicationException e) { - reconnect(); - return getLdapContext().getAttributes(name); - } - } - - public synchronized boolean entryExists(LdapName name) throws NamingException { - String[] noAttrOID = new String[] { "1.1" }; - try { - getLdapContext().getAttributes(name, noAttrOID); - return true; - } catch (CommunicationException e) { - reconnect(); - getLdapContext().getAttributes(name, noAttrOID); - return true; - } catch (NameNotFoundException e) { - return false; - } - } - - public synchronized void prepareChanges(WorkingCopy wc) throws NamingException { - // make sure connection will work - reconnect(); - - // delete - for (LdapName dn : wc.getDeletedData().keySet()) { - if (!entryExists(dn)) - throw new IllegalStateException("User to delete no found " + dn); - } - // add - for (LdapName dn : wc.getNewData().keySet()) { - if (entryExists(dn)) - throw new IllegalStateException("User to create found " + dn); - } - // modify - for (LdapName dn : wc.getModifiedData().keySet()) { - if (!wc.getNewData().containsKey(dn) && !entryExists(dn)) - throw new IllegalStateException("User to modify not found " + dn); - } - - } - -// protected boolean entryExists(LdapName dn) throws NamingException { -// try { -// return getAttributes(dn).size() != 0; -// } catch (NameNotFoundException e) { -// return false; -// } -// } - - public synchronized void commitChanges(LdapEntryWorkingCopy wc) throws NamingException { - // delete - for (LdapName dn : wc.getDeletedData().keySet()) { - getLdapContext().destroySubcontext(dn); - } - // add - for (LdapName dn : wc.getNewData().keySet()) { - LdapEntry user = wc.getNewData().get(dn); - getLdapContext().createSubcontext(dn, user.getAttributes()); - } - // modify - for (LdapName dn : wc.getModifiedData().keySet()) { - Attributes modifiedAttrs = wc.getModifiedData().get(dn); - getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs); - } - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java deleted file mode 100644 index 9157f23a4..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java +++ /dev/null @@ -1,264 +0,0 @@ -package org.argeo.util.directory.ldap; - -import static org.argeo.util.naming.LdapAttrs.objectClass; - -import java.util.ArrayList; -import java.util.List; - -import javax.naming.AuthenticationNotSupportedException; -import javax.naming.Binding; -import javax.naming.InvalidNameException; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttributes; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.LdapObjs; - -/** A user admin based on a LDAP server. */ -public class LdapDao extends AbstractLdapDirectoryDao { - private LdapConnection ldapConnection; - - public LdapDao(AbstractLdapDirectory directory) { - super(directory); - } - - @Override - public void init() { - ldapConnection = new LdapConnection(getDirectory().getUri().toString(), getDirectory().cloneConfigProperties()); - } - - public void destroy() { - ldapConnection.destroy(); - } - - @Override - public boolean checkConnection() { - try { - return ldapConnection.entryExists(getDirectory().getBaseDn()); - } catch (NamingException e) { - return false; - } - } - - @Override - public boolean entryExists(LdapName dn) { - try { - return ldapConnection.entryExists(dn); - } catch (NameNotFoundException e) { - return false; - } catch (NamingException e) { - throw new IllegalStateException("Cannot check " + dn, e); - } - } - - @Override - public LdapEntry doGetEntry(LdapName name) throws NameNotFoundException { -// if (!entryExists(name)) -// throw new NameNotFoundException(name + " was not found in " + getDirectory().getBaseDn()); - try { - Attributes attrs = ldapConnection.getAttributes(name); - - LdapEntry res; - Rdn technicalRdn = LdapNameUtils.getParentRdn(name); - if (getDirectory().getGroupBaseRdn().equals(technicalRdn)) { - if (attrs.size() == 0) {// exists but not accessible - attrs = new BasicAttributes(); - attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name()); - attrs.put(LdapAttrs.objectClass.name(), getDirectory().getGroupObjectClass()); - } - res = newGroup(name); - } else if (getDirectory().getSystemRoleBaseRdn().equals(technicalRdn)) { - if (attrs.size() == 0) {// exists but not accessible - attrs = new BasicAttributes(); - attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name()); - attrs.put(LdapAttrs.objectClass.name(), getDirectory().getGroupObjectClass()); - } - res = newGroup(name); - } else if (getDirectory().getUserBaseRdn().equals(technicalRdn)) { - if (attrs.size() == 0) {// exists but not accessible - attrs = new BasicAttributes(); - attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name()); - attrs.put(LdapAttrs.objectClass.name(), getDirectory().getUserObjectClass()); - } - res = newUser(name); - } else { - res = new DefaultLdapEntry(getDirectory(), name); - } - return res; - } catch (NameNotFoundException e) { - throw e; - } catch (NamingException e) { - throw new IllegalStateException("Cannot retrieve entry " + name, e); - } - } - - @Override - public Attributes doGetAttributes(LdapName name) { - try { - Attributes attrs = ldapConnection.getAttributes(name); - return attrs; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get attributes for " + name); - } - } - - @Override - public List doGetEntries(LdapName searchBase, String f, boolean deep) { - ArrayList res = new ArrayList<>(); - try { - String searchFilter = f != null ? f.toString() - : "(|(" + objectClass.name() + "=" + getDirectory().getUserObjectClass() + ")(" + objectClass.name() - + "=" + getDirectory().getGroupObjectClass() + "))"; - SearchControls searchControls = new SearchControls(); - // only attribute needed is objectClass - searchControls.setReturningAttributes(new String[] { objectClass.name() }); - // FIXME make one level consistent with deep - searchControls.setSearchScope(deep ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE); - - // LdapName searchBase = getBaseDn(); - NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); - - results: while (results.hasMoreElements()) { - SearchResult searchResult = results.next(); - Attributes attrs = searchResult.getAttributes(); - Attribute objectClassAttr = attrs.get(objectClass.name()); - LdapName dn = toDn(searchBase, searchResult); - LdapEntry role; - if (objectClassAttr.contains(getDirectory().getGroupObjectClass()) - || objectClassAttr.contains(getDirectory().getGroupObjectClass().toLowerCase())) - role = newGroup(dn); - else if (objectClassAttr.contains(getDirectory().getUserObjectClass()) - || objectClassAttr.contains(getDirectory().getUserObjectClass().toLowerCase())) - role = newUser(dn); - else { -// log.warn("Unsupported LDAP type for " + searchResult.getName()); - continue results; - } - res.add(role); - } - return res; - } catch (AuthenticationNotSupportedException e) { - // ignore (typically an unsupported anonymous bind) - // TODO better logging - return res; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get roles for filter " + f, e); - } - } - - private LdapName toDn(LdapName baseDn, Binding binding) throws InvalidNameException { - return new LdapName(binding.isRelative() ? binding.getName() + "," + baseDn : binding.getName()); - } - - @Override - public List getDirectGroups(LdapName dn) { - List directGroups = new ArrayList(); - try { - String searchFilter = "(&(" + objectClass + "=" + getDirectory().getGroupObjectClass() + ")(" - + getDirectory().getMemberAttributeId() + "=" + dn + "))"; - - SearchControls searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - LdapName searchBase = getDirectory().getBaseDn(); - NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); - - while (results.hasMoreElements()) { - SearchResult searchResult = (SearchResult) results.nextElement(); - directGroups.add(toDn(searchBase, searchResult)); - } - return directGroups; - } catch (NamingException e) { - throw new IllegalStateException("Cannot populate direct members of " + dn, e); - } - } - - @Override - public void prepare(LdapEntryWorkingCopy wc) { - try { - ldapConnection.prepareChanges(wc); - } catch (NamingException e) { - throw new IllegalStateException("Cannot prepare LDAP", e); - } - } - - @Override - public void commit(LdapEntryWorkingCopy wc) { - try { - ldapConnection.commitChanges(wc); - } catch (NamingException e) { - throw new IllegalStateException("Cannot commit LDAP", e); - } - } - - @Override - public void rollback(LdapEntryWorkingCopy wc) { - // prepare not impacting - } - - /* - * HIERARCHY - */ - - @Override - public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { - List res = new ArrayList<>(); - try { - String structuralFilter = functionalOnly ? "" - : "(" + getDirectory().getUserBaseRdn() + ")(" + getDirectory().getGroupBaseRdn() + ")(" - + getDirectory().getSystemRoleBaseRdn() + ")"; - String searchFilter = "(|(" + objectClass + "=" + LdapObjs.organizationalUnit.name() + ")(" + objectClass - + "=" + LdapObjs.organization.name() + ")" + structuralFilter + ")"; - - SearchControls searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); - // no attributes needed - searchControls.setReturningAttributes(new String[0]); - - NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); - - while (results.hasMoreElements()) { - SearchResult searchResult = (SearchResult) results.nextElement(); - LdapName dn = toDn(searchBase, searchResult); -// Attributes attrs = searchResult.getAttributes(); - LdapHierarchyUnit hierarchyUnit = new LdapHierarchyUnit(getDirectory(), dn); - if (functionalOnly) { - if (hierarchyUnit.isFunctional()) - res.add(hierarchyUnit); - } else { - res.add(hierarchyUnit); - } - } - return res; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get direct hierarchy units ", e); - } - } - - @Override - public HierarchyUnit doGetHierarchyUnit(LdapName dn) { - try { - if (getDirectory().getBaseDn().equals(dn)) - return getDirectory(); - if (!dn.startsWith(getDirectory().getBaseDn())) - throw new IllegalArgumentException(dn + " does not start with base DN " + getDirectory().getBaseDn()); - if (!ldapConnection.entryExists(dn)) - return null; - return new LdapHierarchyUnit(getDirectory(), dn); - } catch (NameNotFoundException e) { - return null; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get hierarchy unit " + dn, e); - } - } - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java deleted file mode 100644 index c70d8c54f..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.util.List; - -import javax.naming.NameNotFoundException; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.transaction.WorkingCopyProcessor; - -/** Low-level access to an LDAP/LDIF directory. */ -public interface LdapDirectoryDao extends WorkingCopyProcessor { - boolean checkConnection(); - - boolean entryExists(LdapName dn); - - LdapEntry doGetEntry(LdapName name) throws NameNotFoundException; - - Attributes doGetAttributes(LdapName name); - - List doGetEntries(LdapName searchBase, String filter, boolean deep); - - List getDirectGroups(LdapName dn); - - Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly); - - HierarchyUnit doGetHierarchyUnit(LdapName dn); - - LdapEntry newUser(LdapName name); - - LdapEntry newGroup(LdapName name); - - void init(); - - void destroy(); -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java deleted file mode 100644 index 4657c8798..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Dictionary; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.StringJoiner; -import java.util.TreeSet; - -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.argeo.util.naming.LdapAttrs; - -/** An LDAP entry. */ -public interface LdapEntry { - LdapName getDn(); - - Attributes getAttributes(); - - void publishAttributes(Attributes modifiedAttributes); - - List getReferences(String attributeId); - - Dictionary getProperties(); - - boolean hasCredential(String key, Object value); - - /* - * UTILITIES - */ - /** - * Convert a collection of object classes to the format expected by an LDAP - * backend. - */ - public static void addObjectClasses(Dictionary properties, Collection objectClasses) { - String value = properties.get(LdapAttrs.objectClasses.name()).toString(); - Set currentObjectClasses = new TreeSet<>(Arrays.asList(value.toString().split("\n"))); - currentObjectClasses.addAll(objectClasses); - StringJoiner values = new StringJoiner("\n"); - currentObjectClasses.forEach((s) -> values.add(s)); - properties.put(LdapAttrs.objectClasses.name(), values.toString()); - } - - public static Object getLocalized(Dictionary properties, String key, Locale locale) { - if (locale == null) - return null; - Object value = null; - value = properties.get(key + ";lang-" + locale.getLanguage() + "-" + locale.getCountry()); - if (value == null) - value = properties.get(key + ";lang-" + locale.getLanguage()); - return value; - } - - public static String toLocalizedKey(String key, Locale locale) { - String country = locale.getCountry(); - if ("".equals(country)) { - return key + ";lang-" + locale.getLanguage(); - } else { - return key + ";lang-" + locale.getLanguage() + "-" + locale.getCountry(); - } - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java deleted file mode 100644 index 381c11b2f..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.argeo.util.directory.ldap; - -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.argeo.util.transaction.AbstractWorkingCopy; - -/** Working copy for a user directory being edited. */ -public class LdapEntryWorkingCopy extends AbstractWorkingCopy { - @Override - protected LdapName getId(LdapEntry entry) { - return entry.getDn(); - } - - @Override - protected Attributes cloneAttributes(LdapEntry entry) { - return (Attributes) entry.getAttributes().clone(); - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java deleted file mode 100644 index 961f2e358..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.util.Locale; - -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -import org.argeo.util.directory.HierarchyUnit; - -/** LDIF/LDAP based implementation of {@link HierarchyUnit}. */ -public class LdapHierarchyUnit extends DefaultLdapEntry implements HierarchyUnit { - private final boolean functional; - - public LdapHierarchyUnit(AbstractLdapDirectory directory, LdapName dn) { - super(directory, dn); - - Rdn rdn = LdapNameUtils.getLastRdn(dn); - functional = !(directory.getUserBaseRdn().equals(rdn) || directory.getGroupBaseRdn().equals(rdn) - || directory.getSystemRoleBaseRdn().equals(rdn)); - } - - @Override - public HierarchyUnit getParent() { - return getDirectoryDao().doGetHierarchyUnit(LdapNameUtils.getParent(getDn())); - } - - @Override - public Iterable getDirectHierarchyUnits(boolean functionalOnly) { - return getDirectoryDao().doGetDirectHierarchyUnits(getDn(), functionalOnly); - } - - @Override - public boolean isFunctional() { - return functional; - } - - @Override - public String getHierarchyUnitName() { - String name = LdapNameUtils.getLastRdnValue(getDn()); - // TODO check ou, o, etc. - return name; - } - - @Override - public String getHierarchyUnitLabel(Locale locale) { - String key = LdapNameUtils.getLastRdn(getDn()).getType(); - Object value = LdapEntry.getLocalized(getProperties(), key, locale); - if (value == null) - value = getHierarchyUnitName(); - assert value != null; - return value.toString(); - } - - @Override - public String getBase() { - return getDn().toString(); - } - - @Override - public String toString() { - return "Hierarchy Unit " + getDn().toString(); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java deleted file mode 100644 index 88d317542..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.argeo.util.directory.ldap; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -/** Utilities to simplify using {@link LdapName}. */ -public class LdapNameUtils { - - public static LdapName relativeName(LdapName prefix, LdapName dn) { - try { - if (!dn.startsWith(prefix)) - throw new IllegalArgumentException("Prefix " + prefix + " not consistent with " + dn); - LdapName res = (LdapName) dn.clone(); - for (int i = 0; i < prefix.size(); i++) { - res.remove(0); - } - return res; - } catch (InvalidNameException e) { - throw new IllegalStateException("Cannot find realtive name", e); - } - } - - public static LdapName getParent(LdapName dn) { - try { - LdapName parent = (LdapName) dn.clone(); - parent.remove(parent.size() - 1); - return parent; - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Cannot get parent of " + dn, e); - } - } - - public static Rdn getParentRdn(LdapName dn) { - if (dn.size() < 2) - throw new IllegalArgumentException(dn + " has no parent"); - Rdn parentRdn = dn.getRdn(dn.size() - 2); - return parentRdn; - } - - public static LdapName toLdapName(String distinguishedName) { - try { - return new LdapName(distinguishedName); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Cannot parse " + distinguishedName + " as LDAP name", e); - } - } - - public static Rdn getLastRdn(LdapName dn) { - return dn.getRdn(dn.size() - 1); - } - - public static String getLastRdnAsString(LdapName dn) { - return getLastRdn(dn).toString(); - } - - public static String getLastRdnValue(String dn) { - return getLastRdnValue(toLdapName(dn)); - } - - public static String getLastRdnValue(LdapName dn) { - return getLastRdn(dn).getValue().toString(); - } - - /** singleton */ - private LdapNameUtils() { - - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java deleted file mode 100644 index c200faa27..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java +++ /dev/null @@ -1,306 +0,0 @@ -package org.argeo.util.directory.ldap; - -import static org.argeo.util.naming.LdapAttrs.objectClass; -import static org.argeo.util.naming.LdapObjs.inetOrgPerson; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.NavigableMap; -import java.util.Objects; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; - -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.argeo.util.directory.HierarchyUnit; -import org.argeo.util.naming.LdapObjs; -import org.osgi.framework.Filter; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.service.useradmin.Role; - -/** A user admin based on a LDIF files. */ -public class LdifDao extends AbstractLdapDirectoryDao { - private NavigableMap entries = new TreeMap<>(); - private NavigableMap hierarchy = new TreeMap<>(); - - private NavigableMap values = new TreeMap<>(); - - public LdifDao(AbstractLdapDirectory directory) { - super(directory); - } - - public void init() { - String uri = getDirectory().getUri(); - if (uri == null) - return; - try { - URI u = new URI(uri); - if (u.getScheme().equals("file")) { - File file = new File(u); - if (!file.exists()) - return; - } - load(u.toURL().openStream()); - } catch (IOException | URISyntaxException e) { - throw new IllegalStateException("Cannot open URL " + getDirectory().getUri(), e); - } - } - - public void save() { - if (getDirectory().getUri() == null) - return; // ignore - if (getDirectory().isReadOnly()) - throw new IllegalStateException( - "Cannot save LDIF user admin: " + getDirectory().getUri() + " is read-only"); - try (FileOutputStream out = new FileOutputStream(new File(new URI(getDirectory().getUri())))) { - save(out); - } catch (IOException | URISyntaxException e) { - throw new IllegalStateException("Cannot save user admin to " + getDirectory().getUri(), e); - } - } - - public void save(OutputStream out) throws IOException { - try { - LdifWriter ldifWriter = new LdifWriter(out); - for (LdapName name : hierarchy.keySet()) - ldifWriter.writeEntry(name, hierarchy.get(name).getAttributes()); - for (LdapName name : entries.keySet()) - ldifWriter.writeEntry(name, entries.get(name).getAttributes()); - } finally { - out.close(); - } - } - - public void load(InputStream in) { - try { - entries.clear(); - hierarchy.clear(); - - LdifParser ldifParser = new LdifParser(); - SortedMap allEntries = ldifParser.read(in); - for (LdapName key : allEntries.keySet()) { - Attributes attributes = allEntries.get(key); - // check for inconsistency - Set lowerCase = new HashSet(); - NamingEnumeration ids = attributes.getIDs(); - while (ids.hasMoreElements()) { - String id = ids.nextElement().toLowerCase(); - if (lowerCase.contains(id)) - throw new IllegalStateException(key + " has duplicate id " + id); - lowerCase.add(id); - } - - values.put(key, attributes); - - // analyse object classes - NamingEnumeration objectClasses = attributes.get(objectClass.name()).getAll(); - // System.out.println(key); - objectClasses: while (objectClasses.hasMore()) { - String objectClass = objectClasses.next().toString(); - // System.out.println(" " + objectClass); - if (objectClass.toLowerCase().equals(inetOrgPerson.name().toLowerCase())) { - entries.put(key, newUser(key)); - break objectClasses; - } else if (objectClass.toLowerCase().equals(getDirectory().getGroupObjectClass().toLowerCase())) { - entries.put(key, newGroup(key)); - break objectClasses; - } else if (objectClass.equalsIgnoreCase(LdapObjs.organizationalUnit.name())) { - // TODO skip if it does not contain groups or users - hierarchy.put(key, new LdapHierarchyUnit(getDirectory(), key)); - break objectClasses; - } - } - } - - } catch (NamingException | IOException e) { - throw new IllegalStateException("Cannot load user admin service from LDIF", e); - } - } - - public void destroy() { -// if (users == null || groups == null) - if (entries == null) - throw new IllegalStateException("User directory " + getDirectory().getBaseDn() + " is already destroyed"); -// users = null; -// groups = null; - entries = null; - } - - /* - * USER ADMIN - */ - - @Override - public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException { - if (entries.containsKey(key)) - return entries.get(key); - throw new NameNotFoundException(key + " not persisted"); - } - - @Override - public Attributes doGetAttributes(LdapName name) { - if (!values.containsKey(name)) - throw new IllegalStateException(name + " doe not exist in " + getDirectory().getBaseDn()); - return values.get(name); - } - - @Override - public boolean checkConnection() { - return true; - } - - @Override - public boolean entryExists(LdapName dn) { - return entries.containsKey(dn);// || groups.containsKey(dn); - } - - @Override - public List doGetEntries(LdapName searchBase, String f, boolean deep) { - Objects.requireNonNull(searchBase); - ArrayList res = new ArrayList<>(); - if (f == null && deep && getDirectory().getBaseDn().equals(searchBase)) { - res.addAll(entries.values()); - } else { - filterRoles(entries, searchBase, f, deep, res); - } - return res; - } - - private void filterRoles(SortedMap map, LdapName searchBase, String f, boolean deep, - List res) { - // FIXME get rid of OSGi references - try { - // TODO reduce map with search base ? - Filter filter = f != null ? FrameworkUtil.createFilter(f) : null; - roles: for (LdapEntry user : map.values()) { - LdapName dn = user.getDn(); - if (dn.startsWith(searchBase)) { - if (!deep && dn.size() != (searchBase.size() + 1)) - continue roles; - if (filter == null) - res.add(user); - else { - if (user instanceof Role) { - if (filter.match(((Role) user).getProperties())) - res.add(user); - } - } - } - } - } catch (InvalidSyntaxException e) { - throw new IllegalArgumentException("Cannot create filter " + f, e); - } - - } - - @Override - public List getDirectGroups(LdapName dn) { - List directGroups = new ArrayList(); - entries: for (LdapName name : entries.keySet()) { - LdapEntry group; - try { - LdapEntry entry = doGetEntry(name); - if (AbstractLdapDirectory.hasObjectClass(entry.getAttributes(), getDirectory().getGroupObjectClass())) { - group = entry; - } else { - continue entries; - } - } catch (NameNotFoundException e) { - throw new IllegalArgumentException("Group " + dn + " not found", e); - } - if (group.getReferences(getDirectory().getMemberAttributeId()).contains(dn)) { - directGroups.add(group.getDn()); - } - } - return directGroups; - } - - @Override - public void prepare(LdapEntryWorkingCopy wc) { - // delete - for (LdapName dn : wc.getDeletedData().keySet()) { - if (entries.containsKey(dn)) - entries.remove(dn); - else - throw new IllegalStateException("User to delete not found " + dn); - } - // add - for (LdapName dn : wc.getNewData().keySet()) { - LdapEntry user = (LdapEntry) wc.getNewData().get(dn); - if (entries.containsKey(dn)) - throw new IllegalStateException("User to create found " + dn); - entries.put(dn, user); - } - // modify - for (LdapName dn : wc.getModifiedData().keySet()) { - Attributes modifiedAttrs = wc.getModifiedData().get(dn); - LdapEntry user; - try { - user = doGetEntry(dn); - } catch (NameNotFoundException e) { - throw new IllegalStateException("User to modify no found " + dn, e); - } - if (user == null) - throw new IllegalStateException("User to modify no found " + dn); - user.publishAttributes(modifiedAttrs); - } - } - - @Override - public void commit(LdapEntryWorkingCopy wc) { - save(); - } - - @Override - public void rollback(LdapEntryWorkingCopy wc) { - init(); - } - - /* - * HIERARCHY - */ - @Override - public HierarchyUnit doGetHierarchyUnit(LdapName dn) { - if (getDirectory().getBaseDn().equals(dn)) - return getDirectory(); - return hierarchy.get(dn); - } - - @Override - public Iterable doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) { - List res = new ArrayList<>(); - for (LdapName n : hierarchy.keySet()) { - if (n.size() == searchBase.size() + 1) { - if (n.startsWith(searchBase)) { - HierarchyUnit hu = hierarchy.get(n); - if (functionalOnly) { - if (hu.isFunctional()) - res.add(hu); - } else { - res.add(hu); - } - } - } - } - return res; - } - - public void scope(LdifDao scoped) { - scoped.entries = Collections.unmodifiableNavigableMap(entries); - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java deleted file mode 100644 index 0022943e1..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.SortedMap; -import java.util.TreeMap; - -import javax.naming.InvalidNameException; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -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 org.argeo.util.naming.LdapAttrs; - -/** Basic LDIF parser. */ -public class LdifParser { - private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - protected Attributes addAttributes(SortedMap res, int lineNumber, LdapName currentDn, - Attributes currentAttributes) { - try { - Rdn nameRdn = currentDn.getRdn(currentDn.size() - 1); - Attribute nameAttr = currentAttributes.get(nameRdn.getType()); - if (nameAttr == null) - currentAttributes.put(nameRdn.getType(), nameRdn.getValue()); - else if (!nameAttr.get().equals(nameRdn.getValue())) - throw new IllegalStateException( - "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + currentDn - + " (shortly before line " + lineNumber + " in LDIF file)"); - Attributes previous = res.put(currentDn, currentAttributes); - return previous; - } catch (NamingException e) { - throw new IllegalStateException("Cannot add " + currentDn, e); - } - } - - /** With UTF-8 charset */ - public SortedMap read(InputStream in) throws IOException { - try (Reader reader = new InputStreamReader(in, DEFAULT_CHARSET)) { - return read(reader); - } finally { - try { - in.close(); - } catch (IOException e) { - // silent - } - } - } - - /** Will close the reader. */ - public SortedMap read(Reader reader) throws IOException { - SortedMap res = new TreeMap(); - try { - List lines = new ArrayList<>(); - try (BufferedReader br = new BufferedReader(reader)) { - String line; - while ((line = br.readLine()) != null) { - lines.add(line); - } - } - if (lines.size() == 0) - return res; - // add an empty new line since the last line is not checked - if (!lines.get(lines.size() - 1).equals("")) - lines.add(""); - - LdapName currentDn = null; - Attributes currentAttributes = null; - StringBuilder currentEntry = new StringBuilder(); - - readLines: for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { - String line = lines.get(lineNumber); - boolean isLastLine = false; - if (lineNumber == lines.size() - 1) - isLastLine = true; - if (line.startsWith(" ")) { - currentEntry.append(line.substring(1)); - if (!isLastLine) - continue readLines; - } - - if (currentEntry.length() != 0 || isLastLine) { - // read previous attribute - StringBuilder attrId = new StringBuilder(8); - boolean isBase64 = false; - readAttrId: for (int i = 0; i < currentEntry.length(); i++) { - char c = currentEntry.charAt(i); - if (c == ':') { - if (i + 1 < currentEntry.length() && currentEntry.charAt(i + 1) == ':') - isBase64 = true; - currentEntry.delete(0, i + (isBase64 ? 2 : 1)); - break readAttrId; - } else { - attrId.append(c); - } - } - - String attributeId = attrId.toString(); - // TODO should we really trim the end of the string as well? - String cleanValueStr = currentEntry.toString().trim(); - Object attributeValue = isBase64 ? Base64.getDecoder().decode(cleanValueStr) : cleanValueStr; - - // manage DN attributes - if (attributeId.equals(LdapAttrs.DN) || isLastLine) { - if (currentDn != null) { - // - // ADD - // - Attributes previous = addAttributes(res, lineNumber, currentDn, currentAttributes); - if (previous != null) { -// log.warn("There was already an entry with DN " + currentDn -// + ", which has been discarded by a subsequent one."); - } - } - - if (attributeId.equals(LdapAttrs.DN)) - try { - currentDn = new LdapName(attributeValue.toString()); - currentAttributes = new BasicAttributes(true); - } catch (InvalidNameException e) { -// log.error(attributeValue + " not a valid DN, skipping the entry."); - currentDn = null; - currentAttributes = null; - } - } - - // store attribute - if (currentAttributes != null) { - Attribute attribute = currentAttributes.get(attributeId); - if (attribute == null) { - attribute = new BasicAttribute(attributeId); - currentAttributes.put(attribute); - } - attribute.add(attributeValue); - } - currentEntry = new StringBuilder(); - } - currentEntry.append(line); - } - } finally { - try { - reader.close(); - } catch (IOException e) { - // silent - } - } - return res; - } -} \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java deleted file mode 100644 index a10f16938..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.argeo.util.directory.ldap; - -import static org.argeo.util.naming.LdapAttrs.DN; -import static org.argeo.util.naming.LdapAttrs.member; -import static org.argeo.util.naming.LdapAttrs.objectClass; -import static org.argeo.util.naming.LdapAttrs.uniqueMember; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Map; -import java.util.SortedSet; -import java.util.TreeSet; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -/** Basic LDIF writer */ -public class LdifWriter { - private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - private final Writer writer; - - /** Writer must be closed by caller */ - public LdifWriter(Writer writer) { - this.writer = writer; - } - - /** Stream must be closed by caller */ - public LdifWriter(OutputStream out) { - this(new OutputStreamWriter(out, DEFAULT_CHARSET)); - } - - public void writeEntry(LdapName name, Attributes attributes) throws IOException { - try { - // check consistency - Rdn nameRdn = name.getRdn(name.size() - 1); - Attribute nameAttr = attributes.get(nameRdn.getType()); - if (!nameAttr.get().equals(nameRdn.getValue())) - throw new IllegalArgumentException( - "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + name); - - writer.append(DN + ": ").append(name.toString()).append('\n'); - Attribute objectClassAttr = attributes.get(objectClass.name()); - if (objectClassAttr != null) - writeAttribute(objectClassAttr); - attributes: for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { - Attribute attribute = attrs.next(); - if (attribute.getID().equals(DN) || attribute.getID().equals(objectClass.name())) - continue attributes;// skip DN attribute - if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) - continue attributes;// skip member and uniqueMember attributes, so that they are always written last - writeAttribute(attribute); - } - // write member and uniqueMember attributes last - for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { - Attribute attribute = attrs.next(); - if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) - writeMemberAttribute(attribute); - } - writer.append('\n'); - writer.flush(); - } catch (NamingException e) { - throw new IllegalStateException("Cannot write LDIF", e); - } - } - - public void write(Map entries) throws IOException { - for (LdapName dn : entries.keySet()) - writeEntry(dn, entries.get(dn)); - } - - protected void writeAttribute(Attribute attribute) throws NamingException, IOException { - for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { - Object value = attrValues.next(); - if (value instanceof byte[]) { - String encoded = Base64.getEncoder().encodeToString((byte[]) value); - writer.append(attribute.getID()).append(":: ").append(encoded).append('\n'); - } else { - writer.append(attribute.getID()).append(": ").append(value.toString()).append('\n'); - } - } - } - - protected void writeMemberAttribute(Attribute attribute) throws NamingException, IOException { - // Note: duplicate entries will be swallowed - SortedSet values = new TreeSet<>(); - for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { - String value = attrValues.next().toString(); - values.add(value); - } - - for (String value : values) { - writer.append(attribute.getID()).append(": ").append(value).append('\n'); - } - } -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/SharedSecret.java b/org.argeo.util/src/org/argeo/util/directory/ldap/SharedSecret.java deleted file mode 100644 index eaab167e8..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/SharedSecret.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; - -import org.argeo.util.naming.NamingUtils; - -public class SharedSecret extends AuthPassword { - public final static String X_SHARED_SECRET = "X-SharedSecret"; - private final Instant expiry; - - public SharedSecret(String authInfo, String authValue) { - super(authInfo, authValue); - expiry = null; - } - - public SharedSecret(AuthPassword authPassword) { - super(authPassword); - String authInfo = getAuthInfo(); - if (authInfo.length() == 16) { - expiry = NamingUtils.ldapDateToInstant(authInfo); - } else { - expiry = null; - } - } - - public SharedSecret(ZonedDateTime expiryTimestamp, String value) { - super(NamingUtils.instantToLdapDate(expiryTimestamp), value); - expiry = expiryTimestamp.withZoneSameInstant(ZoneOffset.UTC).toInstant(); - } - - public SharedSecret(int hours, String value) { - this(ZonedDateTime.now().plusHours(hours), value); - } - - @Override - protected String getExpectedAuthScheme() { - return X_SHARED_SECRET; - } - - public boolean isExpired() { - if (expiry == null) - return false; - return expiry.isBefore(Instant.now()); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/http/HttpHeader.java b/org.argeo.util/src/org/argeo/util/http/HttpHeader.java deleted file mode 100644 index 74cf94c03..000000000 --- a/org.argeo.util/src/org/argeo/util/http/HttpHeader.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.argeo.util.http; - -/** Standard HTTP headers (including WebDav). */ -public enum HttpHeader { - AUTHORIZATION("Authorization"), // - WWW_AUTHENTICATE("WWW-Authenticate"), // - ALLOW("Allow"), // - - // WebDav - DAV("DAV"), // - DEPTH("Depth"), // - ; - - public final static String BASIC = "Basic"; - public final static String REALM = "realm"; - public final static String NEGOTIATE = "Negotiate"; - - private final String name; - - private HttpHeader(String headerName) { - this.name = headerName; - } - - public String getHeaderName() { - return name; - } - - @Override - public String toString() { - return getHeaderName(); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/http/HttpMethod.java b/org.argeo.util/src/org/argeo/util/http/HttpMethod.java deleted file mode 100644 index 27b4d8f19..000000000 --- a/org.argeo.util/src/org/argeo/util/http/HttpMethod.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.argeo.util.http; - -/** Generic HTTP methods. */ -public enum HttpMethod { - OPTIONS, // - HEAD, // - GET, // - POST, // - PUT, // - DELETE, // - - // WebDav - PROPFIND, // - PROPPATCH, // - MKCOL, // - MOVE, // - COPY, // - ; -} diff --git a/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java b/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java deleted file mode 100644 index 9127d2c21..000000000 --- a/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.argeo.util.http; - -import java.net.URI; -import java.util.Objects; - -import com.sun.net.httpserver.HttpContext; -import com.sun.net.httpserver.HttpExchange; - -public class HttpServerUtils { - private final static String SLASH = "/"; - - private static String extractPathWithingContext(HttpContext httpContext, String fullPath, boolean startWithSlash) { - Objects.requireNonNull(fullPath); - String contextPath = httpContext.getPath(); - if (!fullPath.startsWith(contextPath)) - throw new IllegalArgumentException(fullPath + " does not belong to context" + contextPath); - String path = fullPath.substring(contextPath.length()); - // TODO optimise? - if (!startWithSlash && path.startsWith(SLASH)) { - path = path.substring(1); - } else if (startWithSlash && !path.startsWith(SLASH)) { - path = SLASH + path; - } - return path; - } - - /** Path within the context, NOT starting with a slash. */ - public static String relativize(HttpExchange exchange) { - URI uri = exchange.getRequestURI(); - HttpContext httpContext = exchange.getHttpContext(); - return extractPathWithingContext(httpContext, uri.getPath(), false); - } - - /** Path within the context, starting with a slash. */ - public static String subPath(HttpExchange exchange) { - URI uri = exchange.getRequestURI(); - HttpContext httpContext = exchange.getHttpContext(); - return extractPathWithingContext(httpContext, uri.getPath(), true); - } - - /** singleton */ - private HttpServerUtils() { - - } -} diff --git a/org.argeo.util/src/org/argeo/util/http/HttpStatus.java b/org.argeo.util/src/org/argeo/util/http/HttpStatus.java deleted file mode 100644 index 11e0a3645..000000000 --- a/org.argeo.util/src/org/argeo/util/http/HttpStatus.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.argeo.util.http; - -/** - * Standard HTTP response status codes (including WebDav ones). - * - * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status" - */ -public enum HttpStatus { - // Successful responses (200–299) - OK(200, "OK"), // - NO_CONTENT(204, "No Content"), // - MULTI_STATUS(207, "Multi-Status"), // WebDav - // Client error responses (400–499) - UNAUTHORIZED(401, "Unauthorized"), // - FORBIDDEN(403, "Forbidden"), // - NOT_FOUND(404, "Not Found"), // - // Server error responses (500-599) - INTERNAL_SERVER_ERROR(500, "Internal Server Error"), // - NOT_IMPLEMENTED(501, "Not Implemented"), // - ; - - private final int code; - private final String reasonPhrase; - - HttpStatus(int statusCode, String reasonPhrase) { - this.code = statusCode; - this.reasonPhrase = reasonPhrase; - } - - public int getCode() { - return code; - } - - public String getReasonPhrase() { - return reasonPhrase; - } - - /** - * The status line, as defined by RFC2616. - * - * @see "https://www.rfc-editor.org/rfc/rfc2616#section-6.1" - */ - public String getStatusLine(String httpVersion) { - return httpVersion + " " + code + " " + reasonPhrase; - } - - public static HttpStatus parseStatusLine(String statusLine) { - try { - String[] arr = statusLine.split(" "); - int code = Integer.parseInt(arr[1]); - for (HttpStatus status : values()) { - if (status.getCode() == code) - return status; - } - } catch (Exception e) { - throw new IllegalArgumentException("Invalid status line: " + statusLine, e); - } - throw new IllegalArgumentException("Unkown status code: " + statusLine); - } - - @Override - public String toString() { - return code + " " + reasonPhrase; - } - -} diff --git a/org.argeo.util/src/org/argeo/util/internal/DisplayQName.java b/org.argeo.util/src/org/argeo/util/internal/DisplayQName.java deleted file mode 100644 index 6cc39dc6a..000000000 --- a/org.argeo.util/src/org/argeo/util/internal/DisplayQName.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.argeo.util.internal; - -import javax.xml.namespace.QName; - -public class DisplayQName extends QName { - private static final long serialVersionUID = 2376484886212253123L; - - public DisplayQName(String namespaceURI, String localPart, String prefix) { - super(namespaceURI, localPart, prefix); - } - - public DisplayQName(String localPart) { - super(localPart); - } - - @Override - public String toString() { - String prefix = getPrefix(); - assert prefix != null; - return "".equals(prefix) ? getLocalPart() : prefix + ":" + getLocalPart(); - } - - } \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/naming/Distinguished.java b/org.argeo.util/src/org/argeo/util/naming/Distinguished.java deleted file mode 100644 index e339edeef..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/Distinguished.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.argeo.util.naming; - -import java.util.EnumSet; -import java.util.Set; -import java.util.TreeSet; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -/** - * An object that can be identified with an X.500 distinguished name. - * - * @see "https://tools.ietf.org/html/rfc1779" - */ -public interface Distinguished { - /** The related distinguished name. */ - String dn(); - - /** The related distinguished name as an {@link LdapName}. */ - default LdapName distinguishedName() { - try { - return new LdapName(dn()); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Distinguished name " + dn() + " is not properly formatted.", e); - } - } - - /** List all DNs of an enumeration as strings. */ - static Set enumToDns(EnumSet enumSet) { - Set res = new TreeSet<>(); - for (Enum enm : enumSet) { - res.add(((Distinguished) enm).dn()); - } - return res; - } -} diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.csv b/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.csv deleted file mode 100644 index 676d72720..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.csv +++ /dev/null @@ -1,129 +0,0 @@ -uid,,,0.9.2342.19200300.100.1.1,,RFC 4519 -mail,,,0.9.2342.19200300.100.1.3,,RFC 4524 -info,,,0.9.2342.19200300.100.1.4,,RFC 4524 -drink,,,0.9.2342.19200300.100.1.5,,RFC 4524 -roomNumber,,,0.9.2342.19200300.100.1.6,,RFC 4524 -photo,,,0.9.2342.19200300.100.1.7,,RFC 2798 -userClass,,,0.9.2342.19200300.100.1.8,,RFC 4524 -host,,,0.9.2342.19200300.100.1.9,,RFC 4524 -manager,,,0.9.2342.19200300.100.1.10,,RFC 4524 -documentIdentifier,,,0.9.2342.19200300.100.1.11,,RFC 4524 -documentTitle,,,0.9.2342.19200300.100.1.12,,RFC 4524 -documentVersion,,,0.9.2342.19200300.100.1.13,,RFC 4524 -documentAuthor,,,0.9.2342.19200300.100.1.14,,RFC 4524 -documentLocation,,,0.9.2342.19200300.100.1.15,,RFC 4524 -homePhone,,,0.9.2342.19200300.100.1.20,,RFC 4524 -secretary,,,0.9.2342.19200300.100.1.21,,RFC 4524 -dc,,,0.9.2342.19200300.100.1.25,,RFC 4519 -associatedDomain,,,0.9.2342.19200300.100.1.37,,RFC 4524 -associatedName,,,0.9.2342.19200300.100.1.38,,RFC 4524 -homePostalAddress,,,0.9.2342.19200300.100.1.39,,RFC 4524 -personalTitle,,,0.9.2342.19200300.100.1.40,,RFC 4524 -mobile,,,0.9.2342.19200300.100.1.41,,RFC 4524 -pager,,,0.9.2342.19200300.100.1.42,,RFC 4524 -co,,,0.9.2342.19200300.100.1.43,,RFC 4524 -uniqueIdentifier,,,0.9.2342.19200300.100.1.44,,RFC 4524 -organizationalStatus,,,0.9.2342.19200300.100.1.45,,RFC 4524 -buildingName,,,0.9.2342.19200300.100.1.48,,RFC 4524 -audio,,,0.9.2342.19200300.100.1.55,,RFC 2798 -documentPublisher,,,0.9.2342.19200300.100.1.56,,RFC 4524 -jpegPhoto,,,0.9.2342.19200300.100.1.60,,RFC 2798 -vendorName,,,1.3.6.1.1.4,,RFC 3045 -vendorVersion,,,1.3.6.1.1.5,,RFC 3045 -entryUUID,,,1.3.6.1.1.16.4,,RFC 4530 -entryDN,,,1.3.6.1.1.20,,RFC 5020 -labeledURI,,,1.3.6.1.4.1.250.1.57,,RFC 2798 -numSubordinates,,,1.3.6.1.4.1.453.16.2.103,,draft-ietf-boreham-numsubordinates -namingContexts,,,1.3.6.1.4.1.1466.101.120.5,,RFC 4512 -altServer,,,1.3.6.1.4.1.1466.101.120.6,,RFC 4512 -supportedExtension,,,1.3.6.1.4.1.1466.101.120.7,,RFC 4512 -supportedControl,,,1.3.6.1.4.1.1466.101.120.13,,RFC 4512 -supportedSASLMechanisms,,,1.3.6.1.4.1.1466.101.120.14,,RFC 4512 -supportedLDAPVersion,,,1.3.6.1.4.1.1466.101.120.15,,RFC 4512 -ldapSyntaxes,,,1.3.6.1.4.1.1466.101.120.16,,RFC 4512 -supportedAuthPasswordSchemes,,,1.3.6.1.4.1.4203.1.3.3,,RFC 3112 -authPassword,,,1.3.6.1.4.1.4203.1.3.4,,RFC 3112 -supportedFeatures,,,1.3.6.1.4.1.4203.1.3.5,,RFC 4512 -inheritable,,,1.3.6.1.4.1.7628.5.4.1,,draft-ietf-ldup-subentry -blockInheritance,,,1.3.6.1.4.1.7628.5.4.2,,draft-ietf-ldup-subentry -objectClass,,,2.5.4.0,,RFC 4512 -aliasedObjectName,,,2.5.4.1,,RFC 4512 -cn,,,2.5.4.3,,RFC 4519 -sn,,,2.5.4.4,,RFC 4519 -serialNumber,,,2.5.4.5,,RFC 4519 -c,,,2.5.4.6,,RFC 4519 -l,,,2.5.4.7,,RFC 4519 -st,,,2.5.4.8,,RFC 4519 -street,,,2.5.4.9,,RFC 4519 -o,,,2.5.4.10,,RFC 4519 -ou,,,2.5.4.11,,RFC 4519 -title,,,2.5.4.12,,RFC 4519 -description,,,2.5.4.13,,RFC 4519 -searchGuide,,,2.5.4.14,,RFC 4519 -businessCategory,,,2.5.4.15,,RFC 4519 -postalAddress,,,2.5.4.16,,RFC 4519 -postalCode,,,2.5.4.17,,RFC 4519 -postOfficeBox,,,2.5.4.18,,RFC 4519 -physicalDeliveryOfficeName,,,2.5.4.19,,RFC 4519 -telephoneNumber,,,2.5.4.20,,RFC 4519 -telexNumber,,,2.5.4.21,,RFC 4519 -teletexTerminalIdentifier,,,2.5.4.22,,RFC 4519 -facsimileTelephoneNumber,,,2.5.4.23,,RFC 4519 -x121Address,,,2.5.4.24,,RFC 4519 -internationalISDNNumber,,,2.5.4.25,,RFC 4519 -registeredAddress,,,2.5.4.26,,RFC 4519 -destinationIndicator,,,2.5.4.27,,RFC 4519 -preferredDeliveryMethod,,,2.5.4.28,,RFC 4519 -member,,,2.5.4.31,,RFC 4519 -owner,,,2.5.4.32,,RFC 4519 -roleOccupant,,,2.5.4.33,,RFC 4519 -seeAlso,,,2.5.4.34,,RFC 4519 -userPassword,,,2.5.4.35,,RFC 4519 -userCertificate,,,2.5.4.36,,RFC 4523 -cACertificate,,,2.5.4.37,,RFC 4523 -authorityRevocationList,,,2.5.4.38,,RFC 4523 -certificateRevocationList,,,2.5.4.39,,RFC 4523 -crossCertificatePair,,,2.5.4.40,,RFC 4523 -name,,,2.5.4.41,,RFC 4519 -givenName,,,2.5.4.42,,RFC 4519 -initials,,,2.5.4.43,,RFC 4519 -generationQualifier,,,2.5.4.44,,RFC 4519 -x500UniqueIdentifier,,,2.5.4.45,,RFC 4519 -dnQualifier,,,2.5.4.46,,RFC 4519 -enhancedSearchGuide,,,2.5.4.47,,RFC 4519 -distinguishedName,,,2.5.4.49,,RFC 4519 -uniqueMember,,,2.5.4.50,,RFC 4519 -houseIdentifier,,,2.5.4.51,,RFC 4519 -supportedAlgorithms,,,2.5.4.52,,RFC 4523 -deltaRevocationList,,,2.5.4.53,,RFC 4523 -createTimestamp,,,2.5.18.1,,RFC 4512 -modifyTimestamp,,,2.5.18.2,,RFC 4512 -creatorsName,,,2.5.18.3,,RFC 4512 -modifiersName,,,2.5.18.4,,RFC 4512 -subschemaSubentry,,,2.5.18.10,,RFC 4512 -dITStructureRules,,,2.5.21.1,,RFC 4512 -dITContentRules,,,2.5.21.2,,RFC 4512 -matchingRules,,,2.5.21.4,,RFC 4512 -attributeTypes,,,2.5.21.5,,RFC 4512 -objectClasses,,,2.5.21.6,,RFC 4512 -nameForms,,,2.5.21.7,,RFC 4512 -matchingRuleUse,,,2.5.21.8,,RFC 4512 -structuralObjectClass,,,2.5.21.9,,RFC 4512 -governingStructureRule,,,2.5.21.10,,RFC 4512 -carLicense,,,2.16.840.1.113730.3.1.1,,RFC 2798 -departmentNumber,,,2.16.840.1.113730.3.1.2,,RFC 2798 -employeeNumber,,,2.16.840.1.113730.3.1.3,,RFC 2798 -employeeType,,,2.16.840.1.113730.3.1.4,,RFC 2798 -changeNumber,,,2.16.840.1.113730.3.1.5,,draft-good-ldap-changelog -targetDN,,,2.16.840.1.113730.3.1.6,,draft-good-ldap-changelog -changeType,,,2.16.840.1.113730.3.1.7,,draft-good-ldap-changelog -changes,,,2.16.840.1.113730.3.1.8,,draft-good-ldap-changelog -newRDN,,,2.16.840.1.113730.3.1.9,,draft-good-ldap-changelog -deleteOldRDN,,,2.16.840.1.113730.3.1.10,,draft-good-ldap-changelog -newSuperior,,,2.16.840.1.113730.3.1.11,,draft-good-ldap-changelog -ref,,,2.16.840.1.113730.3.1.34,,RFC 3296 -changelog,,,2.16.840.1.113730.3.1.35,,draft-good-ldap-changelog -preferredLanguage,,,2.16.840.1.113730.3.1.39,,RFC 2798 -userSMIMECertificate,,,2.16.840.1.113730.3.1.40,,RFC 2798 -userPKCS12,,,2.16.840.1.113730.3.1.216,,RFC 2798 -displayName,,,2.16.840.1.113730.3.1.241,,RFC 2798 diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java b/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java deleted file mode 100644 index 1a6642f05..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java +++ /dev/null @@ -1,356 +0,0 @@ -package org.argeo.util.naming; - -import java.util.function.Supplier; - -import javax.xml.namespace.QName; - -import org.argeo.util.internal.DisplayQName; - -/** - * Standard LDAP attributes as per:
- * - Standard LDAP
- * - Kerberos - * LDAP (partial) - */ -public enum LdapAttrs implements SpecifiedName, Supplier { - /** */ - uid("0.9.2342.19200300.100.1.1", "RFC 4519"), - /** */ - mail("0.9.2342.19200300.100.1.3", "RFC 4524"), - /** */ - info("0.9.2342.19200300.100.1.4", "RFC 4524"), - /** */ - drink("0.9.2342.19200300.100.1.5", "RFC 4524"), - /** */ - roomNumber("0.9.2342.19200300.100.1.6", "RFC 4524"), - /** */ - photo("0.9.2342.19200300.100.1.7", "RFC 2798"), - /** */ - userClass("0.9.2342.19200300.100.1.8", "RFC 4524"), - /** */ - host("0.9.2342.19200300.100.1.9", "RFC 4524"), - /** */ - manager("0.9.2342.19200300.100.1.10", "RFC 4524"), - /** */ - documentIdentifier("0.9.2342.19200300.100.1.11", "RFC 4524"), - /** */ - documentTitle("0.9.2342.19200300.100.1.12", "RFC 4524"), - /** */ - documentVersion("0.9.2342.19200300.100.1.13", "RFC 4524"), - /** */ - documentAuthor("0.9.2342.19200300.100.1.14", "RFC 4524"), - /** */ - documentLocation("0.9.2342.19200300.100.1.15", "RFC 4524"), - /** */ - homePhone("0.9.2342.19200300.100.1.20", "RFC 4524"), - /** */ - secretary("0.9.2342.19200300.100.1.21", "RFC 4524"), - /** */ - dc("0.9.2342.19200300.100.1.25", "RFC 4519"), - /** */ - associatedDomain("0.9.2342.19200300.100.1.37", "RFC 4524"), - /** */ - associatedName("0.9.2342.19200300.100.1.38", "RFC 4524"), - /** */ - homePostalAddress("0.9.2342.19200300.100.1.39", "RFC 4524"), - /** */ - personalTitle("0.9.2342.19200300.100.1.40", "RFC 4524"), - /** */ - mobile("0.9.2342.19200300.100.1.41", "RFC 4524"), - /** */ - pager("0.9.2342.19200300.100.1.42", "RFC 4524"), - /** */ - co("0.9.2342.19200300.100.1.43", "RFC 4524"), - /** */ - uniqueIdentifier("0.9.2342.19200300.100.1.44", "RFC 4524"), - /** */ - organizationalStatus("0.9.2342.19200300.100.1.45", "RFC 4524"), - /** */ - buildingName("0.9.2342.19200300.100.1.48", "RFC 4524"), - /** */ - audio("0.9.2342.19200300.100.1.55", "RFC 2798"), - /** */ - documentPublisher("0.9.2342.19200300.100.1.56", "RFC 4524"), - /** */ - jpegPhoto("0.9.2342.19200300.100.1.60", "RFC 2798"), - /** */ - vendorName("1.3.6.1.1.4", "RFC 3045"), - /** */ - vendorVersion("1.3.6.1.1.5", "RFC 3045"), - /** */ - entryUUID("1.3.6.1.1.16.4", "RFC 4530"), - /** */ - entryDN("1.3.6.1.1.20", "RFC 5020"), - /** */ - labeledURI("1.3.6.1.4.1.250.1.57", "RFC 2798"), - /** */ - numSubordinates("1.3.6.1.4.1.453.16.2.103", "draft-ietf-boreham-numsubordinates"), - /** */ - namingContexts("1.3.6.1.4.1.1466.101.120.5", "RFC 4512"), - /** */ - altServer("1.3.6.1.4.1.1466.101.120.6", "RFC 4512"), - /** */ - supportedExtension("1.3.6.1.4.1.1466.101.120.7", "RFC 4512"), - /** */ - supportedControl("1.3.6.1.4.1.1466.101.120.13", "RFC 4512"), - /** */ - supportedSASLMechanisms("1.3.6.1.4.1.1466.101.120.14", "RFC 4512"), - /** */ - supportedLDAPVersion("1.3.6.1.4.1.1466.101.120.15", "RFC 4512"), - /** */ - ldapSyntaxes("1.3.6.1.4.1.1466.101.120.16", "RFC 4512"), - /** */ - supportedAuthPasswordSchemes("1.3.6.1.4.1.4203.1.3.3", "RFC 3112"), - /** */ - authPassword("1.3.6.1.4.1.4203.1.3.4", "RFC 3112"), - /** */ - supportedFeatures("1.3.6.1.4.1.4203.1.3.5", "RFC 4512"), - /** */ - inheritable("1.3.6.1.4.1.7628.5.4.1", "draft-ietf-ldup-subentry"), - /** */ - blockInheritance("1.3.6.1.4.1.7628.5.4.2", "draft-ietf-ldup-subentry"), - /** */ - objectClass("2.5.4.0", "RFC 4512"), - /** */ - aliasedObjectName("2.5.4.1", "RFC 4512"), - /** */ - cn("2.5.4.3", "RFC 4519"), - /** */ - sn("2.5.4.4", "RFC 4519"), - /** */ - serialNumber("2.5.4.5", "RFC 4519"), - /** */ - c("2.5.4.6", "RFC 4519"), - /** */ - l("2.5.4.7", "RFC 4519"), - /** */ - st("2.5.4.8", "RFC 4519"), - /** */ - street("2.5.4.9", "RFC 4519"), - /** */ - o("2.5.4.10", "RFC 4519"), - /** */ - ou("2.5.4.11", "RFC 4519"), - /** */ - title("2.5.4.12", "RFC 4519"), - /** */ - description("2.5.4.13", "RFC 4519"), - /** */ - searchGuide("2.5.4.14", "RFC 4519"), - /** */ - businessCategory("2.5.4.15", "RFC 4519"), - /** */ - postalAddress("2.5.4.16", "RFC 4519"), - /** */ - postalCode("2.5.4.17", "RFC 4519"), - /** */ - postOfficeBox("2.5.4.18", "RFC 4519"), - /** */ - physicalDeliveryOfficeName("2.5.4.19", "RFC 4519"), - /** */ - telephoneNumber("2.5.4.20", "RFC 4519"), - /** */ - telexNumber("2.5.4.21", "RFC 4519"), - /** */ - teletexTerminalIdentifier("2.5.4.22", "RFC 4519"), - /** */ - facsimileTelephoneNumber("2.5.4.23", "RFC 4519"), - /** */ - x121Address("2.5.4.24", "RFC 4519"), - /** */ - internationalISDNNumber("2.5.4.25", "RFC 4519"), - /** */ - registeredAddress("2.5.4.26", "RFC 4519"), - /** */ - destinationIndicator("2.5.4.27", "RFC 4519"), - /** */ - preferredDeliveryMethod("2.5.4.28", "RFC 4519"), - /** */ - member("2.5.4.31", "RFC 4519"), - /** */ - owner("2.5.4.32", "RFC 4519"), - /** */ - roleOccupant("2.5.4.33", "RFC 4519"), - /** */ - seeAlso("2.5.4.34", "RFC 4519"), - /** */ - userPassword("2.5.4.35", "RFC 4519"), - /** */ - userCertificate("2.5.4.36", "RFC 4523"), - /** */ - cACertificate("2.5.4.37", "RFC 4523"), - /** */ - authorityRevocationList("2.5.4.38", "RFC 4523"), - /** */ - certificateRevocationList("2.5.4.39", "RFC 4523"), - /** */ - crossCertificatePair("2.5.4.40", "RFC 4523"), - /** */ - name("2.5.4.41", "RFC 4519"), - /** */ - givenName("2.5.4.42", "RFC 4519"), - /** */ - initials("2.5.4.43", "RFC 4519"), - /** */ - generationQualifier("2.5.4.44", "RFC 4519"), - /** */ - x500UniqueIdentifier("2.5.4.45", "RFC 4519"), - /** */ - dnQualifier("2.5.4.46", "RFC 4519"), - /** */ - enhancedSearchGuide("2.5.4.47", "RFC 4519"), - /** */ - distinguishedName("2.5.4.49", "RFC 4519"), - /** */ - uniqueMember("2.5.4.50", "RFC 4519"), - /** */ - houseIdentifier("2.5.4.51", "RFC 4519"), - /** */ - supportedAlgorithms("2.5.4.52", "RFC 4523"), - /** */ - deltaRevocationList("2.5.4.53", "RFC 4523"), - /** */ - createTimestamp("2.5.18.1", "RFC 4512"), - /** */ - modifyTimestamp("2.5.18.2", "RFC 4512"), - /** */ - creatorsName("2.5.18.3", "RFC 4512"), - /** */ - modifiersName("2.5.18.4", "RFC 4512"), - /** */ - subschemaSubentry("2.5.18.10", "RFC 4512"), - /** */ - dITStructureRules("2.5.21.1", "RFC 4512"), - /** */ - dITContentRules("2.5.21.2", "RFC 4512"), - /** */ - matchingRules("2.5.21.4", "RFC 4512"), - /** */ - attributeTypes("2.5.21.5", "RFC 4512"), - /** */ - objectClasses("2.5.21.6", "RFC 4512"), - /** */ - nameForms("2.5.21.7", "RFC 4512"), - /** */ - matchingRuleUse("2.5.21.8", "RFC 4512"), - /** */ - structuralObjectClass("2.5.21.9", "RFC 4512"), - /** */ - governingStructureRule("2.5.21.10", "RFC 4512"), - /** */ - carLicense("2.16.840.1.113730.3.1.1", "RFC 2798"), - /** */ - departmentNumber("2.16.840.1.113730.3.1.2", "RFC 2798"), - /** */ - employeeNumber("2.16.840.1.113730.3.1.3", "RFC 2798"), - /** */ - employeeType("2.16.840.1.113730.3.1.4", "RFC 2798"), - /** */ - changeNumber("2.16.840.1.113730.3.1.5", "draft-good-ldap-changelog"), - /** */ - targetDN("2.16.840.1.113730.3.1.6", "draft-good-ldap-changelog"), - /** */ - changeType("2.16.840.1.113730.3.1.7", "draft-good-ldap-changelog"), - /** */ - changes("2.16.840.1.113730.3.1.8", "draft-good-ldap-changelog"), - /** */ - newRDN("2.16.840.1.113730.3.1.9", "draft-good-ldap-changelog"), - /** */ - deleteOldRDN("2.16.840.1.113730.3.1.10", "draft-good-ldap-changelog"), - /** */ - newSuperior("2.16.840.1.113730.3.1.11", "draft-good-ldap-changelog"), - /** */ - ref("2.16.840.1.113730.3.1.34", "RFC 3296"), - /** */ - changelog("2.16.840.1.113730.3.1.35", "draft-good-ldap-changelog"), - /** */ - preferredLanguage("2.16.840.1.113730.3.1.39", "RFC 2798"), - /** */ - userSMIMECertificate("2.16.840.1.113730.3.1.40", "RFC 2798"), - /** */ - userPKCS12("2.16.840.1.113730.3.1.216", "RFC 2798"), - /** */ - displayName("2.16.840.1.113730.3.1.241", "RFC 2798"), - - // Sun memberOf - memberOf("1.2.840.113556.1.2.102", "389 DS memberOf"), - - // KERBEROS (partial) - krbPrincipalName("2.16.840.1.113719.1.301.6.8.1", "Novell Kerberos Schema Definitions"), - - // RFC 2985 and RFC 3039 (partial) - dateOfBirth("1.3.6.1.5.5.7.9.1", "RFC 2985"), - /** */ - placeOfBirth("1.3.6.1.5.5.7.9.2", "RFC 2985"), - /** */ - gender("1.3.6.1.5.5.7.9.3", "RFC 2985"), - /** */ - countryOfCitizenship("1.3.6.1.5.5.7.9.4", "RFC 2985"), - /** */ - countryOfResidence("1.3.6.1.5.5.7.9.5", "RFC 2985"), - - // RFC 2307bis (partial) - /** */ - uidNumber("1.3.6.1.1.1.1.0", "RFC 2307bis"), - /** */ - gidNumber("1.3.6.1.1.1.1.1", "RFC 2307bis"), - /** */ - homeDirectory("1.3.6.1.1.1.1.3", "RFC 2307bis"), - /** */ - loginShell("1.3.6.1.1.1.1.4", "RFC 2307bis"), - /** */ - memberUid("1.3.6.1.1.1.1.12", "RFC 2307bis"), - - // - ; - - public final static String DN = "dn"; - -// private final static String LDAP_ = "ldap:"; - - private final String oid, spec; - private final QName value; - - LdapAttrs(String oid, String spec) { - this.oid = oid; - this.spec = spec; - this.value = new DisplayQName(LdapObjs.LDAP_NAMESPACE_URI, name(), LdapObjs.LDAP_DEFAULT_PREFIX); - } - - public QName qName() { - return value; - } - - @Override - public String getID() { - return oid; - } - - @Override - public String getSpec() { - return spec; - } - - @Deprecated - public String property() { - return get(); - } - - @Deprecated - public String qualified() { - return get(); - } - - @Override - public String get() { - return LdapObjs.LDAP_DEFAULT_PREFIX + ":" + name(); - } - - @Override - public final String toString() { - // must return the name - return name(); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapObjs.csv b/org.argeo.util/src/org/argeo/util/naming/LdapObjs.csv deleted file mode 100644 index 3d907cbeb..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/LdapObjs.csv +++ /dev/null @@ -1,42 +0,0 @@ -account,,,0.9.2342.19200300.100.4.5,,RFC 4524 -document,,,0.9.2342.19200300.100.4.6,,RFC 4524 -room,,,0.9.2342.19200300.100.4.7,,RFC 4524 -documentSeries,,,0.9.2342.19200300.100.4.9,,RFC 4524 -domain,,,0.9.2342.19200300.100.4.13,,RFC 4524 -rFC822localPart,,,0.9.2342.19200300.100.4.14,,RFC 4524 -domainRelatedObject,,,0.9.2342.19200300.100.4.17,,RFC 4524 -friendlyCountry,,,0.9.2342.19200300.100.4.18,,RFC 4524 -simpleSecurityObject,,,0.9.2342.19200300.100.4.19,,RFC 4524 -uidObject,,,1.3.6.1.1.3.1,,RFC 4519 -extensibleObject,,,1.3.6.1.4.1.1466.101.120.111,,RFC 4512 -dcObject,,,1.3.6.1.4.1.1466.344,,RFC 4519 -authPasswordObject,,,1.3.6.1.4.1.4203.1.4.7,,RFC 3112 -namedObject,,,1.3.6.1.4.1.5322.13.1.1,,draft-howard-namedobject -inheritableLDAPSubEntry,,,1.3.6.1.4.1.7628.5.6.1.1,,draft-ietf-ldup-subentry -top,,,2.5.6.0,,RFC 4512 -alias,,,2.5.6.1,,RFC 4512 -country,,,2.5.6.2,,RFC 4519 -locality,,,2.5.6.3,,RFC 4519 -organization,,,2.5.6.4,,RFC 4519 -organizationalUnit,,,2.5.6.5,,RFC 4519 -person,,,2.5.6.6,,RFC 4519 -organizationalPerson,,,2.5.6.7,,RFC 4519 -organizationalRole,,,2.5.6.8,,RFC 4519 -groupOfNames,,,2.5.6.9,,RFC 4519 -residentialPerson,,,2.5.6.10,,RFC 4519 -applicationProcess,,,2.5.6.11,,RFC 4519 -device,,,2.5.6.14,,RFC 4519 -strongAuthenticationUser,,,2.5.6.15,,RFC 4523 -certificationAuthority,,,2.5.6.16,,RFC 4523 -certificationAuthority-V2,,,2.5.6.16.2,,RFC 4523 -groupOfUniqueNames,,,2.5.6.17,,RFC 4519 -userSecurityInformation,,,2.5.6.18,,RFC 4523 -cRLDistributionPoint,,,2.5.6.19,,RFC 4523 -pkiUser,,,2.5.6.21,,RFC 4523 -pkiCA,,,2.5.6.22,,RFC 4523 -deltaCRL,,,2.5.6.23,,RFC 4523 -subschema,,,2.5.20.1,,RFC 4512 -ldapSubEntry,,,2.16.840.1.113719.2.142.6.1.1,,draft-ietf-ldup-subentry -changeLogEntry,,,2.16.840.1.113730.3.2.1,,draft-good-ldap-changelog -inetOrgPerson,,,2.16.840.1.113730.3.2.2,,RFC 2798 -referral,,,2.16.840.1.113730.3.2.6,,RFC 3296 diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapObjs.java b/org.argeo.util/src/org/argeo/util/naming/LdapObjs.java deleted file mode 100644 index 995c68cc9..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/LdapObjs.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.argeo.util.naming; - -import java.util.function.Supplier; - -import javax.xml.namespace.QName; - -import org.argeo.util.internal.DisplayQName; - -/** - * Standard LDAP object classes as per - * https://www.ldap.com/ldap- - * oid-reference - */ -public enum LdapObjs implements SpecifiedName, Supplier { - account("0.9.2342.19200300.100.4.5", "RFC 4524"), - /** */ - document("0.9.2342.19200300.100.4.6", "RFC 4524"), - /** */ - room("0.9.2342.19200300.100.4.7", "RFC 4524"), - /** */ - documentSeries("0.9.2342.19200300.100.4.9", "RFC 4524"), - /** */ - domain("0.9.2342.19200300.100.4.13", "RFC 4524"), - /** */ - rFC822localPart("0.9.2342.19200300.100.4.14", "RFC 4524"), - /** */ - domainRelatedObject("0.9.2342.19200300.100.4.17", "RFC 4524"), - /** */ - friendlyCountry("0.9.2342.19200300.100.4.18", "RFC 4524"), - /** */ - simpleSecurityObject("0.9.2342.19200300.100.4.19", "RFC 4524"), - /** */ - uidObject("1.3.6.1.1.3.1", "RFC 4519"), - /** */ - extensibleObject("1.3.6.1.4.1.1466.101.120.111", "RFC 4512"), - /** */ - dcObject("1.3.6.1.4.1.1466.344", "RFC 4519"), - /** */ - authPasswordObject("1.3.6.1.4.1.4203.1.4.7", "RFC 3112"), - /** */ - namedObject("1.3.6.1.4.1.5322.13.1.1", "draft-howard-namedobject"), - /** */ - inheritableLDAPSubEntry("1.3.6.1.4.1.7628.5.6.1.1", "draft-ietf-ldup-subentry"), - /** */ - top("2.5.6.0", "RFC 4512"), - /** */ - alias("2.5.6.1", "RFC 4512"), - /** */ - country("2.5.6.2", "RFC 4519"), - /** */ - locality("2.5.6.3", "RFC 4519"), - /** */ - organization("2.5.6.4", "RFC 4519"), - /** */ - organizationalUnit("2.5.6.5", "RFC 4519"), - /** */ - person("2.5.6.6", "RFC 4519"), - /** */ - organizationalPerson("2.5.6.7", "RFC 4519"), - /** */ - organizationalRole("2.5.6.8", "RFC 4519"), - /** */ - groupOfNames("2.5.6.9", "RFC 4519"), - /** */ - residentialPerson("2.5.6.10", "RFC 4519"), - /** */ - applicationProcess("2.5.6.11", "RFC 4519"), - /** */ - device("2.5.6.14", "RFC 4519"), - /** */ - strongAuthenticationUser("2.5.6.15", "RFC 4523"), - /** */ - certificationAuthority("2.5.6.16", "RFC 4523"), - // /** Should be certificationAuthority-V2 */ - // certificationAuthority_V2("2.5.6.16.2", "RFC 4523") { - // }, - /** */ - groupOfUniqueNames("2.5.6.17", "RFC 4519"), - /** */ - userSecurityInformation("2.5.6.18", "RFC 4523"), - /** */ - cRLDistributionPoint("2.5.6.19", "RFC 4523"), - /** */ - pkiUser("2.5.6.21", "RFC 4523"), - /** */ - pkiCA("2.5.6.22", "RFC 4523"), - /** */ - deltaCRL("2.5.6.23", "RFC 4523"), - /** */ - subschema("2.5.20.1", "RFC 4512"), - /** */ - ldapSubEntry("2.16.840.1.113719.2.142.6.1.1", "draft-ietf-ldup-subentry"), - /** */ - changeLogEntry("2.16.840.1.113730.3.2.1", "draft-good-ldap-changelog"), - /** */ - inetOrgPerson("2.16.840.1.113730.3.2.2", "RFC 2798"), - /** */ - referral("2.16.840.1.113730.3.2.6", "RFC 3296"), - - // RFC 2307bis (partial) - /** */ - posixAccount("1.3.6.1.1.1.2.0", "RFC 2307bis"), - /** */ - posixGroup("1.3.6.1.1.1.2.2", "RFC 2307bis"), - - // - ; - - /** MUST be equal to ContentRepository LDAP namespace. */ - final static String LDAP_NAMESPACE_URI = "http://www.argeo.org/ns/ldap"; - /** MUST be equal to ContentRepository LDAP prefix. */ - final static String LDAP_DEFAULT_PREFIX = "ldap"; - - private final String oid, spec; - private final QName value; - - private LdapObjs(String oid, String spec) { - this.oid = oid; - this.spec = spec; - this.value = new DisplayQName(LDAP_NAMESPACE_URI, name(), LDAP_DEFAULT_PREFIX); - } - - public QName qName() { - return value; - } - - public String getOid() { - return oid; - } - - public String getSpec() { - return spec; - } - - @Deprecated - public String property() { - return get(); - } - - @Override - public String get() { - return LdapObjs.LDAP_DEFAULT_PREFIX + ":" + name(); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/naming/NamingUtils.java b/org.argeo.util/src/org/argeo/util/naming/NamingUtils.java deleted file mode 100644 index ff4ed31b4..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/NamingUtils.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.argeo.util.naming; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.temporal.ChronoField; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -public class NamingUtils { - /** As per https://tools.ietf.org/html/rfc4517#section-3.3.13 */ - private final static DateTimeFormatter utcLdapDate = DateTimeFormatter.ofPattern("uuuuMMddHHmmssX") - .withZone(ZoneOffset.UTC); - - /** @return null if not parseable */ - public static Instant ldapDateToInstant(String ldapDate) { - try { - return OffsetDateTime.parse(ldapDate, utcLdapDate).toInstant(); - } catch (DateTimeParseException e) { - return null; - } - } - - /** @return null if not parseable */ - public static ZonedDateTime ldapDateToZonedDateTime(String ldapDate) { - try { - return OffsetDateTime.parse(ldapDate, utcLdapDate).toZonedDateTime(); - } catch (DateTimeParseException e) { - return null; - } - } - - public static Calendar ldapDateToCalendar(String ldapDate) { - OffsetDateTime instant = OffsetDateTime.parse(ldapDate, utcLdapDate); - GregorianCalendar calendar = new GregorianCalendar(); - calendar.set(Calendar.DAY_OF_MONTH, instant.get(ChronoField.DAY_OF_MONTH)); - calendar.set(Calendar.MONTH, instant.get(ChronoField.MONTH_OF_YEAR)); - calendar.set(Calendar.YEAR, instant.get(ChronoField.YEAR)); - return calendar; - } - - public static String instantToLdapDate(ZonedDateTime instant) { - return utcLdapDate.format(instant.withZoneSameInstant(ZoneOffset.UTC)); - } - - public static String getQueryValue(Map> query, String key) { - if (!query.containsKey(key)) - return null; - List val = query.get(key); - if (val.size() == 1) - return val.get(0); - else - throw new IllegalArgumentException("There are " + val.size() + " value(s) for " + key); - } - - public static Map> queryToMap(URI uri) { - return queryToMap(uri.getQuery()); - } - - private static Map> queryToMap(String queryPart) { - try { - final Map> query_pairs = new LinkedHashMap>(); - if (queryPart == null) - return query_pairs; - final String[] pairs = queryPart.split("&"); - for (String pair : pairs) { - final int idx = pair.indexOf("="); - final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name()) - : pair; - if (!query_pairs.containsKey(key)) { - query_pairs.put(key, new LinkedList()); - } - final String value = idx > 0 && pair.length() > idx + 1 - ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name()) - : null; - query_pairs.get(key).add(value); - } - return query_pairs; - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException("Cannot convert " + queryPart + " to map", e); - } - } - - private NamingUtils() { - - } - - public static void main(String args[]) { - ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC); - String str = utcLdapDate.format(now); - System.out.println(str); - utcLdapDate.parse(str); - utcLdapDate.parse("19520512000000Z"); - } -} diff --git a/org.argeo.util/src/org/argeo/util/naming/NodeOID.java b/org.argeo.util/src/org/argeo/util/naming/NodeOID.java deleted file mode 100644 index ea163d6a4..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/NodeOID.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.argeo.util.naming; - -interface NodeOID { - String BASE = "1.3.6.1.4.1" + ".48308" + ".1"; - - // uuidgen --md5 --namespace @oid --name 1.3.6.1.4.1.48308 - String BASE_UUID_V3 = "6869e86b-96b7-3d49-b6ab-ffffc5ad95fb"; - - // uuidgen --sha1 --namespace @oid --name 1.3.6.1.4.1.48308 - String BASE_UUID_V5 = "58873947-460c-59a6-a7b4-28a97def5f27"; - - // ATTRIBUTE TYPES - String ATTRIBUTE_TYPES = BASE + ".4"; - - // OBJECT CLASSES - String OBJECT_CLASSES = BASE + ".6"; -} diff --git a/org.argeo.util/src/org/argeo/util/naming/SpecifiedName.java b/org.argeo.util/src/org/argeo/util/naming/SpecifiedName.java deleted file mode 100644 index 22f2a2d69..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/SpecifiedName.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.argeo.util.naming; - -/** - * A name which has been specified and for which an id has been defined - * (typically an OID). - */ -public interface SpecifiedName { - /** The name */ - String name(); - - /** An RFC or the URLof some specification */ - default String getSpec() { - return null; - } - - /** Typically an OID */ - default String getID() { - return getClass().getName() + "." + name(); - } -} diff --git a/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java b/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java deleted file mode 100644 index 4fba43405..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java +++ /dev/null @@ -1,216 +0,0 @@ -package org.argeo.util.naming.dns; - -import java.io.Closeable; -import java.io.IOException; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.SortedSet; -import java.util.StringJoiner; -import java.util.TreeMap; -import java.util.TreeSet; - -import javax.naming.Binding; -import javax.naming.Context; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.InitialDirContext; - -public class DnsBrowser implements Closeable { - private final DirContext initialCtx; - - public DnsBrowser() { - this(new ArrayList<>()); - } - - public DnsBrowser(List dnsServerUrls) { - try { - Objects.requireNonNull(dnsServerUrls); - Hashtable env = new Hashtable<>(); - env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); - if (!dnsServerUrls.isEmpty()) { - boolean specified = false; - StringJoiner providerUrl = new StringJoiner(" "); - for (String dnsUrl : dnsServerUrls) { - if (dnsUrl != null) { - providerUrl.add(dnsUrl); - specified = true; - } - } - if (specified) - env.put(Context.PROVIDER_URL, providerUrl.toString()); - } - initialCtx = new InitialDirContext(env); - } catch (NamingException e) { - throw new IllegalStateException("Cannot initialise DNS borowser.", e); - } - } - - public Map> getAllRecords(String name) { - try { - Map> res = new TreeMap<>(); - Attributes attrs = initialCtx.getAttributes(name); - NamingEnumeration ids = attrs.getIDs(); - while (ids.hasMore()) { - String recordType = ids.next(); - List lst = new ArrayList(); - res.put(recordType, lst); - Attribute attr = attrs.get(recordType); - addValues(attr, lst); - } - return Collections.unmodifiableMap(res); - } catch (NamingException e) { - throw new IllegalStateException("Cannot get allrecords of " + name, e); - } - } - - /** - * Return a single record (typically A, AAAA, etc. or null if not available. - * Will fail if multiple records. - */ - public String getRecord(String name, String recordType) { - try { - Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType }); - if (attrs.size() == 0) - return null; - Attribute attr = attrs.get(recordType); - if (attr.size() > 1) - throw new IllegalArgumentException("Multiple record type " + recordType); - assert attr.size() != 0; - Object value = attr.get(); - assert value != null; - return value.toString(); - } catch (NameNotFoundException e) { - return null; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get DNS entry " + recordType + " of " + name, e); - } - } - - /** - * Return records of a given type. - */ - public List getRecords(String name, String recordType) { - try { - List res = new ArrayList(); - Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType }); - Attribute attr = attrs.get(recordType); - addValues(attr, res); - return res; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get records " + recordType + " of " + name, e); - } - } - - /** Ordered, with preferred first. */ - public List getSrvRecordsAsHosts(String name, boolean withPort) { - List raw = getRecords(name, "SRV"); - if (raw.size() == 0) - return null; - SortedSet res = new TreeSet<>(); - for (int i = 0; i < raw.size(); i++) { - String record = raw.get(i); - String[] arr = record.split(" "); - Integer priority = Integer.parseInt(arr[0]); - Integer weight = Integer.parseInt(arr[1]); - Integer port = Integer.parseInt(arr[2]); - String hostname = arr[3]; - SrvRecord order = new SrvRecord(priority, weight, port, hostname); - res.add(order); - } - List lst = new ArrayList<>(); - for (SrvRecord order : res) { - lst.add(order.toHost(withPort)); - } - return Collections.unmodifiableList(lst); - } - - private void addValues(Attribute attr, List lst) throws NamingException { - NamingEnumeration values = attr.getAll(); - while (values.hasMore()) { - Object value = values.next(); - if (value != null) { - if (value instanceof byte[]) { - String str = Base64.getEncoder().encodeToString((byte[]) value); - lst.add(str); - } else - lst.add(value.toString()); - } - } - - } - - public List listEntries(String name) { - try { - List res = new ArrayList(); - NamingEnumeration ne = initialCtx.listBindings(name); - while (ne.hasMore()) { - Binding b = ne.next(); - res.add(b.getName()); - } - return Collections.unmodifiableList(res); - } catch (NamingException e) { - throw new IllegalStateException("Cannot list entries of " + name, e); - } - } - - @Override - public void close() throws IOException { - destroy(); - } - - public void destroy() { - try { - initialCtx.close(); - } catch (NamingException e) { - // silent - } - } - - public static void main(String[] args) { - if (args.length == 0) { - printUsage(System.err); - System.exit(1); - } - try (DnsBrowser dnsBrowser = new DnsBrowser()) { - String hostname = args[0]; - String recordType = args.length > 1 ? args[1] : "A"; - if (recordType.equals("*")) { - Map> records = dnsBrowser.getAllRecords(hostname); - for (String type : records.keySet()) { - for (String record : records.get(type)) { - String typeLabel; - if ("44".equals(type)) - typeLabel = "SSHFP"; - else if ("46".equals(type)) - typeLabel = "RRSIG"; - else if ("48".equals(type)) - typeLabel = "DNSKEY"; - else - typeLabel = type; - System.out.println(typeLabel + "\t" + record); - } - } - } else { - System.out.println(dnsBrowser.getRecord(hostname, recordType)); - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - - public static void printUsage(PrintStream out) { - out.println("java org.argeo.naming.DnsBrowser [ | *]"); - } - -} \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/naming/dns/SrvRecord.java b/org.argeo.util/src/org/argeo/util/naming/dns/SrvRecord.java deleted file mode 100644 index ea6f3cc96..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/dns/SrvRecord.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.argeo.util.naming.dns; - -class SrvRecord implements Comparable { - private final Integer priority; - private final Integer weight; - private final Integer port; - private final String hostname; - - public SrvRecord(Integer priority, Integer weight, Integer port, String hostname) { - this.priority = priority; - this.weight = weight; - this.port = port; - this.hostname = hostname; - } - - @Override - public int compareTo(SrvRecord other) { - // https: // en.wikipedia.org/wiki/SRV_record - if (priority != other.priority) - return priority - other.priority; - if (weight != other.weight) - return other.weight - other.weight; - String host = toHost(false); - String otherHost = other.toHost(false); - if (host.length() == otherHost.length()) - return host.compareTo(otherHost); - else - return host.length() - otherHost.length(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof SrvRecord) { - SrvRecord other = (SrvRecord) obj; - return priority == other.priority && weight == other.weight && port == other.port - && hostname.equals(other.hostname); - } - return false; - } - - @Override - public String toString() { - return priority + " " + weight; - } - - public String toHost(boolean withPort) { - String hostStr = hostname; - if (hostname.charAt(hostname.length() - 1) == '.') - hostStr = hostname.substring(0, hostname.length() - 1); - return hostStr + (withPort ? ":" + port : ""); - } -} diff --git a/org.argeo.util/src/org/argeo/util/naming/package-info.java b/org.argeo.util/src/org/argeo/util/naming/package-info.java deleted file mode 100644 index f62af365e..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Generic naming and LDAP support. */ -package org.argeo.util.naming; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/package-info.java b/org.argeo.util/src/org/argeo/util/package-info.java deleted file mode 100644 index 4354b0a14..000000000 --- a/org.argeo.util/src/org/argeo/util/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Generic Java utilities. */ -package org.argeo.util; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/register/Component.java b/org.argeo.util/src/org/argeo/util/register/Component.java deleted file mode 100644 index 275811e9d..000000000 --- a/org.argeo.util/src/org/argeo/util/register/Component.java +++ /dev/null @@ -1,333 +0,0 @@ -package org.argeo.util.register; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * A wrapper for an object, whose dependencies and life cycle can be managed. - */ -public class Component implements Supplier, Comparable> { - - private final I instance; - - private final Runnable init; - private final Runnable close; - - private final Map, PublishedType> types; - private final Set> dependencies; - private final Map properties; - - private CompletableFuture activationStarted = null; - private CompletableFuture activated = null; - - private CompletableFuture deactivationStarted = null; - private CompletableFuture deactivated = null; - - // internal - private Set> dependants = new HashSet<>(); - - private RankingKey rankingKey; - - Component(ComponentRegister register, I instance, Runnable init, Runnable close, Set> dependencies, - Set> classes, Map properties) { - assert instance != null; - assert init != null; - assert close != null; - assert dependencies != null; - assert classes != null; - - this.instance = instance; - this.init = init; - this.close = close; - - // types - Map, PublishedType> types = new HashMap<>(classes.size()); - for (Class clss : classes) { -// if (!clss.isAssignableFrom(instance.getClass())) -// throw new IllegalArgumentException( -// "Type " + clss.getName() + " is not compatible with " + instance.getClass().getName()); - types.put(clss, new PublishedType<>(this, clss)); - } - this.types = Collections.unmodifiableMap(types); - - // dependencies - this.dependencies = Collections.unmodifiableSet(new HashSet<>(dependencies)); - for (Dependency dependency : this.dependencies) { - dependency.setDependantComponent(this); - } - - // deactivated by default - deactivated = CompletableFuture.completedFuture(null); - deactivationStarted = CompletableFuture.completedFuture(null); - - // TODO check whether context is active, so that we start right away - prepareNextActivation(); - - long serviceId = register.register(this); - Map props = new HashMap<>(properties); - props.put(RankingKey.SERVICE_ID, serviceId); - this.properties = Collections.unmodifiableMap(props); - rankingKey = new RankingKey(properties); - } - - private void prepareNextActivation() { - activationStarted = new CompletableFuture(); - activated = activationStarted // - .thenComposeAsync(this::dependenciesActivated) // - .thenRun(this.init) // - .thenRun(() -> prepareNextDeactivation()); - } - - private void prepareNextDeactivation() { - deactivationStarted = new CompletableFuture(); - deactivated = deactivationStarted // - .thenComposeAsync(this::dependantsDeactivated) // - .thenRun(this.close) // - .thenRun(() -> prepareNextActivation()); - } - - CompletableFuture getActivated() { - return activated; - } - - CompletableFuture getDeactivated() { - return deactivated; - } - - void startActivating() { - if (activated.isDone() || activationStarted.isDone()) - return; - activationStarted.complete(null); - } - - void startDeactivating() { - if (deactivated.isDone() || deactivationStarted.isDone()) - return; - deactivationStarted.complete(null); - } - - CompletableFuture dependenciesActivated(Void v) { - Set> constraints = new HashSet<>(this.dependencies.size()); - for (Dependency dependency : this.dependencies) { - CompletableFuture dependencyActivated = dependency.publisherActivated() // - .thenCompose(dependency::set); - constraints.add(dependencyActivated); - } - return CompletableFuture.allOf(constraints.toArray(new CompletableFuture[constraints.size()])); - } - - CompletableFuture dependantsDeactivated(Void v) { - Set> constraints = new HashSet<>(this.dependants.size()); - for (Dependency dependant : this.dependants) { - CompletableFuture dependantDeactivated = dependant.dependantDeactivated() // - .thenCompose(dependant::unset); - constraints.add(dependantDeactivated); - } - CompletableFuture dependantsDeactivated = CompletableFuture - .allOf(constraints.toArray(new CompletableFuture[constraints.size()])); - return dependantsDeactivated; - - } - - void addDependant(Dependency dependant) { - dependants.add(dependant); - } - - @Override - public I get() { - return instance; - } - - @SuppressWarnings("unchecked") - public PublishedType getType(Class clss) { - if (!types.containsKey(clss)) - throw new IllegalArgumentException(clss.getName() + " is not a type published by this component"); - return (PublishedType) types.get(clss); - } - - public boolean isPublishedType(Class clss) { - return types.containsKey(clss); - } - - public Map getProperties() { - return properties; - } - - @Override - public int compareTo(Component o) { - return rankingKey.compareTo(rankingKey); - } - - @Override - public int hashCode() { - Long serviceId = (Long) properties.get(RankingKey.SERVICE_ID); - if (serviceId != null) - return serviceId.intValue(); - else - return super.hashCode(); - } - - @Override - public String toString() { - List classes = new ArrayList<>(); - for (Class clss : types.keySet()) { - classes.add(clss.getName()); - } - return "Component " + classes + " " + properties + ""; - } - - /** A type which has been explicitly exposed by a component. */ - public static class PublishedType { - private Component component; - private Class clss; - - private CompletableFuture value; - - public PublishedType(Component component, Class clss) { - this.clss = clss; - this.component = component; - value = CompletableFuture.completedFuture((T) component.instance); - } - - public Component getPublisher() { - return component; - } - - public Class getType() { - return clss; - } - - public CompletionStage getValue() { - return value.minimalCompletionStage(); - } - } - - /** Builds a {@link Component}. */ - public static class Builder implements Supplier { - private final I instance; - - private Runnable init; - private Runnable close; - - private Set> dependencies = new HashSet<>(); - private Set> types = new HashSet<>(); - private final Map properties = new HashMap<>(); - - public Builder(I instance) { - this.instance = instance; - } - - public Component build(ComponentRegister register) { - // default values - if (types.isEmpty()) { - types.add(getInstanceClass()); - } - - if (init == null) - init = () -> { - }; - if (close == null) - close = () -> { - }; - - // instantiation - Component component = new Component(register, instance, init, close, dependencies, types, properties); - for (Dependency dependency : dependencies) { - dependency.type.getPublisher().addDependant(dependency); - } - return component; - } - - public Builder addType(Class clss) { - types.add(clss); - return this; - } - - public Builder addActivation(Runnable init) { - if (this.init != null) - throw new IllegalArgumentException("init method is already set"); - this.init = init; - return this; - } - - public Builder addDeactivation(Runnable close) { - if (this.close != null) - throw new IllegalArgumentException("close method is already set"); - this.close = close; - return this; - } - - public Builder addDependency(PublishedType type, Consumer set, Consumer unset) { - dependencies.add(new Dependency(type, set, unset)); - return this; - } - - public void addProperty(String key, Object value) { - if (properties.containsKey(key)) - throw new IllegalStateException("Key " + key + " is already set."); - properties.put(key, value); - } - - @Override - public I get() { - return instance; - } - - @SuppressWarnings("unchecked") - private Class getInstanceClass() { - return (Class) instance.getClass(); - } - - } - - static class Dependency { - private PublishedType type; - private Consumer set; - private Consumer unset; - - // live - Component dependantComponent; - CompletableFuture setStage; - CompletableFuture unsetStage; - - public Dependency(PublishedType types, Consumer set, Consumer unset) { - super(); - this.type = types; - this.set = set != null ? set : t -> { - }; - this.unset = unset != null ? unset : t -> { - }; - } - - // live - void setDependantComponent(Component component) { - this.dependantComponent = component; - } - - CompletableFuture publisherActivated() { - return type.getPublisher().activated.copy(); - } - - CompletableFuture dependantDeactivated() { - return dependantComponent.deactivated.copy(); - } - - CompletableFuture set(Void v) { - return type.value.thenAccept(set); - } - - CompletableFuture unset(Void v) { - return type.value.thenAccept(unset); - } - - } -} diff --git a/org.argeo.util/src/org/argeo/util/register/ComponentRegister.java b/org.argeo.util/src/org/argeo/util/register/ComponentRegister.java deleted file mode 100644 index d78b6badb..000000000 --- a/org.argeo.util/src/org/argeo/util/register/ComponentRegister.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.argeo.util.register; - -import java.util.Map; -import java.util.SortedSet; -import java.util.function.Predicate; - -/** A register of components which can coordinate their activation. */ -public interface ComponentRegister { - long register(Component component); - - SortedSet> find(Class clss, Predicate> filter); - - default Component.PublishedType getSingleton(Class type) { - SortedSet> found = find(type, null); - if (found.size() == 0) - throw new IllegalStateException("No component found for " + type); - return found.first().getType(type); - } - - default T getObject(Class clss) { - SortedSet> found = find(clss, null); - if (found.size() == 0) - return null; - return found.first().get(); - } - - Component get(Object instance); - -// default PublishedType getType(Class clss) { -// SortedSet> components = find(clss, null); -// if (components.size() == 0) -// return null; -// return components.first().getType(clss); -// } - - void activate(); - - void deactivate(); - - boolean isActive(); - - void clear(); -} diff --git a/org.argeo.util/src/org/argeo/util/register/RankingKey.java b/org.argeo.util/src/org/argeo/util/register/RankingKey.java deleted file mode 100644 index 7a43e359e..000000000 --- a/org.argeo.util/src/org/argeo/util/register/RankingKey.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.argeo.util.register; - -import java.util.Map; -import java.util.Objects; - -/** - * Key used to classify and filter available components. - */ -public class RankingKey implements Comparable { - public final static String SERVICE_PID = "service.pid"; - public final static String SERVICE_ID = "service.id"; - public final static String SERVICE_RANKING = "service.ranking"; - - private String pid; - private Integer ranking = 0; - private Long id = 0l; - - public RankingKey(String pid, Integer ranking, Long id) { - super(); - this.pid = pid; - this.ranking = ranking; - this.id = id; - } - - public RankingKey(Map properties) { - this.pid = properties.containsKey(SERVICE_PID) ? properties.get(SERVICE_PID).toString() : null; - this.ranking = properties.containsKey(SERVICE_RANKING) - ? Integer.parseInt(properties.get(SERVICE_RANKING).toString()) - : 0; - this.id = properties.containsKey(SERVICE_ID) ? (Long) properties.get(SERVICE_ID) : null; - } - - @Override - public int hashCode() { - Integer result = 0; - if (pid != null) - result = +pid.hashCode(); - if (ranking != null) - result = +ranking; - return result; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - return new RankingKey(pid, ranking, id); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(""); - if (pid != null) - sb.append(pid); - if (ranking != null && ranking != 0) - sb.append(' ').append(ranking); - return sb.toString(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof RankingKey)) - return false; - RankingKey other = (RankingKey) obj; - return Objects.equals(pid, other.pid) && Objects.equals(ranking, other.ranking) && Objects.equals(id, other.id); - } - - @Override - public int compareTo(RankingKey o) { - if (pid != null && o.pid != null) { - if (pid.equals(o.pid)) { - if (ranking.equals(o.ranking)) - if (id != null && o.id != null) - return id.compareTo(o.id); - else - return 0; - else - return ranking.compareTo(o.ranking); - } else { - return pid.compareTo(o.pid); - } - - } else { - } - return -1; - } - - public String getPid() { - return pid; - } - - public Integer getRanking() { - return ranking; - } - - public Long getId() { - return id; - } - - public static RankingKey minPid(String pid) { - return new RankingKey(pid, Integer.MIN_VALUE, null); - } - - public static RankingKey maxPid(String pid) { - return new RankingKey(pid, Integer.MAX_VALUE, null); - } -} diff --git a/org.argeo.util/src/org/argeo/util/register/SimpleRegister.java b/org.argeo.util/src/org/argeo/util/register/SimpleRegister.java deleted file mode 100644 index 7aa9ebef9..000000000 --- a/org.argeo.util/src/org/argeo/util/register/SimpleRegister.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.argeo.util.register; - -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Predicate; - -/** A minimal component register. */ -public class SimpleRegister implements ComponentRegister { - private final AtomicBoolean started = new AtomicBoolean(false); - private final IdentityHashMap> components = new IdentityHashMap<>(); - private final AtomicLong nextServiceId = new AtomicLong(0l); - - @Override - public long register(Component component) { - return registerComponent(component); - } - - @SuppressWarnings({ "unchecked" }) - @Override - public synchronized SortedSet> find(Class clss, - Predicate> filter) { - SortedSet> result = new TreeSet<>(); - instances: for (Object instance : components.keySet()) { - if (!clss.isAssignableFrom(instance.getClass())) - continue instances; - Component component = (Component) components.get(instance); - - if (component.isPublishedType(clss)) { - if (filter != null) { - filter.test(component.getProperties()); - } - result.add(component); - } - } - if (result.isEmpty()) - return null; - return result; - - } - - synchronized long registerComponent(Component component) { - if (started.get()) // TODO make it really dynamic - throw new IllegalStateException("Already activated"); - if (components.containsKey(component.get())) - throw new IllegalArgumentException("Already registered as component"); - components.put(component.get(), component); - return nextServiceId.incrementAndGet(); - } - - @Override - public synchronized Component get(Object instance) { - if (!components.containsKey(instance)) - throw new IllegalArgumentException("Not registered as component"); - return components.get(instance); - } - - @Override - public synchronized void activate() { - if (started.get()) - throw new IllegalStateException("Already activated"); - Set> constraints = new HashSet<>(); - for (Component component : components.values()) { - component.startActivating(); - constraints.add(component.getActivated()); - } - - // wait - try { - CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(true)) - .get(); - } catch (InterruptedException e) { - throw new RuntimeException("Register activation has been interrupted", e); - } catch (ExecutionException e) { - if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) { - throw (RuntimeException) e.getCause(); - } else { - throw new IllegalStateException("Cannot activate register", e.getCause()); - } - } - } - - @Override - public synchronized void deactivate() { - if (!started.get()) - throw new IllegalStateException("Not activated"); - Set> constraints = new HashSet<>(); - for (Component component : components.values()) { - component.startDeactivating(); - constraints.add(component.getDeactivated()); - } - - // wait - try { - CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(false)) - .get(); - } catch (InterruptedException e) { - throw new RuntimeException("Register deactivation has been interrupted", e); - } catch (ExecutionException e) { - if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) { - throw (RuntimeException) e.getCause(); - } else { - throw new IllegalStateException("Cannot deactivate register", e.getCause()); - } - } - } - - @Override - public synchronized boolean isActive() { - return started.get(); - } - - @Override - public synchronized void clear() { - components.clear(); - } -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/AbstractWorkingCopy.java b/org.argeo.util/src/org/argeo/util/transaction/AbstractWorkingCopy.java deleted file mode 100644 index 0da35ac7b..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/AbstractWorkingCopy.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.argeo.util.transaction; - -import java.util.HashMap; -import java.util.Map; - -public abstract class AbstractWorkingCopy implements WorkingCopy { - private Map newData = new HashMap(); - private Map modifiedData = new HashMap(); - private Map deletedData = new HashMap(); - - protected abstract ID getId(DATA data); - - protected abstract ATTR cloneAttributes(DATA data); - - public void cleanUp() { - // clean collections - newData.clear(); - newData = null; - modifiedData.clear(); - modifiedData = null; - deletedData.clear(); - deletedData = null; - } - - public boolean noModifications() { - return newData.size() == 0 && modifiedData.size() == 0 && deletedData.size() == 0; - } - - public void startEditing(DATA user) { - ID id = getId(user); - if (modifiedData.containsKey(id)) - throw new IllegalStateException("Already editing " + id); - modifiedData.put(id, cloneAttributes(user)); - } - - public Map getNewData() { - return newData; - } - - public Map getDeletedData() { - return deletedData; - } - - public Map getModifiedData() { - return modifiedData; - } - -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/JtaStatusAdapter.java b/org.argeo.util/src/org/argeo/util/transaction/JtaStatusAdapter.java deleted file mode 100644 index bd977069f..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/JtaStatusAdapter.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.argeo.util.transaction; - -/** JTA transaction status. */ -public class JtaStatusAdapter implements TransactionStatusAdapter { - private static final Integer STATUS_ACTIVE = 0; - private static final Integer STATUS_COMMITTED = 3; - private static final Integer STATUS_COMMITTING = 8; - private static final Integer STATUS_MARKED_ROLLBACK = 1; - private static final Integer STATUS_NO_TRANSACTION = 6; - private static final Integer STATUS_PREPARED = 2; - private static final Integer STATUS_PREPARING = 7; - private static final Integer STATUS_ROLLEDBACK = 4; - private static final Integer STATUS_ROLLING_BACK = 9; -// private static final Integer STATUS_UNKNOWN = 5; - - @Override - public Integer getActiveStatus() { - return STATUS_ACTIVE; - } - - @Override - public Integer getPreparingStatus() { - return STATUS_PREPARING; - } - - @Override - public Integer getMarkedRollbackStatus() { - return STATUS_MARKED_ROLLBACK; - } - - @Override - public Integer getPreparedStatus() { - return STATUS_PREPARED; - } - - @Override - public Integer getCommittingStatus() { - return STATUS_COMMITTING; - } - - @Override - public Integer getCommittedStatus() { - return STATUS_COMMITTED; - } - - @Override - public Integer getRollingBackStatus() { - return STATUS_ROLLING_BACK; - } - - @Override - public Integer getRolledBackStatus() { - return STATUS_ROLLEDBACK; - } - - @Override - public Integer getNoTransactionStatus() { - return STATUS_NO_TRANSACTION; - } - -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/SimpleRollbackException.java b/org.argeo.util/src/org/argeo/util/transaction/SimpleRollbackException.java deleted file mode 100644 index 010b549c7..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/SimpleRollbackException.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.util.transaction; - -/** Internal unchecked rollback exception. */ -class SimpleRollbackException extends RuntimeException { - private static final long serialVersionUID = 8055601819719780566L; - - public SimpleRollbackException() { - super(); - } - - public SimpleRollbackException(Throwable cause) { - super(cause); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/SimpleTransaction.java b/org.argeo.util/src/org/argeo/util/transaction/SimpleTransaction.java deleted file mode 100644 index 56ef06353..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/SimpleTransaction.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.argeo.util.transaction; - -import java.util.ArrayList; -import java.util.List; - -import javax.transaction.xa.XAException; -import javax.transaction.xa.XAResource; -import javax.transaction.xa.Xid; - -/** Simple implementation of an XA transaction. */ -class SimpleTransaction -//implements Transaction, Status -{ - private final Xid xid; - private T status; - private final List xaResources = new ArrayList(); - - private final SimpleTransactionManager transactionManager; - private TransactionStatusAdapter tsa; - - public SimpleTransaction(SimpleTransactionManager transactionManager, TransactionStatusAdapter tsa) { - this.tsa = tsa; - this.status = tsa.getActiveStatus(); - this.xid = new UuidXid(); - this.transactionManager = transactionManager; - } - - public synchronized void commit() -// throws RollbackException, HeuristicMixedException, HeuristicRollbackException, -// SecurityException, IllegalStateException, SystemException - { - status = tsa.getPreparingStatus(); - for (XAResource xaRes : xaResources) { - if (status.equals(tsa.getMarkedRollbackStatus())) - break; - try { - xaRes.prepare(xid); - } catch (XAException e) { - status = tsa.getMarkedRollbackStatus(); - error("Cannot prepare " + xaRes + " for " + xid, e); - } - } - if (status.equals(tsa.getMarkedRollbackStatus())) { - rollback(); - throw new SimpleRollbackException(); - } - status = tsa.getPreparedStatus(); - - status = tsa.getCommittingStatus(); - for (XAResource xaRes : xaResources) { - if (status.equals(tsa.getMarkedRollbackStatus())) - break; - try { - xaRes.commit(xid, false); - } catch (XAException e) { - status = tsa.getMarkedRollbackStatus(); - error("Cannot prepare " + xaRes + " for " + xid, e); - } - } - if (status.equals(tsa.getMarkedRollbackStatus())) { - rollback(); - throw new SimpleRollbackException(); - } - - // complete - status = tsa.getCommittedStatus(); - clearResources(XAResource.TMSUCCESS); - transactionManager.unregister(xid); - } - - public synchronized void rollback() -// throws IllegalStateException, SystemException - { - status = tsa.getRollingBackStatus(); - for (XAResource xaRes : xaResources) { - try { - xaRes.rollback(xid); - } catch (XAException e) { - error("Cannot rollback " + xaRes + " for " + xid, e); - } - } - - // complete - status = tsa.getRolledBackStatus(); - clearResources(XAResource.TMFAIL); - transactionManager.unregister(xid); - } - - public synchronized boolean enlistResource(XAResource xaRes) -// throws RollbackException, IllegalStateException, SystemException - { - if (xaResources.add(xaRes)) { - try { - xaRes.start(getXid(), XAResource.TMNOFLAGS); - return true; - } catch (XAException e) { - error("Cannot enlist " + xaRes, e); - return false; - } - } else - return false; - } - - public synchronized boolean delistResource(XAResource xaRes, int flag) -// throws IllegalStateException, SystemException - { - if (xaResources.remove(xaRes)) { - try { - xaRes.end(getXid(), flag); - } catch (XAException e) { - error("Cannot delist " + xaRes, e); - return false; - } - return true; - } else - return false; - } - - protected void clearResources(int flag) { - for (XAResource xaRes : xaResources) - try { - xaRes.end(getXid(), flag); - } catch (XAException e) { - error("Cannot end " + xaRes, e); - } - xaResources.clear(); - } - - protected void error(Object obj, Exception e) { - System.err.println(obj); - e.printStackTrace(); - } - - public synchronized T getStatus() -// throws SystemException - { - return status; - } - -// public void registerSynchronization(Synchronization sync) -// throws RollbackException, IllegalStateException, SystemException { -// throw new UnsupportedOperationException(); -// } - - public void setRollbackOnly() -// throws IllegalStateException, SystemException - { - status = tsa.getMarkedRollbackStatus(); - } - - @Override - public int hashCode() { - return xid.hashCode(); - } - - Xid getXid() { - return xid; - } - -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/SimpleTransactionManager.java b/org.argeo.util/src/org/argeo/util/transaction/SimpleTransactionManager.java deleted file mode 100644 index f5be7c8e5..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/SimpleTransactionManager.java +++ /dev/null @@ -1,214 +0,0 @@ -package org.argeo.util.transaction; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Callable; - -import javax.transaction.xa.XAResource; -import javax.transaction.xa.Xid; - -/** - * Simple implementation of an XA transaction manager. - */ -public class SimpleTransactionManager -// implements TransactionManager, UserTransaction - implements WorkControl, WorkTransaction { - private ThreadLocal> current = new ThreadLocal>(); - - private Map> knownTransactions = Collections - .synchronizedMap(new HashMap>()); - private TransactionStatusAdapter tsa = new JtaStatusAdapter(); -// private SyncRegistry syncRegistry = new SyncRegistry(); - - /* - * WORK IMPLEMENTATION - */ - @Override - public T required(Callable work) { - T res; - begin(); - try { - res = work.call(); - commit(); - } catch (Exception e) { - rollback(); - throw new SimpleRollbackException(e); - } - return res; - } - - @Override - public WorkContext getWorkContext() { - return new WorkContext() { - - @Override - public void registerXAResource(XAResource resource, String recoveryId) { - getTransaction().enlistResource(resource); - } - }; - } - - /* - * WORK TRANSACTION IMPLEMENTATION - */ - - @Override - public boolean isNoTransactionStatus() { - return tsa.getNoTransactionStatus().equals(getStatus()); - } - - /* - * JTA IMPLEMENTATION - */ - - public void begin() -// throws NotSupportedException, SystemException - { - if (getCurrent() != null) - throw new UnsupportedOperationException("Nested transactions are not supported"); - SimpleTransaction transaction = new SimpleTransaction(this, tsa); - knownTransactions.put(transaction.getXid(), transaction); - current.set(transaction); - } - - public void commit() -// throws RollbackException, HeuristicMixedException, HeuristicRollbackException, -// SecurityException, IllegalStateException, SystemException - { - if (getCurrent() == null) - throw new IllegalStateException("No transaction registered with the current thread."); - getCurrent().commit(); - } - - public int getStatus() -// throws SystemException - { - if (getCurrent() == null) - return tsa.getNoTransactionStatus(); - return getTransaction().getStatus(); - } - - public SimpleTransaction getTransaction() -// throws SystemException - { - return getCurrent(); - } - - protected SimpleTransaction getCurrent() -// throws SystemException - { - SimpleTransaction transaction = current.get(); - if (transaction == null) - return null; - Integer status = transaction.getStatus(); - if (status.equals(tsa.getCommittedStatus()) || status.equals(tsa.getRolledBackStatus())) { - current.remove(); - return null; - } - return transaction; - } - - void unregister(Xid xid) { - knownTransactions.remove(xid); - } - - public void resume(SimpleTransaction tobj) -// throws InvalidTransactionException, IllegalStateException, SystemException - { - if (getCurrent() != null) - throw new IllegalStateException("Transaction " + current.get() + " already registered"); - current.set(tobj); - } - - public void rollback() -// throws IllegalStateException, SecurityException, SystemException - { - if (getCurrent() == null) - throw new IllegalStateException("No transaction registered with the current thread."); - getCurrent().rollback(); - } - - public void setRollbackOnly() -// throws IllegalStateException, SystemException - { - if (getCurrent() == null) - throw new IllegalStateException("No transaction registered with the current thread."); - getCurrent().setRollbackOnly(); - } - - public void setTransactionTimeout(int seconds) -// throws SystemException - { - throw new UnsupportedOperationException(); - } - - public SimpleTransaction suspend() -// throws SystemException - { - SimpleTransaction transaction = getCurrent(); - current.remove(); - return transaction; - } - -// public TransactionSynchronizationRegistry getTsr() { -// return syncRegistry; -// } -// -// private class SyncRegistry implements TransactionSynchronizationRegistry { -// @Override -// public Object getTransactionKey() { -// try { -// SimpleTransaction transaction = getCurrent(); -// if (transaction == null) -// return null; -// return getCurrent().getXid(); -// } catch (SystemException e) { -// throw new IllegalStateException("Cannot get transaction key", e); -// } -// } -// -// @Override -// public void putResource(Object key, Object value) { -// throw new UnsupportedOperationException(); -// } -// -// @Override -// public Object getResource(Object key) { -// throw new UnsupportedOperationException(); -// } -// -// @Override -// public void registerInterposedSynchronization(Synchronization sync) { -// throw new UnsupportedOperationException(); -// } -// -// @Override -// public int getTransactionStatus() { -// try { -// return getStatus(); -// } catch (SystemException e) { -// throw new IllegalStateException("Cannot get status", e); -// } -// } -// -// @Override -// public boolean getRollbackOnly() { -// try { -// return getStatus() == Status.STATUS_MARKED_ROLLBACK; -// } catch (SystemException e) { -// throw new IllegalStateException("Cannot get status", e); -// } -// } -// -// @Override -// public void setRollbackOnly() { -// try { -// getCurrent().setRollbackOnly(); -// } catch (Exception e) { -// throw new IllegalStateException("Cannot set rollback only", e); -// } -// } -// -// } -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/TransactionStatusAdapter.java b/org.argeo.util/src/org/argeo/util/transaction/TransactionStatusAdapter.java deleted file mode 100644 index a74fef1c9..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/TransactionStatusAdapter.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.argeo.util.transaction; - -/** Abstract the various approaches to represent transaction status. */ -public interface TransactionStatusAdapter { - T getActiveStatus(); - - T getPreparingStatus(); - - T getMarkedRollbackStatus(); - - T getPreparedStatus(); - - T getCommittingStatus(); - - T getCommittedStatus(); - - T getRollingBackStatus(); - - T getRolledBackStatus(); - - T getNoTransactionStatus(); -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/UuidXid.java b/org.argeo.util/src/org/argeo/util/transaction/UuidXid.java deleted file mode 100644 index b6acebec1..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/UuidXid.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.argeo.util.transaction; - -import java.io.Serializable; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.UUID; - -import javax.transaction.xa.Xid; - -/** - * Implementation of {@link Xid} based on {@link UUID}, using max significant - * bits as global transaction id, and least significant bits as branch - * qualifier. - */ -public class UuidXid implements Xid, Serializable { - private static final long serialVersionUID = -5380531989917886819L; - public final static int FORMAT = (int) serialVersionUID; - - private final static int BYTES_PER_LONG = Long.SIZE / Byte.SIZE; - - private final int format; - private final byte[] globalTransactionId; - private final byte[] branchQualifier; - private final String uuid; - private final int hashCode; - - public UuidXid() { - this(UUID.randomUUID()); - } - - public UuidXid(UUID uuid) { - this.format = FORMAT; - this.globalTransactionId = uuidToBytes(uuid.getMostSignificantBits()); - this.branchQualifier = uuidToBytes(uuid.getLeastSignificantBits()); - this.uuid = uuid.toString(); - this.hashCode = uuid.hashCode(); - } - - public UuidXid(Xid xid) { - this(xid.getFormatId(), xid.getGlobalTransactionId(), xid - .getBranchQualifier()); - } - - private UuidXid(int format, byte[] globalTransactionId, - byte[] branchQualifier) { - this.format = format; - this.globalTransactionId = globalTransactionId; - this.branchQualifier = branchQualifier; - this.uuid = bytesToUUID(globalTransactionId, branchQualifier) - .toString(); - this.hashCode = uuid.hashCode(); - } - - @Override - public int getFormatId() { - return format; - } - - @Override - public byte[] getGlobalTransactionId() { - return Arrays.copyOf(globalTransactionId, globalTransactionId.length); - } - - @Override - public byte[] getBranchQualifier() { - return Arrays.copyOf(branchQualifier, branchQualifier.length); - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj instanceof UuidXid) { - UuidXid that = (UuidXid) obj; - return Arrays.equals(globalTransactionId, that.globalTransactionId) - && Arrays.equals(branchQualifier, that.branchQualifier); - } - if (obj instanceof Xid) { - Xid that = (Xid) obj; - return Arrays.equals(globalTransactionId, - that.getGlobalTransactionId()) - && Arrays - .equals(branchQualifier, that.getBranchQualifier()); - } - return uuid.equals(obj.toString()); - } - - @Override - protected Object clone() throws CloneNotSupportedException { - return new UuidXid(format, globalTransactionId, branchQualifier); - } - - @Override - public String toString() { - return uuid; - } - - public UUID asUuid() { - return bytesToUUID(globalTransactionId, branchQualifier); - } - - public static byte[] uuidToBytes(long bits) { - ByteBuffer buffer = ByteBuffer.allocate(BYTES_PER_LONG); - buffer.putLong(0, bits); - return buffer.array(); - } - - public static UUID bytesToUUID(byte[] most, byte[] least) { - if (most.length < BYTES_PER_LONG) - most = Arrays.copyOf(most, BYTES_PER_LONG); - if (least.length < BYTES_PER_LONG) - least = Arrays.copyOf(least, BYTES_PER_LONG); - ByteBuffer buffer = ByteBuffer.allocate(2 * BYTES_PER_LONG); - buffer.put(most, 0, BYTES_PER_LONG); - buffer.put(least, 0, BYTES_PER_LONG); - buffer.flip(); - return new UUID(buffer.getLong(), buffer.getLong()); - } - - // public static void main(String[] args) { - // UUID uuid = UUID.randomUUID(); - // System.out.println(uuid); - // uuid = bytesToUUID(uuidToBytes(uuid.getMostSignificantBits()), - // uuidToBytes(uuid.getLeastSignificantBits())); - // System.out.println(uuid); - // } -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkContext.java b/org.argeo.util/src/org/argeo/util/transaction/WorkContext.java deleted file mode 100644 index e818b830f..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/WorkContext.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.argeo.util.transaction; - -import javax.transaction.xa.XAResource; - -/** - * A minimalistic interface similar to OSGi transaction context in order to - * register XA resources. - */ -public interface WorkContext { - void registerXAResource(XAResource resource, String recoveryId); -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkControl.java b/org.argeo.util/src/org/argeo/util/transaction/WorkControl.java deleted file mode 100644 index db0e475a3..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/WorkControl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.util.transaction; - -import java.util.concurrent.Callable; - -/** - * A minimalistic interface inspired by OSGi transaction control in order to - * commit units of work externally. - */ -public interface WorkControl { - T required(Callable work); - - void setRollbackOnly(); - - WorkContext getWorkContext(); -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkTransaction.java b/org.argeo.util/src/org/argeo/util/transaction/WorkTransaction.java deleted file mode 100644 index 245ca41af..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/WorkTransaction.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.util.transaction; - -/** - * A minimalistic interface inspired by JTA user transaction in order to commit - * units of work externally. - */ -public interface WorkTransaction { - void begin(); - - void commit(); - - void rollback(); - - boolean isNoTransactionStatus(); -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkingCopy.java b/org.argeo.util/src/org/argeo/util/transaction/WorkingCopy.java deleted file mode 100644 index 9dd3fc537..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/WorkingCopy.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.argeo.util.transaction; - -import java.util.Map; - -public interface WorkingCopy { - void startEditing(DATA user); - - boolean noModifications(); - - void cleanUp(); - - Map getNewData(); - - Map getDeletedData(); - - Map getModifiedData(); - -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyProcessor.java b/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyProcessor.java deleted file mode 100644 index cdd640488..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyProcessor.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.argeo.util.transaction; - -public interface WorkingCopyProcessor> { - void prepare(WC wc); - - void commit(WC wc); - - void rollback(WC wc); - - WC newWorkingCopy(); -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyXaResource.java b/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyXaResource.java deleted file mode 100644 index ddb605a19..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyXaResource.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.argeo.util.transaction; - -import java.util.HashMap; -import java.util.Map; - -import javax.transaction.xa.XAException; -import javax.transaction.xa.XAResource; -import javax.transaction.xa.Xid; - -/** {@link XAResource} for a user directory being edited. */ -public class WorkingCopyXaResource> implements XAResource { - private final WorkingCopyProcessor processor; - - private Map workingCopies = new HashMap(); - private Xid editingXid = null; - private int transactionTimeout = 0; - - public WorkingCopyXaResource(WorkingCopyProcessor processor) { - this.processor = processor; - } - - @Override - public synchronized void start(Xid xid, int flags) throws XAException { - if (editingXid != null) - throw new IllegalStateException("Already editing " + editingXid); - WC wc = workingCopies.put(xid, processor.newWorkingCopy()); - if (wc != null) - throw new IllegalStateException("There is already a working copy for " + xid); - this.editingXid = xid; - } - - @Override - public void end(Xid xid, int flags) throws XAException { - checkXid(xid); - } - - private WC wc(Xid xid) { - return workingCopies.get(xid); - } - - public synchronized WC wc() { - if (editingXid == null) - return null; - WC wc = workingCopies.get(editingXid); - if (wc == null) - throw new IllegalStateException("No working copy found for " + editingXid); - return wc; - } - - private synchronized void cleanUp(Xid xid) { - WC wc = workingCopies.get(xid); - if (wc != null) { - wc.cleanUp(); - workingCopies.remove(xid); - } - editingXid = null; - } - - @Override - public int prepare(Xid xid) throws XAException { - checkXid(xid); - WC wc = wc(xid); - if (wc.noModifications()) - return XA_RDONLY; - try { - processor.prepare(wc); - } catch (Exception e) { - e.printStackTrace(); - throw new XAException(XAException.XAER_RMERR); - } - return XA_OK; - } - - @Override - public void commit(Xid xid, boolean onePhase) throws XAException { - try { - checkXid(xid); - WC wc = wc(xid); - if (wc.noModifications()) - return; - if (onePhase) - processor.prepare(wc); - processor.commit(wc); - } catch (Exception e) { - e.printStackTrace(); - throw new XAException(XAException.XAER_RMERR); - } finally { - cleanUp(xid); - } - } - - @Override - public void rollback(Xid xid) throws XAException { - try { - checkXid(xid); - processor.rollback(wc(xid)); - } catch (Exception e) { - e.printStackTrace(); - throw new XAException(XAException.XAER_RMERR); - } finally { - cleanUp(xid); - } - } - - @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 { - return new Xid[0]; - } - - @Override - public int getTransactionTimeout() throws XAException { - return transactionTimeout; - } - - @Override - public boolean setTransactionTimeout(int seconds) throws XAException { - transactionTimeout = seconds; - return true; - } - - private void checkXid(Xid xid) throws XAException { - if (xid == null) - throw new XAException(XAException.XAER_OUTSIDE); - if (!xid.equals(xid)) - throw new XAException(XAException.XAER_NOTA); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/XAResourceProvider.java b/org.argeo.util/src/org/argeo/util/transaction/XAResourceProvider.java deleted file mode 100644 index b0b211bad..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/XAResourceProvider.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.argeo.util.transaction; - -import javax.transaction.xa.XAResource; - -public interface XAResourceProvider { - XAResource getXaResource(); -} diff --git a/org.argeo.util/src/org/argeo/util/transaction/package-info.java b/org.argeo.util/src/org/argeo/util/transaction/package-info.java deleted file mode 100644 index f4811613d..000000000 --- a/org.argeo.util/src/org/argeo/util/transaction/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Minimalistic and partial XA transaction manager implementation. */ -package org.argeo.util.transaction; \ No newline at end of file diff --git a/osgi/equinox/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java b/osgi/equinox/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java index 50be8b7a7..2cd600152 100644 --- a/osgi/equinox/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java +++ b/osgi/equinox/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java @@ -14,9 +14,9 @@ import org.argeo.api.cms.CmsConstants; import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.CmsState; import org.argeo.cms.CmsDeployProperty; +import org.argeo.cms.util.LangUtils; import org.argeo.cms.websocket.server.CmsWebSocketConfigurator; import org.argeo.cms.websocket.server.TestEndpoint; -import org.argeo.util.LangUtils; import org.eclipse.equinox.http.jetty.JettyConfigurator; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; diff --git a/swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangePassword.java b/swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangePassword.java index 393aede8f..dd761267d 100644 --- a/swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangePassword.java +++ b/swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangePassword.java @@ -12,11 +12,11 @@ import javax.inject.Inject; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; +import org.argeo.api.cms.keyring.CryptoKeyring; +import org.argeo.api.cms.transaction.WorkTransaction; import org.argeo.cms.auth.CurrentUser; -import org.argeo.cms.security.CryptoKeyring; import org.argeo.cms.swt.dialogs.CmsFeedback; import org.argeo.cms.swt.dialogs.CmsMessageDialog; -import org.argeo.util.transaction.WorkTransaction; import org.eclipse.e4.core.di.annotations.Execute; import org.eclipse.e4.core.di.annotations.Optional; import org.eclipse.jface.dialogs.Dialog; diff --git a/swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseWorkbench.java b/swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseWorkbench.java index f77c94d7c..91070f222 100644 --- a/swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseWorkbench.java +++ b/swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseWorkbench.java @@ -3,7 +3,7 @@ package org.argeo.cms.e4.handlers; import javax.security.auth.Subject; import org.argeo.cms.auth.CurrentUser; -import org.argeo.util.CurrentSubject; +import org.argeo.cms.util.CurrentSubject; import org.eclipse.e4.core.di.annotations.Execute; import org.eclipse.e4.ui.workbench.IWorkbench; diff --git a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtCmsView.java b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtCmsView.java index ddb6e1b33..59624f93c 100644 --- a/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtCmsView.java +++ b/swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtCmsView.java @@ -18,7 +18,7 @@ import org.argeo.api.cms.ux.CmsUi; import org.argeo.api.cms.ux.CmsView; import org.argeo.api.cms.ux.UxContext; import org.argeo.cms.auth.CurrentUser; -import org.argeo.util.CurrentSubject; +import org.argeo.cms.util.CurrentSubject; import org.eclipse.swt.widgets.Display; public abstract class AbstractSwtCmsView implements CmsView { diff --git a/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebApp.java b/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebApp.java index 31555d168..67fa5ceac 100644 --- a/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebApp.java +++ b/swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebApp.java @@ -12,7 +12,7 @@ import org.argeo.api.cms.CmsLog; import org.argeo.api.cms.ux.CmsTheme; import org.argeo.api.cms.ux.CmsView; import org.argeo.cms.swt.CmsSwtUtils; -import org.argeo.util.LangUtils; +import org.argeo.cms.util.LangUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.application.Application; import org.eclipse.rap.rwt.application.Application.OperationMode; diff --git a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java index e94825db8..a83a54db3 100644 --- a/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java +++ b/swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java @@ -5,7 +5,7 @@ import java.lang.System.Logger.Level; import java.nio.file.Path; import org.argeo.api.cms.CmsApp; -import org.argeo.util.OS; +import org.argeo.cms.util.OS; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.widgets.Display; diff --git a/swt/rcp/org.argeo.swt.specific.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpResourceManager.java b/swt/rcp/org.argeo.swt.specific.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpResourceManager.java index ac4217184..47ff35dc0 100644 --- a/swt/rcp/org.argeo.swt.specific.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpResourceManager.java +++ b/swt/rcp/org.argeo.swt.specific.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpResourceManager.java @@ -7,7 +7,7 @@ import java.util.Collections; import java.util.Map; import java.util.TreeMap; -import org.argeo.util.StreamUtils; +import org.argeo.cms.util.StreamUtils; import org.eclipse.rap.rwt.service.ResourceManager; public class RcpResourceManager implements ResourceManager {