oVirt LDAP authentication and authorization extension
=====================================================

Generic LDAP implementation for oVirt engine.

IMPLEMENTATION NOTES
--------------------

Implementation uses UnboundID LDAP SDK for Java. Many of the terms and
configuration options derived from the SDK terms. More information can
be found at UnboundID site[1].

Refer to README.unboundid-ldapsdk for known issues and limitations.

[1] https://www.unboundid.com/products/ldap-sdk/

EXTENSION CONFIGURATION
-----------------------

AUTHZ

Configure authorization extension.

/etc/ovirt-engine/extensions.d/@AUTHZ_NAME@.properties

ovirt.engine.extension.name = @AUTHZ_NAME@
ovirt.engine.extension.bindings.method = jbossmodule
ovirt.engine.extension.binding.jbossmodule.module = org.ovirt.engine-extensions.aaa.ldap
ovirt.engine.extension.binding.jbossmodule.class = org.ovirt.engineextensions.aaa.ldap.AuthzExtension
ovirt.engine.extension.provides = org.ovirt.engine.api.extensions.aaa.Authz
config.profile.file.1 = @PROFILE_CONFIGURATION@

@AUTHZ_NAME@
    Extension instance name.
@PROFILE@
    Profile name, visible to user.
@PROFILE_CONFIGURATION@
    Profile configuration file, may be relative to extension configuration.

AUTHN

Configure authentication extension.

/etc/ovirt-engine/extensions.d/@AUTHN_NAME@.properties

ovirt.engine.extension.name = @AUTHN_NAME@
ovirt.engine.extension.bindings.method = jbossmodule
ovirt.engine.extension.binding.jbossmodule.module = org.ovirt.engine-extensions.aaa.ldap
ovirt.engine.extension.binding.jbossmodule.class = org.ovirt.engineextensions.aaa.ldap.AuthnExtension
ovirt.engine.extension.provides = org.ovirt.engine.api.extensions.aaa.Authn
ovirt.engine.aaa.authn.profile.name = @PROFILE@
ovirt.engine.aaa.authn.authz.plugin = @AUTHZ_NAME@
config.profile.file.1 = @PROFILE_CONFIGURATION@

@AUTHN_NAME@
    Extension instance name.
@AUTHZ_NAME@
    Authz extension instance name.
@PROFILE@
    Profile name, visible to user.
@PROFILE_CONFIGURATION@
    Profile configuration file, may be relative to extension configuration.

PROFILE CONFIGURATION EXAMPLES
------------------------------

EXAMPLES

Examples are available at the following directory:

  /usr/share/ovirt-engine-extension-aaa-ldap/example

Examples are relative to /etc/ovirt-engine directory.

OPENLDAP/389DS/IPA/...

Using simple bind transport using startTLS:

    # select one
    include = <openldap.properties>
    #include = <389ds.properties>
    #include = <rhds.properties>
    #include = <ipa.properties>
    #include = <iplanet.properties>
    #include = <rfc2307.properties>

    vars.server = ldap1.company.com
    vars.user = uid=search,cn=users,cn=accounts,dc=company,dc=com
    vars.password = 123456

    pool.default.serverset.single.server = ${global:vars.server}
    pool.default.auth.simple.bindDN = ${global:vars.user}
    pool.default.auth.simple.password = ${global:vars.password}
    pool.default.ssl.startTLS = true
    pool.default.ssl.truststore.file = ${local:_basedir}/${global:vars.server}.jks
    pool.default.ssl.truststore.password = changeit

Round robin configuration:

    pool.default.serverset.type = round-robin
    pool.default.serverset.round-robin.1.server = ${global:vars.server1}
    pool.default.serverset.round-robin.2.server = ${global:vars.server2}

In case sasl mechanism is used, such as gssapi, set the following within
extension configuration:

# Except of active directory
config.globals.bindFormat.simple_bindFormat = realm

More supported configuration at README.profile.

ACTIVE DIRECTORY

Active Directory 2003 R2 and above is supported.

Using simple bind transport using startTLS. Unfortunately, SASL does not provide
bind failure reasons.

Connect to Domain Controller DNS Server directly, use SRV record to
resolve hosts.

    include = <ad.properties>

    vars.domain = company.com
    vars.user = search@${global:vars.domain}
    vars.password = 123456
    vars.dns = dns://dc1.${global:vars.domain} dns://dc2.${global:vars.domain}

    pool.default.serverset.type = srvrecord
    pool.default.serverset.srvrecord.domain = ${global:vars.domain}
    pool.default.auth.simple.bindDN = ${global:vars.user}
    pool.default.auth.simple.password = ${global:vars.password}
    pool.default.serverset.srvrecord.jndi-properties.java.naming.provider.url = ${global:vars.dns}
    pool.default.socketfactory.resolver.uRL = ${global:vars.dns}
    pool.default.ssl.startTLS = true
    pool.default.ssl.truststore.file = ${local:_basedir}/${global:vars.domain}.jks
    pool.default.ssl.truststore.password = changeit

More supported configuration at README.profile.

X.509 CERTIFICATE TRUST STORE
-----------------------------

When using TLS/SSL to communicate with LDAP server an X.509 certificate
trust store should be provided if the certificate of the LDAP server is
not signed by well known certification authority.

The trust store can be anything Java supports, by default it is
JKS (Java Key Store) format.

Use the following command to create a JKS myrootca.jks trust store using
password 'changeit' and import the certificate myrootca.pem into
alias myrootca:

 $ keytool -importcert -noprompt -trustcacerts -alias myrootca \
       -file myrootca.pem -keystore myrootca.jks -storepass changeit

APACHE SSO CONFIGURATION
------------------------

Authorization extension can be used in an environment in which apache
preforms the authentication, common example is kerberos. Use the
ovirt-engine-extension-aaa-misc and configure the http authentication
extension to acquire principal name out of the request.

APACHE CONFIGURATION

The following example enforces kerberos authentication, and delegate
principal name via HTTP headers. The actual kerberos configuration is
out of scope for this document.

<LocationMatch ^(/ovirt-engine/(webadmin|userportal|api)|/api)>
    RewriteEngine on
    RewriteCond %{LA-U:REMOTE_USER} ^(.*)$
    RewriteRule ^(.*)$ - [L,P,E=REMOTE_USER:%1]
    RequestHeader set X-Remote-User %{REMOTE_USER}s

    AuthType Kerberos
    AuthName "Kerberos Login"
    Krb5Keytab /etc/krb5.keytab
    KrbAuthRealms REALM.COM
    Require valid-user
</LocationMatch>

WARNING!!!

In case SSO is enforced on partial URI list (example only api), The
X-Remote-User must be reseted for the remaining URIs, to avoid security
bypass.

AUTHN EXTENSION

The following configuration read the X-Remote-User header and sets it as
principal name.

/etc/ovirt-engine/extensions.d/http-authn.properties

ovirt.engine.extension.name = http-authn
ovirt.engine.extension.bindings.method = jbossmodule
ovirt.engine.extension.binding.jbossmodule.module = org.ovirt.engine-extensions.aaa.misc
ovirt.engine.extension.binding.jbossmodule.class = org.ovirt.engineextensions.aaa.misc.http.AuthnExtension
ovirt.engine.extension.provides = org.ovirt.engine.api.extensions.aaa.Authn
ovirt.engine.aaa.authn.profile.name = http
ovirt.engine.aaa.authn.authz.plugin = ldap-authz
ovirt.engine.aaa.authn.mapping.plugin = http-mapping
config.artifact.name = HEADER
config.artifact.arg = X-Remote-User

MAPPING

/etc/ovirt-engine/extensions.d/http-mapping.properties

ovirt.engine.extension.enabled = true
ovirt.engine.extension.name = http-mapping
ovirt.engine.extension.bindings.method = jbossmodule
ovirt.engine.extension.binding.jbossmodule.module = org.ovirt.engine-extensions.aaa.misc
ovirt.engine.extension.binding.jbossmodule.class = org.ovirt.engineextensions.aaa.misc.mapping.MappingExtension
ovirt.engine.extension.provides = org.ovirt.engine.api.extensions.aaa.Mapping
config.mapAuthRecord.type = regex
config.mapAuthRecord.regex.mustMatch = true
config.mapAuthRecord.regex.pattern = ^(?<user>.*?)((\\\\(?<at>@)(?<suffix>.*?)@.*)|(?<realm>@.*))$

# START-PLATFORM-DEPENDED
# Active directory:
config.mapAuthRecord.regex.replacement = ${user}${at}${suffix}${realm}
# Other
config.mapAuthRecord.regex.replacement = ${user}${at}${suffix}
# END-PLATFORM-DEPENDED

PROBLEM DETERMINATION
---------------------

A logger by the name of org.ovirt.engineextensions.aaa.ldap can be set to
DEBUG, FINE, FINER or ALL to receive verbose output.

Per the time this text is written, setting the log level of a logger within
ovirt-engine is performed by updating:

  /usr/share/ovirt-engine/services/ovirt-engine/ovirt-engine.xml.in

And adding the following before the <root-logger> line:

      <logger category="org.ovirt.engineextensions.aaa.ldap">
        <level name="ALL"/>
      </logger>

ADVANCED EXTENSION CONFIGURATION
--------------------------------

config.profile.searchdir.@SORT@ = DIRECTORY
    Additional profile configuration search directories.
    xxx is alphabetic sorted.

config.profile.file.@SORT@ = FILE
    Profile configurations to read.
    xxx is alphabetic sorted.

config.globals.@SORT@.@VAR@ = VALUE
    Sequence variables to set before initialization.

config.authn.credentials-change.message = TEXT
    A message to display if password is expired.

config.authn.credentials-change.url = URL
    A URL to display if password is expired.

config.authn.sequence.authn.name = ID [authn]
    Sequence name of authentication.

    Input:
        user
        password

    Output:
        authTranslatedMessage
        PrincipalRecord_PRINCIPAL
        message

attrmap.map-principal-record.name = ID [map-principal-record]
    Attribute map to map between principal record and native attributes.

attrmap.map-group-record.name = ID [map-group-record]
    Attribute map to map between group record and native attributes.

config.authz.query.max_filter_size = INT [50]
    A default maximum filter size in elements. Usually, should be set by
    configuration.

config.authz.sequence.credentials-change.name = ID [credentials-change]
    Sequence name of credentials change.

    Input:
        user
        password
        passwordNew

config.authz.sequence.namespace.attribute.namespace = ID [namespace]
    Attribute name of namespace within the namespace query.

config.authz.sequence.namespace.name = ID [namespace]
    Sequence name of namespace query.
    Used during initialization to determine namespaces.

    Output:
        query

config.authz.sequence.resolve-principal.name = ID [resolve-principal]
    Sequence name of resolve principal.
    Used during user login to fetch properties of principal name.

    Input:
        PrincipalRecord_PRINCIPAL

    Output:
        query

config.authz.sequence.resolve-groups.name = ID [resolve-groups]
    Sequence name of resolve groups out of DN.
    Used during user login to fetch groups recursively.
    Used during directory sync.

    Input:
        dn
        dnType - principal|group

    Output:
        query*

config.authz.sequence.query-principals.name = ID [query-principals]
    Sequence name of query principal.
    Used during administrative tasks.

    Input:
        namespace
        filter

    Output:
        query

config.authz.sequence.query-groups.name = ID [query-groups]
    Sequence name of query groups.
    Used during administrative tasks.

    Input:
        namespace
        filter

    Output:
        query
