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 \
--- /dev/null
+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";
+
+}
// 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()), //
;
//
;
- 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;
@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;
}
}
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() {
--- /dev/null
+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<String> enumToDns(EnumSet<? extends Distinguished> enumSet) {
+ Set<String> res = new TreeSet<>();
+ for (Enum<? extends Distinguished> enm : enumSet) {
+ res.add(((Distinguished) enm).dn());
+ }
+ return res;
+ }
+}
--- /dev/null
+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:<br>
+ * - <a href= "https://www.ldap.com/ldap-oid-reference">Standard LDAP</a><br>
+ * - <a href=
+ * "https://github.com/krb5/krb5/blob/master/src/plugins/kdb/ldap/libkdb_ldap/kerberos.schema">Kerberos
+ * LDAP (partial)</a>
+ */
+public enum LdapAttrs implements QNamed, SpecifiedName, Supplier<String> {
+ /** */
+ 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;
+ }
+
+}
--- /dev/null
+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
+ * <a href="https://www.ldap.com/ldap-oid-reference">https://www.ldap.com/ldap-
+ * oid-reference</a>
+ */
+public enum LdapObjs implements QNamed, SpecifiedName, Supplier<String> {
+ 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;
+ }
+
+}
--- /dev/null
+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<String, List<String>> query, String key) {
+ if (!query.containsKey(key))
+ return null;
+ List<String> 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<String, List<String>> queryToMap(URI uri) {
+ return queryToMap(uri.getQuery());
+ }
+
+ private static Map<String, List<String>> queryToMap(String queryPart) {
+ try {
+ final Map<String, List<String>> query_pairs = new LinkedHashMap<String, List<String>>();
+ 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<String>());
+ }
+ 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");
+ }
+}
--- /dev/null
+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";
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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 <code>null</code> is none available. */
+ public List<TabularColumn> getColumns();
+
+ public TabularRowIterator read();
+}
--- /dev/null
+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();
+}
--- /dev/null
+package org.argeo.api.acr.tabular;
+
+import java.util.Iterator;
+
+/** Navigation of rows */
+public interface TabularRowIterator extends Iterator<TabularRow> {
+ /**
+ * 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();
+}
--- /dev/null
+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();
+}
--- /dev/null
+/** Tabular format API. */
+package org.argeo.api.acr.tabular;
\ No newline at end of file
--- /dev/null
+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<String> 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);
+}
--- /dev/null
+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() {
+ }
+}
--- /dev/null
+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 <code>null</code> if a
+ * {@link Directory}.
+ */
+ HierarchyUnit getParent();
+
+ /** Direct children {@link HierarchyUnit}s. */
+ Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly);
+
+ /**
+ * Whether this is an arbitrary named and placed {@link HierarchyUnit}.
+ *
+ * @return <code>true</code> if functional, <code>false</code> 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<String, Object> getProperties();
+}
--- /dev/null
+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);
+}
--- /dev/null
+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. <b>Experimental. This API may
+ * change.</b>
+ */
+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);
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+/** Argeo CMS reusable security components. */
+package org.argeo.api.cms.keyring;
\ No newline at end of file
--- /dev/null
+package org.argeo.api.cms.transaction;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class AbstractWorkingCopy<DATA, ATTR, ID> implements WorkingCopy<DATA, ATTR, ID> {
+ private Map<ID, DATA> newData = new HashMap<ID, DATA>();
+ private Map<ID, ATTR> modifiedData = new HashMap<ID, ATTR>();
+ private Map<ID, DATA> deletedData = new HashMap<ID, DATA>();
+
+ 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<ID, DATA> getNewData() {
+ return newData;
+ }
+
+ public Map<ID, DATA> getDeletedData() {
+ return deletedData;
+ }
+
+ public Map<ID, ATTR> getModifiedData() {
+ return modifiedData;
+ }
+
+}
--- /dev/null
+package org.argeo.api.cms.transaction;
+
+/** JTA transaction status. */
+public class JtaStatusAdapter implements TransactionStatusAdapter<Integer> {
+ 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;
+ }
+
+}
--- /dev/null
+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);
+ }
+
+}
--- /dev/null
+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<T>
+//implements Transaction, Status
+{
+ private final Xid xid;
+ private T status;
+ private final List<XAResource> xaResources = new ArrayList<XAResource>();
+
+ private final SimpleTransactionManager transactionManager;
+ private TransactionStatusAdapter<T> tsa;
+
+ public SimpleTransaction(SimpleTransactionManager transactionManager, TransactionStatusAdapter<T> 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;
+ }
+
+}
--- /dev/null
+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<SimpleTransaction<Integer>> current = new ThreadLocal<SimpleTransaction<Integer>>();
+
+ private Map<Xid, SimpleTransaction<Integer>> knownTransactions = Collections
+ .synchronizedMap(new HashMap<Xid, SimpleTransaction<Integer>>());
+ private TransactionStatusAdapter<Integer> tsa = new JtaStatusAdapter();
+// private SyncRegistry syncRegistry = new SyncRegistry();
+
+ /*
+ * WORK IMPLEMENTATION
+ */
+ @Override
+ public <T> T required(Callable<T> 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<Integer> transaction = new SimpleTransaction<Integer>(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<Integer> getTransaction()
+// throws SystemException
+ {
+ return getCurrent();
+ }
+
+ protected SimpleTransaction<Integer> getCurrent()
+// throws SystemException
+ {
+ SimpleTransaction<Integer> 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<Integer> 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<Integer> suspend()
+// throws SystemException
+ {
+ SimpleTransaction<Integer> 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);
+// }
+// }
+//
+// }
+}
--- /dev/null
+package org.argeo.api.cms.transaction;
+
+/** Abstract the various approaches to represent transaction status. */
+public interface TransactionStatusAdapter<T> {
+ T getActiveStatus();
+
+ T getPreparingStatus();
+
+ T getMarkedRollbackStatus();
+
+ T getPreparedStatus();
+
+ T getCommittingStatus();
+
+ T getCommittedStatus();
+
+ T getRollingBackStatus();
+
+ T getRolledBackStatus();
+
+ T getNoTransactionStatus();
+}
--- /dev/null
+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);
+ // }
+}
--- /dev/null
+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);
+}
--- /dev/null
+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> T required(Callable<T> work);
+
+ void setRollbackOnly();
+
+ WorkContext getWorkContext();
+}
--- /dev/null
+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();
+}
--- /dev/null
+package org.argeo.api.cms.transaction;
+
+import java.util.Map;
+
+public interface WorkingCopy<DATA, ATTR, ID> {
+ void startEditing(DATA user);
+
+ boolean noModifications();
+
+ void cleanUp();
+
+ Map<ID, DATA> getNewData();
+
+ Map<ID, DATA> getDeletedData();
+
+ Map<ID, ATTR> getModifiedData();
+
+}
--- /dev/null
+package org.argeo.api.cms.transaction;
+
+public interface WorkingCopyProcessor<WC extends WorkingCopy<?, ?, ?>> {
+ void prepare(WC wc);
+
+ void commit(WC wc);
+
+ void rollback(WC wc);
+
+ WC newWorkingCopy();
+}
--- /dev/null
+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<WC extends WorkingCopy<?, ?, ?>> implements XAResource {
+ private final WorkingCopyProcessor<WC> processor;
+
+ private Map<Xid, WC> workingCopies = new HashMap<Xid, WC>();
+ private Xid editingXid = null;
+ private int transactionTimeout = 0;
+
+ public WorkingCopyXaResource(WorkingCopyProcessor<WC> 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);
+ }
+
+}
--- /dev/null
+package org.argeo.api.cms.transaction;
+
+import javax.transaction.xa.XAResource;
+
+public interface XAResourceProvider {
+ XAResource getXaResource();
+}
--- /dev/null
+/** Minimalistic and partial XA transaction manager implementation. */
+package org.argeo.api.cms.transaction;
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
+ <attributes>
+ <attribute name="module" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.argeo.api.register</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ </natures>
+</projectDescription>
--- /dev/null
+/MANIFEST.MF
--- /dev/null
+Import-Package: org.osgi.*;version=0.0.0,\
+!org.apache.commons.logging,\
+*
--- /dev/null
+source.. = src/
\ No newline at end of file
--- /dev/null
+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<I> implements Supplier<I>, Comparable<Component<?>> {
+
+ private final I instance;
+
+ private final Runnable init;
+ private final Runnable close;
+
+ private final Map<Class<? super I>, PublishedType<? super I>> types;
+ private final Set<Dependency<?>> dependencies;
+ private final Map<String, Object> properties;
+
+ private CompletableFuture<Void> activationStarted = null;
+ private CompletableFuture<Void> activated = null;
+
+ private CompletableFuture<Void> deactivationStarted = null;
+ private CompletableFuture<Void> deactivated = null;
+
+ // internal
+ private Set<Dependency<?>> dependants = new HashSet<>();
+
+ private RankingKey rankingKey;
+
+ Component(ComponentRegister register, I instance, Runnable init, Runnable close, Set<Dependency<?>> dependencies,
+ Set<Class<? super I>> classes, Map<String, Object> 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<Class<? super I>, PublishedType<? super I>> types = new HashMap<>(classes.size());
+ for (Class<? super I> 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<String, Object> 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<Void>();
+ activated = activationStarted //
+ .thenComposeAsync(this::dependenciesActivated) //
+ .thenRun(this.init) //
+ .thenRun(() -> prepareNextDeactivation());
+ }
+
+ private void prepareNextDeactivation() {
+ deactivationStarted = new CompletableFuture<Void>();
+ deactivated = deactivationStarted //
+ .thenComposeAsync(this::dependantsDeactivated) //
+ .thenRun(this.close) //
+ .thenRun(() -> prepareNextActivation());
+ }
+
+ CompletableFuture<Void> getActivated() {
+ return activated;
+ }
+
+ CompletableFuture<Void> 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<Void> dependenciesActivated(Void v) {
+ Set<CompletableFuture<?>> constraints = new HashSet<>(this.dependencies.size());
+ for (Dependency<?> dependency : this.dependencies) {
+ CompletableFuture<Void> dependencyActivated = dependency.publisherActivated() //
+ .thenCompose(dependency::set);
+ constraints.add(dependencyActivated);
+ }
+ return CompletableFuture.allOf(constraints.toArray(new CompletableFuture[constraints.size()]));
+ }
+
+ CompletableFuture<Void> dependantsDeactivated(Void v) {
+ Set<CompletableFuture<?>> constraints = new HashSet<>(this.dependants.size());
+ for (Dependency<?> dependant : this.dependants) {
+ CompletableFuture<Void> dependantDeactivated = dependant.dependantDeactivated() //
+ .thenCompose(dependant::unset);
+ constraints.add(dependantDeactivated);
+ }
+ CompletableFuture<Void> 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 <T> PublishedType<T> getType(Class<T> clss) {
+ if (!types.containsKey(clss))
+ throw new IllegalArgumentException(clss.getName() + " is not a type published by this component");
+ return (PublishedType<T>) types.get(clss);
+ }
+
+ public <T> boolean isPublishedType(Class<T> clss) {
+ return types.containsKey(clss);
+ }
+
+ public Map<String, Object> 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<String> 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<T> {
+ private Component<? extends T> component;
+ private Class<T> clss;
+
+ private CompletableFuture<T> value;
+
+ public PublishedType(Component<? extends T> component, Class<T> clss) {
+ this.clss = clss;
+ this.component = component;
+ value = CompletableFuture.completedFuture((T) component.instance);
+ }
+
+ public Component<?> getPublisher() {
+ return component;
+ }
+
+ public Class<T> getType() {
+ return clss;
+ }
+
+ public CompletionStage<T> getValue() {
+ return value.minimalCompletionStage();
+ }
+ }
+
+ /** Builds a {@link Component}. */
+ public static class Builder<I> implements Supplier<I> {
+ private final I instance;
+
+ private Runnable init;
+ private Runnable close;
+
+ private Set<Dependency<?>> dependencies = new HashSet<>();
+ private Set<Class<? super I>> types = new HashSet<>();
+ private final Map<String, Object> properties = new HashMap<>();
+
+ public Builder(I instance) {
+ this.instance = instance;
+ }
+
+ public Component<I> build(ComponentRegister register) {
+ // default values
+ if (types.isEmpty()) {
+ types.add(getInstanceClass());
+ }
+
+ if (init == null)
+ init = () -> {
+ };
+ if (close == null)
+ close = () -> {
+ };
+
+ // instantiation
+ Component<I> component = new Component<I>(register, instance, init, close, dependencies, types, properties);
+ for (Dependency<?> dependency : dependencies) {
+ dependency.type.getPublisher().addDependant(dependency);
+ }
+ return component;
+ }
+
+ public Builder<I> addType(Class<? super I> clss) {
+ types.add(clss);
+ return this;
+ }
+
+ public Builder<I> addActivation(Runnable init) {
+ if (this.init != null)
+ throw new IllegalArgumentException("init method is already set");
+ this.init = init;
+ return this;
+ }
+
+ public Builder<I> addDeactivation(Runnable close) {
+ if (this.close != null)
+ throw new IllegalArgumentException("close method is already set");
+ this.close = close;
+ return this;
+ }
+
+ public <D> Builder<I> addDependency(PublishedType<D> type, Consumer<D> set, Consumer<D> unset) {
+ dependencies.add(new Dependency<D>(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<I> getInstanceClass() {
+ return (Class<I>) instance.getClass();
+ }
+
+ }
+
+ static class Dependency<D> {
+ private PublishedType<D> type;
+ private Consumer<D> set;
+ private Consumer<D> unset;
+
+ // live
+ Component<?> dependantComponent;
+ CompletableFuture<Void> setStage;
+ CompletableFuture<Void> unsetStage;
+
+ public Dependency(PublishedType<D> types, Consumer<D> set, Consumer<D> 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<Void> publisherActivated() {
+ return type.getPublisher().activated.copy();
+ }
+
+ CompletableFuture<Void> dependantDeactivated() {
+ return dependantComponent.deactivated.copy();
+ }
+
+ CompletableFuture<Void> set(Void v) {
+ return type.value.thenAccept(set);
+ }
+
+ CompletableFuture<Void> unset(Void v) {
+ return type.value.thenAccept(unset);
+ }
+
+ }
+}
--- /dev/null
+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);
+
+ <T> SortedSet<Component<? extends T>> find(Class<T> clss, Predicate<Map<String, Object>> filter);
+
+ default <T> Component.PublishedType<T> getSingleton(Class<T> type) {
+ SortedSet<Component<? extends T>> found = find(type, null);
+ if (found.size() == 0)
+ throw new IllegalStateException("No component found for " + type);
+ return found.first().getType(type);
+ }
+
+ default <T> T getObject(Class<T> clss) {
+ SortedSet<Component<? extends T>> found = find(clss, null);
+ if (found.size() == 0)
+ return null;
+ return found.first().get();
+ }
+
+ Component<?> get(Object instance);
+
+// default <T> PublishedType<T> getType(Class<T> clss) {
+// SortedSet<Component<? extends T>> components = find(clss, null);
+// if (components.size() == 0)
+// return null;
+// return components.first().getType(clss);
+// }
+
+ void activate();
+
+ void deactivate();
+
+ boolean isActive();
+
+ void clear();
+}
--- /dev/null
+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<RankingKey> {
+ 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<String, Object> 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);
+ }
+}
--- /dev/null
+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<Object, Component<?>> components = new IdentityHashMap<>();
+ private final AtomicLong nextServiceId = new AtomicLong(0l);
+
+ @Override
+ public long register(Component<?> component) {
+ return registerComponent(component);
+ }
+
+ @SuppressWarnings({ "unchecked" })
+ @Override
+ public synchronized <T> SortedSet<Component<? extends T>> find(Class<T> clss,
+ Predicate<Map<String, Object>> filter) {
+ SortedSet<Component<? extends T>> result = new TreeSet<>();
+ instances: for (Object instance : components.keySet()) {
+ if (!clss.isAssignableFrom(instance.getClass()))
+ continue instances;
+ Component<? extends T> component = (Component<? extends T>) 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<CompletableFuture<?>> 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<CompletableFuture<?>> 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();
+ }
+}
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,\
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;
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;
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;
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;
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;
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;
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;
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 {
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);
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) {
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="start" deactivate="stop" immediate="false" name="Node User Admin">
<implementation class="org.argeo.cms.internal.runtime.CmsUserAdmin"/>
<property name="service.pid" type="String" value="org.argeo.api.userAdmin"/>
- <reference bind="setTransactionManager" cardinality="1..1" interface="org.argeo.util.transaction.WorkControl" name="WorkControl" policy="static"/>
- <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.util.transaction.WorkTransaction" name="WorkTransaction" policy="static"/>
+ <reference bind="setTransactionManager" cardinality="1..1" interface="org.argeo.api.cms.transaction.WorkControl" name="WorkControl" policy="static"/>
+ <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.api.cms.transaction.WorkTransaction" name="WorkTransaction" policy="static"/>
<reference bind="setCmsState" cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
<service>
<provide interface="org.osgi.service.useradmin.UserAdmin"/>
<provide interface="org.argeo.cms.CmsUserManager"/>
</service>
<reference bind="setUserAdmin" cardinality="1..1" interface="org.osgi.service.useradmin.UserAdmin" name="UserAdmin" policy="static"/>
- <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.util.transaction.WorkTransaction" name="UserTransaction" policy="static"/>
+ <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.api.cms.transaction.WorkTransaction" name="UserTransaction" policy="static"/>
</scr:component>
<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="Simple Transaction Manager">
- <implementation class="org.argeo.util.transaction.SimpleTransactionManager"/>
+ <implementation class="org.argeo.api.cms.transaction.SimpleTransactionManager"/>
<service>
- <provide interface="org.argeo.util.transaction.WorkControl"/>
- <provide interface="org.argeo.util.transaction.WorkTransaction"/>
+ <provide interface="org.argeo.api.cms.transaction.WorkControl"/>
+ <provide interface="org.argeo.api.cms.transaction.WorkTransaction"/>
</service>
</scr:component>
--- /dev/null
+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<SecretKey> 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);
+ }
+
+ }
+}
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;
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<QName, Object> implements ProvidedContent {
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;
// 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()) {
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.
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),
//
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}. */
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
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;
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;
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;
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;
Dictionary<String, Object> properties = doGetProperties();
List<QName> 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) {
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;
}
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;
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 {
@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<String> 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) {
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 {
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;
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 {
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;
}
// 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);
}
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;
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());
}
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. */
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 {
private final ContentName name;
CmsRole() {
- name = new ContentName(CrName.ROLE_NAMESPACE_URI, QUALIFIER + name());
+ name = new ContentName(ArgeoNamespace.ROLE_NAMESPACE_URI, QUALIFIER + name());
}
@Override
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;
/**
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 {
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;
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. */
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. */
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;
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;
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;
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 {
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 {
package org.argeo.cms.dav;
-import org.argeo.util.http.HttpHeader;
+import org.argeo.cms.http.HttpHeader;
import com.sun.net.httpserver.HttpExchange;
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;
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 {
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
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<DavResponse> {
private BlockingQueue<DavResponse> queue = new ArrayBlockingQueue<>(64);
--- /dev/null
+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<String, Object> 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<String> credentialAttributeIds = Arrays
+ .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
+
+ private WorkControl transactionControl;
+ private WorkingCopyXaResource<LdapEntryWorkingCopy> xaResource;
+
+ private LdapDirectoryDao directoryDao;
+
+ /** Whether the the directory has is authenticated via a service user. */
+ private boolean authenticated = false;
+
+ public AbstractLdapDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
+ this.configProperties = new Hashtable<String, Object>();
+ for (Enumeration<String> 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<LdapEntry> 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<HierarchyUnit> 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<String, Object> getProperties() {
+ return asLdapEntry().getProperties();
+ }
+
+ /*
+ * ACCESSORS
+ */
+ @Override
+ public Optional<String> 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<String, Object> getConfigProperties() {
+// return configProperties;
+// }
+
+ public Dictionary<String, Object> cloneConfigProperties() {
+ return new Hashtable<>(configProperties);
+ }
+
+ public String getForcedPassword() {
+ return forcedPassword;
+ }
+
+ public boolean isScoped() {
+ return scoped;
+ }
+
+ public List<String> 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();
+ }
+
+}
--- /dev/null
+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);
+ }
+
+}
--- /dev/null
+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<String, Object> {
+ 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<String> keys() {
+ NamingEnumeration<String> namingEnumeration = attributes.getIDs();
+ return new Enumeration<String>() {
+
+ @Override
+ public boolean hasMoreElements() {
+ return namingEnumeration.hasMoreElements();
+ }
+
+ @Override
+ public String nextElement() {
+ return namingEnumeration.nextElement();
+ }
+
+ };
+ }
+
+ @Override
+ public Enumeration<Object> elements() {
+ NamingEnumeration<String> namingEnumeration = attributes.getIDs();
+ return new Enumeration<Object>() {
+
+ @Override
+ public boolean hasMoreElements() {
+ return namingEnumeration.hasMoreElements();
+ }
+
+ @Override
+ public Object nextElement() {
+ String key = namingEnumeration.nextElement();
+ return get(key);
+ }
+
+ };
+ }
+
+ @Override
+ /** @returns a <code>String</code> or <code>String[]</code> */
+ 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 <b>content</b> of an {@link Attributes} to the provided
+ * {@link Dictionary}.
+ */
+ public static void copy(Attributes attributes, Dictionary<String, Object> dictionary) {
+ AttributesDictionary ad = new AttributesDictionary(attributes);
+ Enumeration<String> 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<String, Object> dictionary, Attributes attributes) {
+ AttributesDictionary ad = new AttributesDictionary(attributes);
+ Enumeration<String> keys = dictionary.keys();
+ while (keys.hasMoreElements()) {
+ String key = keys.nextElement();
+ ad.put(key, dictionary.get(key));
+ }
+ }
+}
--- /dev/null
+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());
+ }
+ }
+
+}
--- /dev/null
+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<String> 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<LdapName> getReferences(String attributeId) {
+ Attribute memberAttribute = getAttributes().get(attributeId);
+ if (memberAttribute == null)
+ return new ArrayList<LdapName>();
+ try {
+ List<LdapName> roles = new ArrayList<LdapName>();
+ 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<String, Object> getProperties() {
+ if (properties == null)
+ properties = new AttributeDictionary(false);
+ return properties;
+ }
+
+ public Dictionary<String, Object> 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<String, Object> {
+ private final List<String> effectiveKeys = new ArrayList<String>();
+ private final List<String> attrFilter;
+ private final Boolean includeFilter;
+
+ public AttributeDictionary(Boolean credentials) {
+ this.attrFilter = getDirectory().getCredentialAttributeIds();
+ this.includeFilter = credentials;
+ try {
+ NamingEnumeration<String> 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<String> keys() {
+ return Collections.enumeration(effectiveKeys);
+ }
+
+ @Override
+ public Enumeration<Object> elements() {
+ final Iterator<String> it = effectiveKeys.iterator();
+ return new Enumeration<Object>() {
+
+ @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);
+ }
+ }
+
+ }
+
+}
--- /dev/null
+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<String, Object> 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<String, Object> 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<String> 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<String, Object> res = new Hashtable<>();
+ res.put(DirectoryConf.uri.name(), uriStr.toString());
+ addIpaConfig(kerberosRealm, res);
+ return res;
+ }
+}
--- /dev/null
+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<String, ?> properties) {
+ try {
+ Hashtable<String, Object> connEnv = new Hashtable<String, Object>();
+ 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<SearchResult> search(LdapName searchBase, String searchFilter,
+ SearchControls searchControls) throws NamingException {
+ NamingEnumeration<SearchResult> 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<?, ?, LdapName> 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);
+ }
+ }
+}
--- /dev/null
+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<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
+ ArrayList<LdapEntry> 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<SearchResult> 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<LdapName> getDirectGroups(LdapName dn) {
+ List<LdapName> directGroups = new ArrayList<LdapName>();
+ try {
+ String searchFilter = "(&(" + objectClass + "=" + getDirectory().getGroupObjectClass() + ")("
+ + getDirectory().getMemberAttributeId() + "=" + dn + "))";
+
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+
+ LdapName searchBase = getDirectory().getBaseDn();
+ NamingEnumeration<SearchResult> 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<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
+ List<HierarchyUnit> 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<SearchResult> 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);
+ }
+ }
+
+}
--- /dev/null
+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<LdapEntryWorkingCopy> {
+ boolean checkConnection();
+
+ boolean entryExists(LdapName dn);
+
+ LdapEntry doGetEntry(LdapName name) throws NameNotFoundException;
+
+ Attributes doGetAttributes(LdapName name);
+
+ List<LdapEntry> doGetEntries(LdapName searchBase, String filter, boolean deep);
+
+ List<LdapName> getDirectGroups(LdapName dn);
+
+ Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly);
+
+ HierarchyUnit doGetHierarchyUnit(LdapName dn);
+
+ LdapEntry newUser(LdapName name);
+
+ LdapEntry newGroup(LdapName name);
+
+ void init();
+
+ void destroy();
+}
--- /dev/null
+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<LdapName> getReferences(String attributeId);
+
+ Dictionary<String, Object> 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<String, Object> properties, Collection<String> objectClasses) {
+ String value = properties.get(LdapAttrs.objectClasses.name()).toString();
+ Set<String> 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<String, Object> 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();
+ }
+ }
+}
--- /dev/null
+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<LdapEntry, Attributes, LdapName> {
+ @Override
+ protected LdapName getId(LdapEntry entry) {
+ return entry.getDn();
+ }
+
+ @Override
+ protected Attributes cloneAttributes(LdapEntry entry) {
+ return (Attributes) entry.getAttributes().clone();
+ }
+}
--- /dev/null
+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<HierarchyUnit> 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();
+ }
+
+}
--- /dev/null
+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() {
+
+ }
+}
--- /dev/null
+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<LdapName, LdapEntry> entries = new TreeMap<>();
+ private NavigableMap<LdapName, LdapHierarchyUnit> hierarchy = new TreeMap<>();
+
+ private NavigableMap<LdapName, Attributes> 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<LdapName, Attributes> allEntries = ldifParser.read(in);
+ for (LdapName key : allEntries.keySet()) {
+ Attributes attributes = allEntries.get(key);
+ // check for inconsistency
+ Set<String> lowerCase = new HashSet<String>();
+ NamingEnumeration<String> 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<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
+ Objects.requireNonNull(searchBase);
+ ArrayList<LdapEntry> 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<LdapName, ? extends LdapEntry> map, LdapName searchBase, String f, boolean deep,
+ List<LdapEntry> 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<LdapName> getDirectGroups(LdapName dn) {
+ List<LdapName> directGroups = new ArrayList<LdapName>();
+ 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<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
+ List<HierarchyUnit> 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);
+ }
+}
--- /dev/null
+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<LdapName, Attributes> 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<LdapName, Attributes> 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<LdapName, Attributes> read(Reader reader) throws IOException {
+ SortedMap<LdapName, Attributes> res = new TreeMap<LdapName, Attributes>();
+ try {
+ List<String> 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
--- /dev/null
+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<? extends Attribute> 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<? extends Attribute> 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<LdapName, Attributes> 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<String> 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');
+ }
+ }
+}
--- /dev/null
+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());
+ }
+
+}
--- /dev/null
+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<String> dnsServerUrls) {
+ try {
+ Objects.requireNonNull(dnsServerUrls);
+ Hashtable<String, Object> 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<String, List<String>> getAllRecords(String name) {
+ try {
+ Map<String, List<String>> res = new TreeMap<>();
+ Attributes attrs = initialCtx.getAttributes(name);
+ NamingEnumeration<String> ids = attrs.getIDs();
+ while (ids.hasMore()) {
+ String recordType = ids.next();
+ List<String> lst = new ArrayList<String>();
+ 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<String> getRecords(String name, String recordType) {
+ try {
+ List<String> res = new ArrayList<String>();
+ 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<String> getSrvRecordsAsHosts(String name, boolean withPort) {
+ List<String> raw = getRecords(name, "SRV");
+ if (raw.size() == 0)
+ return null;
+ SortedSet<SrvRecord> 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<String> lst = new ArrayList<>();
+ for (SrvRecord order : res) {
+ lst.add(order.toHost(withPort));
+ }
+ return Collections.unmodifiableList(lst);
+ }
+
+ private void addValues(Attribute attr, List<String> 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<String> listEntries(String name) {
+ try {
+ List<String> res = new ArrayList<String>();
+ NamingEnumeration<Binding> 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<String, List<String>> 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 <hostname> [<record type> | *]");
+ }
+
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.cms.dns;
+
+class SrvRecord implements Comparable<SrvRecord> {
+ 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 : "");
+ }
+}
--- /dev/null
+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<Path>() {
+
+ @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);
+ }
+}
--- /dev/null
+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();
+ }
+
+}
--- /dev/null
+package org.argeo.cms.http;
+
+/** Generic HTTP methods. */
+public enum HttpMethod {
+ OPTIONS, //
+ HEAD, //
+ GET, //
+ POST, //
+ PUT, //
+ DELETE, //
+
+ // WebDav
+ PROPFIND, //
+ PROPPATCH, //
+ MKCOL, //
+ MOVE, //
+ COPY, //
+ ;
+}
--- /dev/null
+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() {
+
+ }
+}
--- /dev/null
+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;
+ }
+
+}
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;
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;
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;
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);
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;
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;
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.
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;
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;
--- /dev/null
+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<String, String> getDirectives() {
+ Map<String, String> directives = new HashMap<>();
+ directives.put(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter);
+ return directives;
+ }
+
+ @Override
+ public Map<String, Object> getAttributes() {
+ return new HashMap<>();
+ }
+
+}
--- /dev/null
+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<String> systemRoles;
+ private final Set<String> roles;
+
+ public AggregatingAuthorization(String name, String displayName, Set<String> systemRoles, String[] roles) {
+ this.name = new X500Principal(name).getName();
+ this.displayName = displayName;
+ this.systemRoles = Collections.unmodifiableSet(new HashSet<>(systemRoles));
+ Set<String> 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<String> res = new ArrayList<String>(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;
+ }
+
+}
--- /dev/null
+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<LdapName, DirectoryUserAdmin> businessRoles = new HashMap<LdapName, DirectoryUserAdmin>();
+
+ // 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<Role> res = new ArrayList<Role>();
+ 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<User> res = new ArrayList<User>();
+ 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<String> rawRoles = Arrays.asList(rawAuthorization.getRoles());
+ List<String> 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<String> sysRoles = new HashSet<String>();
+ 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<String> 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<DirectoryUserAdmin> 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<String, Object> currentState() {
+// Dictionary<String, Object> res = new Hashtable<String, Object>();
+// // 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<String, Object> 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<UserDirectory> getUserDirectories() {
+ TreeSet<UserDirectory> res = new TreeSet<>((o1, o2) -> o1.getBase().compareTo(o2.getBase()));
+ res.addAll(businessRoles.values());
+ res.add(systemRoles);
+ return res;
+ }
+
+}
--- /dev/null
+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<String, Object> 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<String, Object> 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;
+ }
+
+}
--- /dev/null
+package org.argeo.cms.osgi.useradmin;
+
+import org.osgi.service.useradmin.Group;
+
+/** A group in a user directroy. */
+interface DirectoryGroup extends Group, DirectoryUser {
+// List<LdapName> getMemberNames();
+}
--- /dev/null
+package org.argeo.cms.osgi.useradmin;
+
+import org.osgi.service.useradmin.User;
+
+/** A user in a user directory. */
+interface DirectoryUser extends User {
+}
--- /dev/null
+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<String, ?> props) {
+ this(uriArg, props, false);
+ }
+
+ public DirectoryUserAdmin(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
+ super(uriArg, props, scoped);
+ }
+
+ public DirectoryUserAdmin(Dictionary<String, ?> props) {
+ this(null, props);
+ }
+
+ /*
+ * ABSTRACT METHODS
+ */
+
+ protected Optional<DirectoryUserAdmin> 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<DirectoryUserAdmin> scopeLdap(User user) {
+ Dictionary<String, Object> credentials = user.getCredentials();
+ String username = (String) credentials.get(SHARED_STATE_USERNAME);
+ if (username == null)
+ username = user.getName();
+ Dictionary<String, Object> 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<DirectoryUserAdmin> scopeLdif(User user) {
+ Dictionary<String, Object> 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<String, Object> 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<Role> getAllRoles(DirectoryUser user) {
+ List<Role> allRoles = new ArrayList<Role>();
+ if (user != null) {
+ collectRoles((LdapEntry) user, allRoles);
+ allRoles.add(user);
+ } else
+ collectAnonymousRoles(allRoles);
+ return allRoles;
+ }
+
+ private void collectRoles(LdapEntry user, List<Role> allRoles) {
+ List<LdapEntry> 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<Role> 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<? extends Role> res = getRoles(getBaseDn(), filter, true);
+ return res.toArray(new Role[res.size()]);
+ }
+
+ List<DirectoryUser> getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException {
+ LdapEntryWorkingCopy wc = getWorkingCopy();
+// Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
+ List<LdapEntry> searchRes = getDirectoryDao().doGetEntries(searchBase, filter, deep);
+ List<DirectoryUser> res = new ArrayList<>();
+ for (LdapEntry entry : searchRes)
+ res.add((DirectoryUser) entry);
+ if (wc != null) {
+ for (Iterator<DirectoryUser> 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<DirectoryUser> collectedUsers = new ArrayList<DirectoryUser>();
+ 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<DirectoryUser> collectedUsers) {
+ String f = "(" + key + "=" + value + ")";
+ List<LdapEntry> 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<Role> allRoles = new ArrayList<Role>();
+ 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<? extends Role> 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);
+ }
+ }
+}
--- /dev/null
+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<String> allRoles;
+
+ public LdifAuthorization(User user, List<Role> 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<String, Object> 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();
+ }
+}
--- /dev/null
+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<Role> directMembers = new ArrayList<Role>();
+ 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 <code>null</code>.
+ */
+ 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<LdapName> getMemberNames() {
+// Attribute memberAttribute = getAttributes().get(memberAttributeId);
+// if (memberAttribute == null)
+// return new ArrayList<LdapName>();
+// try {
+// List<LdapName> roles = new ArrayList<LdapName>();
+// 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();
+ }
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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<LdapName> 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<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
+ List<LdapEntry> 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<HierarchyUnit> 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);
+ }
+ }
+
+}
--- /dev/null
+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() {
+ }
+}
--- /dev/null
+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<String> tokensUsed(Subject subject, String tokensBaseDn) {
+ Set<String> 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();
+// }
+
+}
--- /dev/null
+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<? extends Role> getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep);
+
+ String getRolePath(Role role);
+
+ String getRoleSimpleName(Role role);
+
+ Role getRoleByPath(String path);
+}
--- /dev/null
+USER_NIX {
+ com.sun.security.auth.module.UnixLoginModule requisite;
+};
+
+USER_NT {
+ com.sun.security.auth.module.NTLoginModule requisite;
+};
+
--- /dev/null
+/** LDAP and LDIF based OSGi useradmin implementation. */
+package org.argeo.cms.osgi.useradmin;
\ No newline at end of file
--- /dev/null
+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<String, ?> properties) {
+ Object res = getRawValue(properties);
+ if (res == null)
+ return null;
+ return res.toString();
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T getRawValue(Dictionary<String, ?> 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<String, ?> properties) {
+ StringBuilder query = new StringBuilder();
+
+ boolean first = true;
+// for (Enumeration<String> 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<String, Object> uriAsProperties(String uriStr) {
+ try {
+ Hashtable<String, Object> res = new Hashtable<String, Object>();
+ 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<String, List<String>> query = NamingUtils.queryToMap(u);
+ for (String key : query.keySet()) {
+ DirectoryConf ldapProp = DirectoryConf.valueOf(key);
+ List<String> 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<String, Object> properties) {
+ String bDn = (String) properties.get(baseDn.name());
+ if (bDn == null)
+ throw new IllegalStateException("No baseDn in " + properties);
+ return DirectoryDigestUtils.sha1str(bDn);
+ }
+}
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;
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;
/**
+++ /dev/null
-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<SecretKey> 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);
- }
-
- }
-}
+++ /dev/null
-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<Path>() {
-
- @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);
- }
-}
+++ /dev/null
-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);
-}
+++ /dev/null
-package org.argeo.cms.security;
-
-import java.io.InputStream;
-
-/**
- * Access to private (typically encrypted) data. The keyring is responsible for
- * retrieving the necessary credentials. <b>Experimental. This API may
- * change.</b>
- */
-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);
-}
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-/** Argeo CMS reusable security components. */
-package org.argeo.cms.security;
\ No newline at end of file
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-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 <code>null</code> is none available. */
- public List<TabularColumn> getColumns();
-
- public TabularRowIterator read();
-}
+++ /dev/null
-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();
-}
+++ /dev/null
-package org.argeo.cms.tabular;
-
-import java.util.Iterator;
-
-/** Navigation of rows */
-public interface TabularRowIterator extends Iterator<TabularRow> {
- /**
- * 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();
-}
+++ /dev/null
-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();
-}
+++ /dev/null
-/** Tabular format API. */
-package org.argeo.cms.tabular;
\ No newline at end of file
--- /dev/null
+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<String> 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());
+ }
+}
--- /dev/null
+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<String> header, List<String> 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<String> header = null;
+ if (!noHeader) {
+ String headerStr = bufferedReader.readLine();
+ if (headerStr == null)// empty file
+ return;
+ lineCount++;
+ header = new ArrayList<String>();
+ 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<String> tokens = new ArrayList<String>();
+ 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<String> 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;
+ }
+
+}
--- /dev/null
+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<String, String> line);
+
+ protected final void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
+ if (header == null)
+ throw new IllegalArgumentException("Only CSV with header is supported");
+ Map<String, String> line = new HashMap<String, String>();
+ 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);
+ }
+
+}
--- /dev/null
+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;
+ }
+
+}
--- /dev/null
+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<Subject> 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> T callAs(Subject subject, Callable<T> 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<T>() {
+
+ @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() {
+ }
+
+}
--- /dev/null
+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<String> {
+ private final Dictionary<String, ?> dictionary;
+
+ public DictionaryKeys(Dictionary<String, ?> dictionary) {
+ this.dictionary = dictionary;
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return new KeyIterator(dictionary.keys());
+ }
+
+ private static class KeyIterator implements Iterator<String> {
+ private final Enumeration<String> keys;
+
+ KeyIterator(Enumeration<String> keys) {
+ this.keys = keys;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return keys.hasMoreElements();
+ }
+
+ @Override
+ public String next() {
+ return keys.nextElement();
+ }
+
+ }
+}
--- /dev/null
+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: <file> [<algorithm>]" + " (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);
+ }
+
+}
--- /dev/null
+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<Path> files = Files.newDirectoryStream(dir)) {
+ List<byte[]> hs = new ArrayList<byte[]>();
+ List<String> 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();
+ }
+ }
+}
--- /dev/null
+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<SystemException> 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<SystemException> getExceptions() {
+ return exceptions;
+ }
+
+ public void setExceptions(List<SystemException> exceptions) {
+ this.exceptions = exceptions;
+ }
+
+ /** An exception in the chain. */
+ public static class SystemException {
+ private String type;
+ private String message;
+ private List<String> 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<String> getStackTrace() {
+ return stackTrace;
+ }
+
+ public void setStackTrace(List<String> stackTrace) {
+ this.stackTrace = stackTrace;
+ }
+
+ @Override
+ public String toString() {
+ return "System exception: " + type + ", " + message + ", " + stackTrace;
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ return exceptions.toString();
+ }
+}
--- /dev/null
+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<Path>() {
+
+ @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<Path>() {
+ @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() {
+ }
+
+}
--- /dev/null
+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<String, Object> map(String key, Object value) {
+// assert key != null;
+// HashMap<String, Object> 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<String, Object> dict(String key, Object value) {
+ assert key != null;
+ Hashtable<String, Object> props = new Hashtable<>();
+ if (value != null)
+ props.put(key, value);
+ return props;
+ }
+
+ /** @deprecated Use {@link #dict(String, Object)} instead. */
+ @Deprecated
+ public static Dictionary<String, Object> dico(String key, Object value) {
+ return dict(key, value);
+ }
+
+ /** Converts a {@link Dictionary} to a {@link Map} of strings. */
+ public static Map<String, String> dictToStringMap(Dictionary<String, ?> properties) {
+ if (properties == null) {
+ return null;
+ }
+ Map<String, String> res = new HashMap<>(properties.size());
+ Enumeration<String> 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<String, Object> dictToMap(Dictionary<String, ?> properties) {
+ if (properties == null) {
+ return null;
+ }
+ Map<String, Object> res = new HashMap<>(properties.size());
+ Enumeration<String> 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
+ * <code>null</code> if not found.
+ */
+ public static String get(Map<String, ?> 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<String, ?> 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<String> keys(Dictionary<String, ?> props) {
+ assert props != null;
+ return new DictionaryKeys(props);
+ }
+
+ static String toJson(Dictionary<String, ?> props) {
+ return toJson(props, false);
+ }
+
+ static String toJson(Dictionary<String, ?> props, boolean pretty) {
+ StringBuilder sb = new StringBuilder();
+ sb.append('{');
+ if (pretty)
+ sb.append('\n');
+ Enumeration<String> 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<String, Object> props, Path path) throws IOException {
+ if (props == null)
+ throw new IllegalArgumentException("Props cannot be null");
+ Properties toStore = new Properties();
+ for (Enumeration<String> 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<String, Object> 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<String> 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<String, Object> loadFromProperties(Path path) throws IOException {
+ Properties toLoad = new Properties();
+ try (InputStream in = Files.newInputStream(path)) {
+ toLoad.load(in);
+ }
+ Dictionary<String, Object> res = new Hashtable<String, Object>();
+ 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 <code>null</code>.
+ */
+ public static List<String> toStringList(Object value) {
+ List<String> 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> T getAt(Iterable<T> iterable, int index) {
+ if (iterable instanceof List) {
+ List<T> lst = ((List<T>) 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<T> 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, <b>starts with a line
+ * return</b>) 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() {
+
+ }
+
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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<Integer> read(ByteBuffer dst) {
+ return executor.submit(() -> in.read(dst));
+ }
+
+ @Override
+ public <A> void read(ByteBuffer dst, A attachment, CompletionHandler<Integer, ? super A> handler) {
+ try {
+ Future<Integer> res = read(dst);
+ handler.completed(res.get(), attachment);
+ } catch (Exception e) {
+ handler.failed(e, attachment);
+ }
+ }
+
+ @Override
+ public Future<Integer> write(ByteBuffer src) {
+ return executor.submit(() -> out.write(src));
+ }
+
+ @Override
+ public <A> void write(ByteBuffer src, A attachment, CompletionHandler<Integer, ? super A> handler) {
+ try {
+ Future<Integer> 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;
+ }
+
+}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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<String, TesterStatus> 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<Method> 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<Method> findMethods(Class<?> clss) {
+ List<Method> methods = new ArrayList<Method>();
+// 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<String, TesterStatus> 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]";
+
+ }
+}
--- /dev/null
+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");
+ }
+
+}
--- /dev/null
+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 <value>/<unit>, 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;
+ }
+
+}
--- /dev/null
+/** Generic Java utilities. */
+package org.argeo.cms.util;
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<classpath>
- <classpathentry kind="src" path="src"/>
- <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
- <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
- <attributes>
- <attribute name="module" value="true"/>
- </attributes>
- </classpathentry>
- <classpathentry kind="output" path="bin"/>
-</classpath>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
- <name>org.argeo.util</name>
- <comment></comment>
- <projects>
- </projects>
- <buildSpec>
- <buildCommand>
- <name>org.eclipse.jdt.core.javabuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- <buildCommand>
- <name>org.eclipse.pde.ManifestBuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- <buildCommand>
- <name>org.eclipse.pde.SchemaBuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- </buildSpec>
- <natures>
- <nature>org.eclipse.jdt.core.javanature</nature>
- <nature>org.eclipse.pde.PluginNature</nature>
- </natures>
-</projectDescription>
+++ /dev/null
-/MANIFEST.MF
+++ /dev/null
-Bundle-Activator: org.argeo.osgi.internal.EnterpriseActivator
-Bundle-ActivationPolicy: lazy
-
-Import-Package: org.osgi.*;version=0.0.0,\
-!org.apache.commons.logging,\
-*
+++ /dev/null
-source.. = src/
\ No newline at end of file
+++ /dev/null
-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 {
- }
-
-}
+++ /dev/null
-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<String, Object> map = Collections.synchronizedSortedMap(new TreeMap<>());
-
- public SimpleProvisioningService() {
- // update count
- map.put(PROVISIONING_UPDATE_COUNT, 0);
- }
-
- @Override
- public Dictionary<String, Object> 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<String> 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<String, Object> {
-
- @Override
- public int size() {
- return map.size();
- }
-
- @Override
- public boolean isEmpty() {
- return map.isEmpty();
- }
-
- @Override
- public Enumeration<String> keys() {
- Iterator<String> it = map.keySet().iterator();
- return new Enumeration<String>() {
-
- @Override
- public boolean hasMoreElements() {
- return it.hasNext();
- }
-
- @Override
- public String nextElement() {
- return it.next();
- }
-
- };
- }
-
- @Override
- public Enumeration<Object> elements() {
- Iterator<Object> it = map.values().iterator();
- return new Enumeration<Object>() {
-
- @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();
- }
-
- }
-}
+++ /dev/null
-/** OSGi provisioning support. */
-package org.argeo.osgi.provisioning;
\ No newline at end of file
+++ /dev/null
-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<String> systemRoles;
- private final Set<String> roles;
-
- public AggregatingAuthorization(String name, String displayName, Set<String> systemRoles, String[] roles) {
- this.name = new X500Principal(name).getName();
- this.displayName = displayName;
- this.systemRoles = Collections.unmodifiableSet(new HashSet<>(systemRoles));
- Set<String> 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<String> res = new ArrayList<String>(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;
- }
-
-}
+++ /dev/null
-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<LdapName, DirectoryUserAdmin> businessRoles = new HashMap<LdapName, DirectoryUserAdmin>();
-
- // 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<Role> res = new ArrayList<Role>();
- 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<User> res = new ArrayList<User>();
- 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<String> rawRoles = Arrays.asList(rawAuthorization.getRoles());
- List<String> 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<String> sysRoles = new HashSet<String>();
- 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<String> 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<DirectoryUserAdmin> 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<String, Object> currentState() {
-// Dictionary<String, Object> res = new Hashtable<String, Object>();
-// // 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<String, Object> 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<UserDirectory> getUserDirectories() {
- TreeSet<UserDirectory> res = new TreeSet<>((o1, o2) -> o1.getBase().compareTo(o2.getBase()));
- res.addAll(businessRoles.values());
- res.add(systemRoles);
- return res;
- }
-
-}
+++ /dev/null
-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<String, Object> 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<String, Object> 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;
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import org.osgi.service.useradmin.Group;
-
-/** A group in a user directroy. */
-interface DirectoryGroup extends Group, DirectoryUser {
-// List<LdapName> getMemberNames();
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import org.osgi.service.useradmin.User;
-
-/** A user in a user directory. */
-interface DirectoryUser extends User {
-}
+++ /dev/null
-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<String, ?> props) {
- this(uriArg, props, false);
- }
-
- public DirectoryUserAdmin(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
- super(uriArg, props, scoped);
- }
-
- public DirectoryUserAdmin(Dictionary<String, ?> props) {
- this(null, props);
- }
-
- /*
- * ABSTRACT METHODS
- */
-
- protected Optional<DirectoryUserAdmin> 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<DirectoryUserAdmin> scopeLdap(User user) {
- Dictionary<String, Object> credentials = user.getCredentials();
- String username = (String) credentials.get(SHARED_STATE_USERNAME);
- if (username == null)
- username = user.getName();
- Dictionary<String, Object> 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<DirectoryUserAdmin> scopeLdif(User user) {
- Dictionary<String, Object> 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<String, Object> 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<Role> getAllRoles(DirectoryUser user) {
- List<Role> allRoles = new ArrayList<Role>();
- if (user != null) {
- collectRoles((LdapEntry) user, allRoles);
- allRoles.add(user);
- } else
- collectAnonymousRoles(allRoles);
- return allRoles;
- }
-
- private void collectRoles(LdapEntry user, List<Role> allRoles) {
- List<LdapEntry> 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<Role> 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<? extends Role> res = getRoles(getBaseDn(), filter, true);
- return res.toArray(new Role[res.size()]);
- }
-
- List<DirectoryUser> getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException {
- LdapEntryWorkingCopy wc = getWorkingCopy();
-// Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
- List<LdapEntry> searchRes = getDirectoryDao().doGetEntries(searchBase, filter, deep);
- List<DirectoryUser> res = new ArrayList<>();
- for (LdapEntry entry : searchRes)
- res.add((DirectoryUser) entry);
- if (wc != null) {
- for (Iterator<DirectoryUser> 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<DirectoryUser> collectedUsers = new ArrayList<DirectoryUser>();
- 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<DirectoryUser> collectedUsers) {
- String f = "(" + key + "=" + value + ")";
- List<LdapEntry> 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<Role> allRoles = new ArrayList<Role>();
- 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<? extends Role> 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);
- }
- }
-}
+++ /dev/null
-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<String> allRoles;
-
- public LdifAuthorization(User user, List<Role> 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<String, Object> 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();
- }
-}
+++ /dev/null
-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<Role> directMembers = new ArrayList<Role>();
- 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 <code>null</code>.
- */
- 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<LdapName> getMemberNames() {
-// Attribute memberAttribute = getAttributes().get(memberAttributeId);
-// if (memberAttribute == null)
-// return new ArrayList<LdapName>();
-// try {
-// List<LdapName> roles = new ArrayList<LdapName>();
-// 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();
- }
-}
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-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<LdapName> 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<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
- List<LdapEntry> 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<HierarchyUnit> 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);
- }
- }
-
-}
+++ /dev/null
-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() {
- }
-}
+++ /dev/null
-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<String> tokensUsed(Subject subject, String tokensBaseDn) {
- Set<String> 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();
-// }
-
-}
+++ /dev/null
-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<? extends Role> getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep);
-
- String getRolePath(Role role);
-
- String getRoleSimpleName(Role role);
-
- Role getRoleByPath(String path);
-}
+++ /dev/null
-USER_NIX {
- com.sun.security.auth.module.UnixLoginModule requisite;
-};
-
-USER_NT {
- com.sun.security.auth.module.NTLoginModule requisite;
-};
-
+++ /dev/null
-/** LDAP and LDIF based OSGi useradmin implementation. */
-package org.argeo.osgi.useradmin;
\ No newline at end of file
+++ /dev/null
-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<String, String> getDirectives() {
- Map<String, String> directives = new HashMap<>();
- directives.put(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter);
- return directives;
- }
-
- @Override
- public Map<String, Object> getAttributes() {
- return new HashMap<>();
- }
-
-}
+++ /dev/null
-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<R> implements Future<R> {
- private BundleContext ownBundleContext = FrameworkUtil.getBundle(OnServiceRegistration.class).getBundleContext();
-
- private ServiceTracker<?, ?> st;
-
- private R result;
- private boolean cancelled = false;
- private Throwable exception;
-
- public <T> OnServiceRegistration(Class<T> clss, Function<T, R> function) {
- this(null, clss, function);
- }
-
- public <T> OnServiceRegistration(BundleContext bundleContext, Class<T> clss, Function<T, R> function) {
- st = new ServiceTracker<T, T>(bundleContext != null ? bundleContext : ownBundleContext, clss, null) {
-
- @Override
- public T addingService(ServiceReference<T> 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;
- }
-
-}
+++ /dev/null
-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<Void> shutdownStarting = new CompletableFuture<Void>();
-
- public OsgiRegister(BundleContext bundleContext) {
- this.bundleContext = bundleContext;
- // TODO experiment with dedicated executors
- this.executor = ForkJoinPool.commonPool();
- }
-
- public <T> void set(T obj, Class<T> clss, Map<String, Object> attributes, Class<?>... classes) {
- CompletableFuture<ServiceRegistration<?>> srf = new CompletableFuture<ServiceRegistration<?>>();
- CompletableFuture<T> postRegistration = CompletableFuture.supplyAsync(() -> {
- List<String> 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<String, Object>(attributes));
- srf.complete(sr);
- return obj;
- }, executor);
-// Singleton<T> singleton = new Singleton<T>(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);
- }
-}
+++ /dev/null
-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<String> 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());
- }
-}
+++ /dev/null
-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<String> header, List<String> 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<String> header = null;
- if (!noHeader) {
- String headerStr = bufferedReader.readLine();
- if (headerStr == null)// empty file
- return;
- lineCount++;
- header = new ArrayList<String>();
- 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<String> tokens = new ArrayList<String>();
- 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<String> 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;
- }
-
-}
+++ /dev/null
-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<String, String> line);
-
- protected final void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
- if (header == null)
- throw new IllegalArgumentException("Only CSV with header is supported");
- Map<String, String> line = new HashMap<String, String>();
- 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);
- }
-
-}
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-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<Subject> 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> T callAs(Subject subject, Callable<T> 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<T>() {
-
- @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() {
- }
-
-}
+++ /dev/null
-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<String> {
- private final Dictionary<String, ?> dictionary;
-
- public DictionaryKeys(Dictionary<String, ?> dictionary) {
- this.dictionary = dictionary;
- }
-
- @Override
- public Iterator<String> iterator() {
- return new KeyIterator(dictionary.keys());
- }
-
- private static class KeyIterator implements Iterator<String> {
- private final Enumeration<String> keys;
-
- KeyIterator(Enumeration<String> keys) {
- this.keys = keys;
- }
-
- @Override
- public boolean hasNext() {
- return keys.hasMoreElements();
- }
-
- @Override
- public String next() {
- return keys.nextElement();
- }
-
- }
-}
+++ /dev/null
-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: <file> [<algorithm>]" + " (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);
- }
-
-}
+++ /dev/null
-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<Path> files = Files.newDirectoryStream(dir)) {
- List<byte[]> hs = new ArrayList<byte[]>();
- List<String> 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();
- }
- }
-}
+++ /dev/null
-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<SystemException> 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<SystemException> getExceptions() {
- return exceptions;
- }
-
- public void setExceptions(List<SystemException> exceptions) {
- this.exceptions = exceptions;
- }
-
- /** An exception in the chain. */
- public static class SystemException {
- private String type;
- private String message;
- private List<String> 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<String> getStackTrace() {
- return stackTrace;
- }
-
- public void setStackTrace(List<String> stackTrace) {
- this.stackTrace = stackTrace;
- }
-
- @Override
- public String toString() {
- return "System exception: " + type + ", " + message + ", " + stackTrace;
- }
-
- }
-
- @Override
- public String toString() {
- return exceptions.toString();
- }
-}
+++ /dev/null
-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<Path>() {
-
- @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<Path>() {
- @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() {
- }
-
-}
+++ /dev/null
-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<String, Object> map(String key, Object value) {
-// assert key != null;
-// HashMap<String, Object> 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<String, Object> dict(String key, Object value) {
- assert key != null;
- Hashtable<String, Object> props = new Hashtable<>();
- if (value != null)
- props.put(key, value);
- return props;
- }
-
- /** @deprecated Use {@link #dict(String, Object)} instead. */
- @Deprecated
- public static Dictionary<String, Object> dico(String key, Object value) {
- return dict(key, value);
- }
-
- /** Converts a {@link Dictionary} to a {@link Map} of strings. */
- public static Map<String, String> dictToStringMap(Dictionary<String, ?> properties) {
- if (properties == null) {
- return null;
- }
- Map<String, String> res = new HashMap<>(properties.size());
- Enumeration<String> 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<String, Object> dictToMap(Dictionary<String, ?> properties) {
- if (properties == null) {
- return null;
- }
- Map<String, Object> res = new HashMap<>(properties.size());
- Enumeration<String> 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
- * <code>null</code> if not found.
- */
- public static String get(Map<String, ?> 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<String, ?> 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<String> keys(Dictionary<String, ?> props) {
- assert props != null;
- return new DictionaryKeys(props);
- }
-
- static String toJson(Dictionary<String, ?> props) {
- return toJson(props, false);
- }
-
- static String toJson(Dictionary<String, ?> props, boolean pretty) {
- StringBuilder sb = new StringBuilder();
- sb.append('{');
- if (pretty)
- sb.append('\n');
- Enumeration<String> 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<String, Object> props, Path path) throws IOException {
- if (props == null)
- throw new IllegalArgumentException("Props cannot be null");
- Properties toStore = new Properties();
- for (Enumeration<String> 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<String, Object> 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<String> 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<String, Object> loadFromProperties(Path path) throws IOException {
- Properties toLoad = new Properties();
- try (InputStream in = Files.newInputStream(path)) {
- toLoad.load(in);
- }
- Dictionary<String, Object> res = new Hashtable<String, Object>();
- 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 <code>null</code>.
- */
- public static List<String> toStringList(Object value) {
- List<String> 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> T getAt(Iterable<T> iterable, int index) {
- if (iterable instanceof List) {
- List<T> lst = ((List<T>) 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<T> 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, <b>starts with a line
- * return</b>) 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() {
-
- }
-
-}
+++ /dev/null
-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;
- }
-}
+++ /dev/null
-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;
- }
-}
+++ /dev/null
-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<Integer> read(ByteBuffer dst) {
- return executor.submit(() -> in.read(dst));
- }
-
- @Override
- public <A> void read(ByteBuffer dst, A attachment, CompletionHandler<Integer, ? super A> handler) {
- try {
- Future<Integer> res = read(dst);
- handler.completed(res.get(), attachment);
- } catch (Exception e) {
- handler.failed(e, attachment);
- }
- }
-
- @Override
- public Future<Integer> write(ByteBuffer src) {
- return executor.submit(() -> out.write(src));
- }
-
- @Override
- public <A> void write(ByteBuffer src, A attachment, CompletionHandler<Integer, ? super A> handler) {
- try {
- Future<Integer> 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;
- }
-
-}
+++ /dev/null
-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();
- }
-}
+++ /dev/null
-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<String, TesterStatus> 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<Method> 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<Method> findMethods(Class<?> clss) {
- List<Method> methods = new ArrayList<Method>();
-// 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<String, TesterStatus> 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]";
-
- }
-}
+++ /dev/null
-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");
- }
-
-}
+++ /dev/null
-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 <value>/<unit>, 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;
- }
-
-}
+++ /dev/null
-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<String> 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);
-}
+++ /dev/null
-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<String, ?> properties) {
- Object res = getRawValue(properties);
- if (res == null)
- return null;
- return res.toString();
- }
-
- @SuppressWarnings("unchecked")
- public <T> T getRawValue(Dictionary<String, ?> 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<String, ?> properties) {
- StringBuilder query = new StringBuilder();
-
- boolean first = true;
-// for (Enumeration<String> 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<String, Object> uriAsProperties(String uriStr) {
- try {
- Hashtable<String, Object> res = new Hashtable<String, Object>();
- 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<String, List<String>> query = NamingUtils.queryToMap(u);
- for (String key : query.keySet()) {
- DirectoryConf ldapProp = DirectoryConf.valueOf(key);
- List<String> 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<String, Object> properties) {
- String bDn = (String) properties.get(baseDn.name());
- if (bDn == null)
- throw new IllegalStateException("No baseDn in " + properties);
- return DirectoryDigestUtils.sha1str(bDn);
- }
-}
+++ /dev/null
-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() {
- }
-}
+++ /dev/null
-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 <code>null</code> if a
- * {@link Directory}.
- */
- HierarchyUnit getParent();
-
- /** Direct children {@link HierarchyUnit}s. */
- Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly);
-
- /**
- * Whether this is an arbitrary named and placed {@link HierarchyUnit}.
- *
- * @return <code>true</code> if functional, <code>false</code> 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<String, Object> getProperties();
-}
+++ /dev/null
-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<String, Object> 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<String> credentialAttributeIds = Arrays
- .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
-
- private WorkControl transactionControl;
- private WorkingCopyXaResource<LdapEntryWorkingCopy> xaResource;
-
- private LdapDirectoryDao directoryDao;
-
- /** Whether the the directory has is authenticated via a service user. */
- private boolean authenticated = false;
-
- public AbstractLdapDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
- this.configProperties = new Hashtable<String, Object>();
- for (Enumeration<String> 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<LdapEntry> 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<HierarchyUnit> 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<String, Object> getProperties() {
- return asLdapEntry().getProperties();
- }
-
- /*
- * ACCESSORS
- */
- @Override
- public Optional<String> 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<String, Object> getConfigProperties() {
-// return configProperties;
-// }
-
- public Dictionary<String, Object> cloneConfigProperties() {
- return new Hashtable<>(configProperties);
- }
-
- public String getForcedPassword() {
- return forcedPassword;
- }
-
- public boolean isScoped() {
- return scoped;
- }
-
- public List<String> 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();
- }
-
-}
+++ /dev/null
-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);
- }
-
-}
+++ /dev/null
-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<String, Object> {
- 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<String> keys() {
- NamingEnumeration<String> namingEnumeration = attributes.getIDs();
- return new Enumeration<String>() {
-
- @Override
- public boolean hasMoreElements() {
- return namingEnumeration.hasMoreElements();
- }
-
- @Override
- public String nextElement() {
- return namingEnumeration.nextElement();
- }
-
- };
- }
-
- @Override
- public Enumeration<Object> elements() {
- NamingEnumeration<String> namingEnumeration = attributes.getIDs();
- return new Enumeration<Object>() {
-
- @Override
- public boolean hasMoreElements() {
- return namingEnumeration.hasMoreElements();
- }
-
- @Override
- public Object nextElement() {
- String key = namingEnumeration.nextElement();
- return get(key);
- }
-
- };
- }
-
- @Override
- /** @returns a <code>String</code> or <code>String[]</code> */
- 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 <b>content</b> of an {@link Attributes} to the provided
- * {@link Dictionary}.
- */
- public static void copy(Attributes attributes, Dictionary<String, Object> dictionary) {
- AttributesDictionary ad = new AttributesDictionary(attributes);
- Enumeration<String> 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<String, Object> dictionary, Attributes attributes) {
- AttributesDictionary ad = new AttributesDictionary(attributes);
- Enumeration<String> keys = dictionary.keys();
- while (keys.hasMoreElements()) {
- String key = keys.nextElement();
- ad.put(key, dictionary.get(key));
- }
- }
-}
+++ /dev/null
-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());
- }
- }
-
-}
+++ /dev/null
-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<String> 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<LdapName> getReferences(String attributeId) {
- Attribute memberAttribute = getAttributes().get(attributeId);
- if (memberAttribute == null)
- return new ArrayList<LdapName>();
- try {
- List<LdapName> roles = new ArrayList<LdapName>();
- 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<String, Object> getProperties() {
- if (properties == null)
- properties = new AttributeDictionary(false);
- return properties;
- }
-
- public Dictionary<String, Object> 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<String, Object> {
- private final List<String> effectiveKeys = new ArrayList<String>();
- private final List<String> attrFilter;
- private final Boolean includeFilter;
-
- public AttributeDictionary(Boolean credentials) {
- this.attrFilter = getDirectory().getCredentialAttributeIds();
- this.includeFilter = credentials;
- try {
- NamingEnumeration<String> 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<String> keys() {
- return Collections.enumeration(effectiveKeys);
- }
-
- @Override
- public Enumeration<Object> elements() {
- final Iterator<String> it = effectiveKeys.iterator();
- return new Enumeration<Object>() {
-
- @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);
- }
- }
-
- }
-
-}
+++ /dev/null
-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<String, Object> 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<String, Object> 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<String> 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<String, Object> res = new Hashtable<>();
- res.put(DirectoryConf.uri.name(), uriStr.toString());
- addIpaConfig(kerberosRealm, res);
- return res;
- }
-}
+++ /dev/null
-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<String, ?> properties) {
- try {
- Hashtable<String, Object> connEnv = new Hashtable<String, Object>();
- 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<SearchResult> search(LdapName searchBase, String searchFilter,
- SearchControls searchControls) throws NamingException {
- NamingEnumeration<SearchResult> 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<?, ?, LdapName> 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);
- }
- }
-}
+++ /dev/null
-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<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
- ArrayList<LdapEntry> 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<SearchResult> 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<LdapName> getDirectGroups(LdapName dn) {
- List<LdapName> directGroups = new ArrayList<LdapName>();
- try {
- String searchFilter = "(&(" + objectClass + "=" + getDirectory().getGroupObjectClass() + ")("
- + getDirectory().getMemberAttributeId() + "=" + dn + "))";
-
- SearchControls searchControls = new SearchControls();
- searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
-
- LdapName searchBase = getDirectory().getBaseDn();
- NamingEnumeration<SearchResult> 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<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
- List<HierarchyUnit> 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<SearchResult> 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);
- }
- }
-
-}
+++ /dev/null
-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<LdapEntryWorkingCopy> {
- boolean checkConnection();
-
- boolean entryExists(LdapName dn);
-
- LdapEntry doGetEntry(LdapName name) throws NameNotFoundException;
-
- Attributes doGetAttributes(LdapName name);
-
- List<LdapEntry> doGetEntries(LdapName searchBase, String filter, boolean deep);
-
- List<LdapName> getDirectGroups(LdapName dn);
-
- Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly);
-
- HierarchyUnit doGetHierarchyUnit(LdapName dn);
-
- LdapEntry newUser(LdapName name);
-
- LdapEntry newGroup(LdapName name);
-
- void init();
-
- void destroy();
-}
+++ /dev/null
-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<LdapName> getReferences(String attributeId);
-
- Dictionary<String, Object> 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<String, Object> properties, Collection<String> objectClasses) {
- String value = properties.get(LdapAttrs.objectClasses.name()).toString();
- Set<String> 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<String, Object> 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();
- }
- }
-}
+++ /dev/null
-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<LdapEntry, Attributes, LdapName> {
- @Override
- protected LdapName getId(LdapEntry entry) {
- return entry.getDn();
- }
-
- @Override
- protected Attributes cloneAttributes(LdapEntry entry) {
- return (Attributes) entry.getAttributes().clone();
- }
-}
+++ /dev/null
-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<HierarchyUnit> 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();
- }
-
-}
+++ /dev/null
-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() {
-
- }
-}
+++ /dev/null
-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<LdapName, LdapEntry> entries = new TreeMap<>();
- private NavigableMap<LdapName, LdapHierarchyUnit> hierarchy = new TreeMap<>();
-
- private NavigableMap<LdapName, Attributes> 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<LdapName, Attributes> allEntries = ldifParser.read(in);
- for (LdapName key : allEntries.keySet()) {
- Attributes attributes = allEntries.get(key);
- // check for inconsistency
- Set<String> lowerCase = new HashSet<String>();
- NamingEnumeration<String> 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<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
- Objects.requireNonNull(searchBase);
- ArrayList<LdapEntry> 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<LdapName, ? extends LdapEntry> map, LdapName searchBase, String f, boolean deep,
- List<LdapEntry> 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<LdapName> getDirectGroups(LdapName dn) {
- List<LdapName> directGroups = new ArrayList<LdapName>();
- 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<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
- List<HierarchyUnit> 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);
- }
-}
+++ /dev/null
-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<LdapName, Attributes> 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<LdapName, Attributes> 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<LdapName, Attributes> read(Reader reader) throws IOException {
- SortedMap<LdapName, Attributes> res = new TreeMap<LdapName, Attributes>();
- try {
- List<String> 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
+++ /dev/null
-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<? extends Attribute> 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<? extends Attribute> 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<LdapName, Attributes> 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<String> 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');
- }
- }
-}
+++ /dev/null
-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());
- }
-
-}
+++ /dev/null
-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();
- }
-
-}
+++ /dev/null
-package org.argeo.util.http;
-
-/** Generic HTTP methods. */
-public enum HttpMethod {
- OPTIONS, //
- HEAD, //
- GET, //
- POST, //
- PUT, //
- DELETE, //
-
- // WebDav
- PROPFIND, //
- PROPPATCH, //
- MKCOL, //
- MOVE, //
- COPY, //
- ;
-}
+++ /dev/null
-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() {
-
- }
-}
+++ /dev/null
-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;
- }
-
-}
+++ /dev/null
-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
+++ /dev/null
-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<String> enumToDns(EnumSet<? extends Distinguished> enumSet) {
- Set<String> res = new TreeSet<>();
- for (Enum<? extends Distinguished> enm : enumSet) {
- res.add(((Distinguished) enm).dn());
- }
- return res;
- }
-}
+++ /dev/null
-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
+++ /dev/null
-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:<br>
- * - <a href= "https://www.ldap.com/ldap-oid-reference">Standard LDAP</a><br>
- * - <a href=
- * "https://github.com/krb5/krb5/blob/master/src/plugins/kdb/ldap/libkdb_ldap/kerberos.schema">Kerberos
- * LDAP (partial)</a>
- */
-public enum LdapAttrs implements SpecifiedName, Supplier<String> {
- /** */
- 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();
- }
-
-}
+++ /dev/null
-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
+++ /dev/null
-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
- * <a href="https://www.ldap.com/ldap-oid-reference">https://www.ldap.com/ldap-
- * oid-reference</a>
- */
-public enum LdapObjs implements SpecifiedName, Supplier<String> {
- 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();
- }
-
-}
+++ /dev/null
-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<String, List<String>> query, String key) {
- if (!query.containsKey(key))
- return null;
- List<String> 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<String, List<String>> queryToMap(URI uri) {
- return queryToMap(uri.getQuery());
- }
-
- private static Map<String, List<String>> queryToMap(String queryPart) {
- try {
- final Map<String, List<String>> query_pairs = new LinkedHashMap<String, List<String>>();
- 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<String>());
- }
- 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");
- }
-}
+++ /dev/null
-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";
-}
+++ /dev/null
-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();
- }
-}
+++ /dev/null
-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<String> dnsServerUrls) {
- try {
- Objects.requireNonNull(dnsServerUrls);
- Hashtable<String, Object> 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<String, List<String>> getAllRecords(String name) {
- try {
- Map<String, List<String>> res = new TreeMap<>();
- Attributes attrs = initialCtx.getAttributes(name);
- NamingEnumeration<String> ids = attrs.getIDs();
- while (ids.hasMore()) {
- String recordType = ids.next();
- List<String> lst = new ArrayList<String>();
- 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<String> getRecords(String name, String recordType) {
- try {
- List<String> res = new ArrayList<String>();
- 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<String> getSrvRecordsAsHosts(String name, boolean withPort) {
- List<String> raw = getRecords(name, "SRV");
- if (raw.size() == 0)
- return null;
- SortedSet<SrvRecord> 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<String> lst = new ArrayList<>();
- for (SrvRecord order : res) {
- lst.add(order.toHost(withPort));
- }
- return Collections.unmodifiableList(lst);
- }
-
- private void addValues(Attribute attr, List<String> 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<String> listEntries(String name) {
- try {
- List<String> res = new ArrayList<String>();
- NamingEnumeration<Binding> 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<String, List<String>> 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 <hostname> [<record type> | *]");
- }
-
-}
\ No newline at end of file
+++ /dev/null
-package org.argeo.util.naming.dns;
-
-class SrvRecord implements Comparable<SrvRecord> {
- 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 : "");
- }
-}
+++ /dev/null
-/** Generic naming and LDAP support. */
-package org.argeo.util.naming;
\ No newline at end of file
+++ /dev/null
-/** Generic Java utilities. */
-package org.argeo.util;
\ No newline at end of file
+++ /dev/null
-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<I> implements Supplier<I>, Comparable<Component<?>> {
-
- private final I instance;
-
- private final Runnable init;
- private final Runnable close;
-
- private final Map<Class<? super I>, PublishedType<? super I>> types;
- private final Set<Dependency<?>> dependencies;
- private final Map<String, Object> properties;
-
- private CompletableFuture<Void> activationStarted = null;
- private CompletableFuture<Void> activated = null;
-
- private CompletableFuture<Void> deactivationStarted = null;
- private CompletableFuture<Void> deactivated = null;
-
- // internal
- private Set<Dependency<?>> dependants = new HashSet<>();
-
- private RankingKey rankingKey;
-
- Component(ComponentRegister register, I instance, Runnable init, Runnable close, Set<Dependency<?>> dependencies,
- Set<Class<? super I>> classes, Map<String, Object> 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<Class<? super I>, PublishedType<? super I>> types = new HashMap<>(classes.size());
- for (Class<? super I> 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<String, Object> 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<Void>();
- activated = activationStarted //
- .thenComposeAsync(this::dependenciesActivated) //
- .thenRun(this.init) //
- .thenRun(() -> prepareNextDeactivation());
- }
-
- private void prepareNextDeactivation() {
- deactivationStarted = new CompletableFuture<Void>();
- deactivated = deactivationStarted //
- .thenComposeAsync(this::dependantsDeactivated) //
- .thenRun(this.close) //
- .thenRun(() -> prepareNextActivation());
- }
-
- CompletableFuture<Void> getActivated() {
- return activated;
- }
-
- CompletableFuture<Void> 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<Void> dependenciesActivated(Void v) {
- Set<CompletableFuture<?>> constraints = new HashSet<>(this.dependencies.size());
- for (Dependency<?> dependency : this.dependencies) {
- CompletableFuture<Void> dependencyActivated = dependency.publisherActivated() //
- .thenCompose(dependency::set);
- constraints.add(dependencyActivated);
- }
- return CompletableFuture.allOf(constraints.toArray(new CompletableFuture[constraints.size()]));
- }
-
- CompletableFuture<Void> dependantsDeactivated(Void v) {
- Set<CompletableFuture<?>> constraints = new HashSet<>(this.dependants.size());
- for (Dependency<?> dependant : this.dependants) {
- CompletableFuture<Void> dependantDeactivated = dependant.dependantDeactivated() //
- .thenCompose(dependant::unset);
- constraints.add(dependantDeactivated);
- }
- CompletableFuture<Void> 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 <T> PublishedType<T> getType(Class<T> clss) {
- if (!types.containsKey(clss))
- throw new IllegalArgumentException(clss.getName() + " is not a type published by this component");
- return (PublishedType<T>) types.get(clss);
- }
-
- public <T> boolean isPublishedType(Class<T> clss) {
- return types.containsKey(clss);
- }
-
- public Map<String, Object> 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<String> 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<T> {
- private Component<? extends T> component;
- private Class<T> clss;
-
- private CompletableFuture<T> value;
-
- public PublishedType(Component<? extends T> component, Class<T> clss) {
- this.clss = clss;
- this.component = component;
- value = CompletableFuture.completedFuture((T) component.instance);
- }
-
- public Component<?> getPublisher() {
- return component;
- }
-
- public Class<T> getType() {
- return clss;
- }
-
- public CompletionStage<T> getValue() {
- return value.minimalCompletionStage();
- }
- }
-
- /** Builds a {@link Component}. */
- public static class Builder<I> implements Supplier<I> {
- private final I instance;
-
- private Runnable init;
- private Runnable close;
-
- private Set<Dependency<?>> dependencies = new HashSet<>();
- private Set<Class<? super I>> types = new HashSet<>();
- private final Map<String, Object> properties = new HashMap<>();
-
- public Builder(I instance) {
- this.instance = instance;
- }
-
- public Component<I> build(ComponentRegister register) {
- // default values
- if (types.isEmpty()) {
- types.add(getInstanceClass());
- }
-
- if (init == null)
- init = () -> {
- };
- if (close == null)
- close = () -> {
- };
-
- // instantiation
- Component<I> component = new Component<I>(register, instance, init, close, dependencies, types, properties);
- for (Dependency<?> dependency : dependencies) {
- dependency.type.getPublisher().addDependant(dependency);
- }
- return component;
- }
-
- public Builder<I> addType(Class<? super I> clss) {
- types.add(clss);
- return this;
- }
-
- public Builder<I> addActivation(Runnable init) {
- if (this.init != null)
- throw new IllegalArgumentException("init method is already set");
- this.init = init;
- return this;
- }
-
- public Builder<I> addDeactivation(Runnable close) {
- if (this.close != null)
- throw new IllegalArgumentException("close method is already set");
- this.close = close;
- return this;
- }
-
- public <D> Builder<I> addDependency(PublishedType<D> type, Consumer<D> set, Consumer<D> unset) {
- dependencies.add(new Dependency<D>(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<I> getInstanceClass() {
- return (Class<I>) instance.getClass();
- }
-
- }
-
- static class Dependency<D> {
- private PublishedType<D> type;
- private Consumer<D> set;
- private Consumer<D> unset;
-
- // live
- Component<?> dependantComponent;
- CompletableFuture<Void> setStage;
- CompletableFuture<Void> unsetStage;
-
- public Dependency(PublishedType<D> types, Consumer<D> set, Consumer<D> 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<Void> publisherActivated() {
- return type.getPublisher().activated.copy();
- }
-
- CompletableFuture<Void> dependantDeactivated() {
- return dependantComponent.deactivated.copy();
- }
-
- CompletableFuture<Void> set(Void v) {
- return type.value.thenAccept(set);
- }
-
- CompletableFuture<Void> unset(Void v) {
- return type.value.thenAccept(unset);
- }
-
- }
-}
+++ /dev/null
-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);
-
- <T> SortedSet<Component<? extends T>> find(Class<T> clss, Predicate<Map<String, Object>> filter);
-
- default <T> Component.PublishedType<T> getSingleton(Class<T> type) {
- SortedSet<Component<? extends T>> found = find(type, null);
- if (found.size() == 0)
- throw new IllegalStateException("No component found for " + type);
- return found.first().getType(type);
- }
-
- default <T> T getObject(Class<T> clss) {
- SortedSet<Component<? extends T>> found = find(clss, null);
- if (found.size() == 0)
- return null;
- return found.first().get();
- }
-
- Component<?> get(Object instance);
-
-// default <T> PublishedType<T> getType(Class<T> clss) {
-// SortedSet<Component<? extends T>> components = find(clss, null);
-// if (components.size() == 0)
-// return null;
-// return components.first().getType(clss);
-// }
-
- void activate();
-
- void deactivate();
-
- boolean isActive();
-
- void clear();
-}
+++ /dev/null
-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<RankingKey> {
- 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<String, Object> 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);
- }
-}
+++ /dev/null
-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<Object, Component<?>> components = new IdentityHashMap<>();
- private final AtomicLong nextServiceId = new AtomicLong(0l);
-
- @Override
- public long register(Component<?> component) {
- return registerComponent(component);
- }
-
- @SuppressWarnings({ "unchecked" })
- @Override
- public synchronized <T> SortedSet<Component<? extends T>> find(Class<T> clss,
- Predicate<Map<String, Object>> filter) {
- SortedSet<Component<? extends T>> result = new TreeSet<>();
- instances: for (Object instance : components.keySet()) {
- if (!clss.isAssignableFrom(instance.getClass()))
- continue instances;
- Component<? extends T> component = (Component<? extends T>) 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<CompletableFuture<?>> 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<CompletableFuture<?>> 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();
- }
-}
+++ /dev/null
-package org.argeo.util.transaction;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public abstract class AbstractWorkingCopy<DATA, ATTR, ID> implements WorkingCopy<DATA, ATTR, ID> {
- private Map<ID, DATA> newData = new HashMap<ID, DATA>();
- private Map<ID, ATTR> modifiedData = new HashMap<ID, ATTR>();
- private Map<ID, DATA> deletedData = new HashMap<ID, DATA>();
-
- 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<ID, DATA> getNewData() {
- return newData;
- }
-
- public Map<ID, DATA> getDeletedData() {
- return deletedData;
- }
-
- public Map<ID, ATTR> getModifiedData() {
- return modifiedData;
- }
-
-}
+++ /dev/null
-package org.argeo.util.transaction;
-
-/** JTA transaction status. */
-public class JtaStatusAdapter implements TransactionStatusAdapter<Integer> {
- 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;
- }
-
-}
+++ /dev/null
-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);
- }
-
-}
+++ /dev/null
-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<T>
-//implements Transaction, Status
-{
- private final Xid xid;
- private T status;
- private final List<XAResource> xaResources = new ArrayList<XAResource>();
-
- private final SimpleTransactionManager transactionManager;
- private TransactionStatusAdapter<T> tsa;
-
- public SimpleTransaction(SimpleTransactionManager transactionManager, TransactionStatusAdapter<T> 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;
- }
-
-}
+++ /dev/null
-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<SimpleTransaction<Integer>> current = new ThreadLocal<SimpleTransaction<Integer>>();
-
- private Map<Xid, SimpleTransaction<Integer>> knownTransactions = Collections
- .synchronizedMap(new HashMap<Xid, SimpleTransaction<Integer>>());
- private TransactionStatusAdapter<Integer> tsa = new JtaStatusAdapter();
-// private SyncRegistry syncRegistry = new SyncRegistry();
-
- /*
- * WORK IMPLEMENTATION
- */
- @Override
- public <T> T required(Callable<T> 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<Integer> transaction = new SimpleTransaction<Integer>(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<Integer> getTransaction()
-// throws SystemException
- {
- return getCurrent();
- }
-
- protected SimpleTransaction<Integer> getCurrent()
-// throws SystemException
- {
- SimpleTransaction<Integer> 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<Integer> 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<Integer> suspend()
-// throws SystemException
- {
- SimpleTransaction<Integer> 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);
-// }
-// }
-//
-// }
-}
+++ /dev/null
-package org.argeo.util.transaction;
-
-/** Abstract the various approaches to represent transaction status. */
-public interface TransactionStatusAdapter<T> {
- T getActiveStatus();
-
- T getPreparingStatus();
-
- T getMarkedRollbackStatus();
-
- T getPreparedStatus();
-
- T getCommittingStatus();
-
- T getCommittedStatus();
-
- T getRollingBackStatus();
-
- T getRolledBackStatus();
-
- T getNoTransactionStatus();
-}
+++ /dev/null
-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);
- // }
-}
+++ /dev/null
-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);
-}
+++ /dev/null
-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> T required(Callable<T> work);
-
- void setRollbackOnly();
-
- WorkContext getWorkContext();
-}
+++ /dev/null
-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();
-}
+++ /dev/null
-package org.argeo.util.transaction;
-
-import java.util.Map;
-
-public interface WorkingCopy<DATA, ATTR, ID> {
- void startEditing(DATA user);
-
- boolean noModifications();
-
- void cleanUp();
-
- Map<ID, DATA> getNewData();
-
- Map<ID, DATA> getDeletedData();
-
- Map<ID, ATTR> getModifiedData();
-
-}
+++ /dev/null
-package org.argeo.util.transaction;
-
-public interface WorkingCopyProcessor<WC extends WorkingCopy<?, ?, ?>> {
- void prepare(WC wc);
-
- void commit(WC wc);
-
- void rollback(WC wc);
-
- WC newWorkingCopy();
-}
+++ /dev/null
-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<WC extends WorkingCopy<?, ?, ?>> implements XAResource {
- private final WorkingCopyProcessor<WC> processor;
-
- private Map<Xid, WC> workingCopies = new HashMap<Xid, WC>();
- private Xid editingXid = null;
- private int transactionTimeout = 0;
-
- public WorkingCopyXaResource(WorkingCopyProcessor<WC> 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);
- }
-
-}
+++ /dev/null
-package org.argeo.util.transaction;
-
-import javax.transaction.xa.XAResource;
-
-public interface XAResourceProvider {
- XAResource getXaResource();
-}
+++ /dev/null
-/** Minimalistic and partial XA transaction manager implementation. */
-package org.argeo.util.transaction;
\ No newline at end of file
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;
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;
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;
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 {
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;
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;
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 {