Giter VIP home page Giter VIP logo

portunus's People

Contributors

dependabot[bot] avatar majewsky avatar supersandro2000 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

portunus's Issues

Portunus doesn't just listen on localhost in NixOS

This may be specific to NixOS, but I figured I'd report here. The option services.portunus.port is described as:

Description: Port where the Portunus webserver should listen on. This must be put behind a
  TLS-capable reverse proxy because Portunus only listens on localhost.

Declared in: [nixos/modules/services/misc/portunus.nix](https://github.com/NixOS/nixpkgs/blob/nixos-22.11/nixos/modules/services/misc/portunus.nix)

But it isn't only listening on localhost:

$ sudo ss --listening  -t --processes
State    Recv-Q   Send-Q       Local Address:Port              Peer Address:Port   Process
LISTEN   0        2048               0.0.0.0:ldap                   0.0.0.0:*       users:(("slapd",pid=621556,fd=7))
LISTEN   0        4096                     *:http-alt                     *:*       users:(("portunus-server",pid=621554,fd=11))
LISTEN   0        2048                  [::]:ldap                      [::]:*       users:(("slapd",pid=621556,fd=8))

Seed is ignored on an existing instance and behaves inconsistently on "seeded" objects

I have a small system that I'm playing around with having a combination of seeded and non-seeded users and groups. I started initially with the default NixOS seed (admin and Dex search user) and have added to it over time.

I have so far noticed the following:

  • Users and groups present in the seed are not added to the database.json or slapd (confirmed both in logs and by attempting to authenticate against them, as well as obviously not appearing in the UI)
  • Users and groups are not updated based on changes made in the seed
  • Users can be created through the UI with an ID matching a seed with no issue
    • Attempting to modifying such users with seed-defined attributes fails
    • Attempting to delete such users displays a success message but the user remains present (this is consistent for users that are actually seeded)
  • Groups cannot be created through the UI with an ID matching a seed; attempting to do so displays a success message but nothing happens
  • Seeded groups can have users added to them through the UI, but not removed; no changes to memberships are made when "re-seeding"

Using v1.1.0.
Current seed.json for reference.

self-signed TLS fails with failed to verify certificate: x509: certificate signed by unknown authority"

Hello, thank you for your effort in this project!
I've been playing with portunus and tried to use SSL certificate signed with my own root CA, but it always fails even if I add my CA system-wide. Is there a way to use self-signed certificates?

Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07be8128 0x7f93900186c0 daemon: activity on 1 descriptor
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07bea50b 0x7f93900186c0 daemon: activity on:65941c57.07beaa3e 0x7f93900186c0
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07beaeaa 0x7f93900186c0 slap_listener_activate(8):
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07bef05a 0x7f93900186c0 daemon: epoll: listen=7 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07befab5 0x7f93900186c0 daemon: epoll: listen=8 busy
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07bf6759 0x7f938f8176c0 >>> slap_listener(ldaps:///)
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07bf95a1 0x7f938f8176c0 daemon: accept() = 12
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07bfe8e0 0x7f938f8176c0 daemon: listen=8, new connection on 12
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c0137a 0x7f93900186c0 daemon: activity on 1 descriptor
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c022e4 0x7f93900186c0 daemon: activity on:65941c57.07c028f6 0x7f93900186c0
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c03ae6 0x7f93900186c0 daemon: epoll: listen=7 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c041be 0x7f93900186c0 daemon: epoll: listen=8 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c05c66 0x7f938f8176c0 daemon: added 12r (active) listener=(nil)
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c0ebfe 0x7f938f8176c0 conn=1008 fd=12 ACCEPT from IP=[::1]:39154 (IP=[::]:636)
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c1661d 0x7f93900186c0 daemon: activity on 2 descriptors
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c174f6 0x7f93900186c0 daemon: activity on:65941c57.07c17e2a 0x7f93900186c0  12r65941c57.07c184a3 0x7f93900186c0
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c18ec1 0x7f93900186c0 daemon: read active on 12
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c24656 0x7f93900186c0 daemon: epoll: listen=7 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c256e0 0x7f93900186c0 daemon: epoll: listen=8 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra slapd[1264]: conn=1008 fd=12 ACCEPT from IP=[::1]:39154 (IP=[::]:636)
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c31333 0x7f938f8176c0 connection_get(12)
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c3235b 0x7f938f8176c0 connection_get(12): got connid=1008
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c3280e 0x7f938f8176c0 connection_read(12): checking for input on id=1008
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c37d55 0x7f938f8176c0 TLS trace: SSL_accept:before SSL initialization
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c39635 0x7f938f8176c0 TLS trace: SSL_accept:before SSL initialization
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c4c54d 0x7f938f8176c0 TLS trace: SSL_accept:SSLv3/TLS read client hello
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c6621e 0x7f938f8176c0 TLS trace: SSL_accept:SSLv3/TLS write server hello
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c6cdba 0x7f938f8176c0 TLS trace: SSL_accept:SSLv3/TLS write change cipher spec
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c6e4e1 0x7f938f8176c0 TLS trace: SSL_accept:TLSv1.3 write encrypted extensions
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c9708e 0x7f938f8176c0 TLS trace: SSL_accept:SSLv3/TLS write certificate
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07c9fb64 0x7f938f8176c0 TLS trace: SSL_accept:TLSv1.3 write server certificate verify
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07cba2e6 0x7f938f8176c0 TLS trace: SSL_accept:SSLv3/TLS write finished
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07cbbca0 0x7f938f8176c0 TLS trace: SSL_accept:TLSv1.3 early data
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07cbcd31 0x7f938f8176c0 TLS trace: SSL_accept:error in TLSv1.3 early data
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07cbfc19 0x7f93900186c0 daemon: activity on 1 descriptor
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07cc037a 0x7f93900186c0 daemon: activity on:65941c57.07cc0a4e 0x7f93900186c0
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07cc1639 0x7f93900186c0 daemon: epoll: listen=7 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07cc1c43 0x7f93900186c0 daemon: epoll: listen=8 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dbe23c 0x7f93900186c0 daemon: activity on 1 descriptor
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dbfc13 0x7f93900186c0 daemon: activity on:65941c57.07dc0662 0x7f93900186c0  12r65941c57.07dc0bcf 0x7f93900186c0
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dc11e8 0x7f93900186c0 daemon: read active on 12
Jan 02 14:23:19 infra slapd[1264]: conn=1008 fd=12 closed (TLS negotiation failure)
Jan 02 14:23:19 infra portunus-orchestrator[1262]: 2024/01/02 14:23:19 INFO: cannot connect to LDAP server (attempt 10/10): LDAP Result Code 200 "Network Error": tls: failed to verify certificate: x509: certifica>
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dc267e 0x7f93900186c0 daemon: epoll: listen=7 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1262]: 2024/01/02 14:23:19 FATAL: giving up on LDAP server after 10 connection attempts
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dc2e0b 0x7f93900186c0 daemon: epoll: listen=8 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dcc4d0 0x7f938f8176c0 connection_get(12)
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dce33c 0x7f938f8176c0 connection_get(12): got connid=1008
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dce933 0x7f938f8176c0 connection_read(12): checking for input on id=1008
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dd19b4 0x7f938f8176c0 TLS trace: SSL3 alert read:fatal:bad certificate
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dd2696 0x7f938f8176c0 TLS trace: SSL_accept:error in error
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dd3a77 0x7f938f8176c0 TLS: can't accept: error:0A000412:SSL routines::sslv3 alert bad certificate.
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dd9bdd 0x7f938f8176c0 connection_read(12): TLS accept failure error=-1 id=1008, closing
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07ddaa1c 0x7f938f8176c0 connection_closing: readying conn=1008 sd=12 for close
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07ddeaf1 0x7f938f8176c0 connection_close: conn=1008 sd=12
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07de067e 0x7f938f8176c0 daemon: removing 12
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07de888f 0x7f938f8176c0 conn=1008 fd=12 closed (TLS negotiation failure)
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07dec6c5 0x7f93900186c0 daemon: activity on 1 descriptor
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07deed34 0x7f93900186c0 daemon: activity on:65941c57.07def3ca 0x7f93900186c0
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07deffcd 0x7f93900186c0 daemon: epoll: listen=7 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1264]: 65941c57.07df042e 0x7f93900186c0 daemon: epoll: listen=8 active_threads=0 tvp=NULL
Jan 02 14:23:19 infra portunus-orchestrator[1251]: 2024/01/02 14:23:19 FATAL: error encountered while running portunus-server: exit status 1
Jan 02 14:23:19 infra systemd[1]: portunus.service: Main process exited, code=exited, status=1/FAILURE

Here is how I'm configuring it on nixos:

services.portunus = {
        enable = true;
        domain = config.networking.domain;
        port = 8080;
        seedPath = seedFile;
        ldap = {
            suffix = "dc=domain,dc=local";
            searchUserName = "search";
        };
    };
    systemd.services.portunus.environment = {
        PORTUNUS_SLAPD_TLS_CA_CERTIFICATE = ./.ssl/ca-chain.pem;
        PORTUNUS_SLAPD_TLS_CERTIFICATE = ./.ssl/ldap.pem;
        PORTUNUS_SLAPD_TLS_DOMAIN_NAME = ldapDomain;
        PORTUNUS_SLAPD_TLS_PRIVATE_KEY = config.sops.secrets.ldaps-key.path;
        PORTUNUS_DEBUG = "true";
    };

    networking.extraHosts = ''
    127.0.0.1 ${ldapDomain}
    '';

re-hash passwords into stronger hashes on login

If I recall correctly, vanilla slapd can do better than Salted SHA by this point, so Portunus should prefer one of the better hashes that slapd supports, and upgrade existing user accounts to use this hash on successful login.

Feature Request: Seed with password hash

It seems that there is currently now way for me to bring my own crypt hash for seeding? Say that my password is "example", then I would like to put $y$j9T$QxVr8WsdgXcVw.AVprEU20$MRgBvLbME5JhfxvcPwOCd4./YFicx/B/MtqZ1Fz12n6 in PORTUNUS_SEED_PATH=.../seed.json:

{
  "users": [{
    // ...
    "password": "$y$j9T$QxVr8WsdgXcVw.AVprEU20$MRgBvLbME5JhfxvcPwOCd4./YFicx/B/MtqZ1Fz12n6"
  }]
}

I also tried

{
  "users": [{
    // ...
    "password": "{CRYPT}$y$j9T$QxVr8WsdgXcVw.AVprEU20$MRgBvLbME5JhfxvcPwOCd4./YFicx/B/MtqZ1Fz12n6"
  }]
}

but that won't have the desired effect (my password then is literally "{CRYPT}$y...").

I would prefer that over putting the password in a file and using a command.

Unable to find the inital admin password for the webui

Hi,
I was just testing this out in NixOS and I am using services.portunus.enable etc...
I see that the initial password is printed to stdout only once but I did this with a nixos-rebuild switch command which runs Portunus as a system service.

I am hoping there is a way to still get the initial password or a simple way to set it myself.

Or should I delete and run it directly first, then enable the service?

OpenLDAP 2.6.3 -> 2.6.4

I've been experimenting with dovecot + portunus/openldap. The authentication works fine, except that the dovecot auth-worker process dumps core in libldap when slapd closes. As a result I tried against a newer OpenLDAP. Portunus seems to have trouble connecting to OpenLDAP 2.6.4:

Apr 05 14:42:14 silver systemd[1]: Started Self-contained authentication service.
Apr 05 14:42:14 silver portunus-orchestrator[2713016]: 2023/04/05 14:42:14 INFO: starting LDAP server
Apr 05 14:42:14 silver slapd[2713025]: @(#) $OpenLDAP: slapd 2.6.4 (Feb  8 2023 21:35:07) $
                                               openldap
Apr 05 14:42:14 silver portunus-orchestrator[2713024]: 2023/04/05 14:42:14 INFO: cannot connect to LDAP server (attempt 6/10): LDAP Result Code 200 "Network Error": dial tcp [::1]:636: connect: connection refused
Apr 05 14:42:15 silver slapd[2713025]: slapd starting
Apr 05 14:42:15 silver slapd[2713025]: conn=1000 fd=12 ACCEPT from IP=[::1]:51440 (IP=[::]:636)
Apr 05 14:42:15 silver slapd[2713025]: conn=1000 fd=12 TLS established tls_ssf=256 ssf=256 tls_proto=TLSv1.3 tls_cipher=TLS_AES_256_GCM_SHA384
Apr 05 14:42:15 silver slapd[2713025]: conn=1000 op=0 BIND dn="cn=portunus,dc=silver" method=128
Apr 05 14:42:15 silver slapd[2713025]: conn=1000 op=0 RESULT tag=97 err=49 qtime=0.000032 etime=0.000336 text=
Apr 05 14:42:15 silver portunus-orchestrator[2713024]: 2023/04/05 14:42:15 INFO: cannot connect to LDAP server (attempt 7/10): LDAP Result Code 49 "Invalid Credentials":

The exact same Portunus configuration works fine with OpenLDAP 2.6.3:

Apr 05 14:51:52 silver systemd[1]: Started Self-contained authentication service.
Apr 05 14:51:52 silver portunus-orchestrator[2713590]: 2023/04/05 14:51:52 INFO: starting LDAP server
Apr 05 14:51:53 silver portunus-orchestrator[2713606]: 2023/04/05 14:51:53 INFO: cannot connect to LDAP server (attempt 6/10): LDAP Result Code 200 "Network Error": dial tcp [::1]:636: connect: connection refused
Apr 05 14:51:53 silver slapd[2713607]: @(#) $OpenLDAP: slapd 2.6.3 (Jul 14 2022 18:37:34) $
                                               openldap
Apr 05 14:51:53 silver portunus-orchestrator[2713606]: 2023/04/05 14:51:53 INFO: cannot connect to LDAP server (attempt 7/10): LDAP Result Code 200 "Network Error": dial tcp [::1]:636: connect: connection refused
Apr 05 14:51:53 silver slapd[2713607]: slapd starting
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 fd=12 ACCEPT from IP=[::1]:56278 (IP=[::]:636)
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 fd=12 TLS established tls_ssf=256 ssf=256 tls_proto=TLSv1.3 tls_cipher=TLS_AES_256_GCM_SHA384
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 op=0 BIND dn="cn=portunus,dc=silver" method=128
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 op=0 BIND dn="cn=portunus,dc=silver" mech=SIMPLE bind_ssf=0 ssf=256
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 op=0 RESULT tag=97 err=0 qtime=0.000032 etime=0.028518 text=
Apr 05 14:51:54 silver portunus-orchestrator[2713606]: 2023/04/05 14:51:54 INFO: connected to LDAP server
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 op=1 ADD dn="dc=silver"
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 op=1 RESULT tag=105 err=0 qtime=0.000035 etime=0.000514 text=
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 op=2 ADD dn="ou=users,dc=silver"
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 op=2 RESULT tag=105 err=0 qtime=0.000030 etime=0.000349 text=
Apr 05 14:51:54 silver slapd[2713607]: conn=1000 op=3 ADD dn="ou=groups,dc=silver"
...

The OpenLDAP configuration doesn't seem to change from 2.6.3->2.6.4. I'm running out of time and energy to experiment with LDAP for now, but I thought I'd open this issue to give a heads up for anyone planning to try Portunus with 2.6.4.

Add 2FA support

It would be nice if we would have 2FA for more security. This wouldn't work over LDAP, so just the Web UI could be protected which would already be a win for admins. Additionally enforcing this for portunus admins would be nice.

What is the best way to programmatically add/modify users?

Hi there,

Is there any way to programatically add/modify users?
I tried using ldapmodify -x -H ldap://sso.example.com -D "uid=portunus,dc=example,dc=com" -w adminPass -f record_to_be_added.ldif but that didnt work out (found out afterwards by looking at teh code that the /var/lib/portunus/database.json is basiclaly the source of truth).

While I was looking I also didnt see anything like a rest api or anything.

I also took a look at seeding, however that has the drawback of everything having to be eitehr public or somethign to generate it.

The system I am planning this for is about 100-200 accounts, some of which will have to be enabled and disabled on a regular enough basis (a socirty with annual membership).

So as the title says any recommendations for automation?
Is it possible?

Add jpegPhoto attribute

Gitea allows syncing SshPublicKey and jpegPhoto. It would be nice if Portunus would allow to configure those.

Add OIDC support

Right now we are using dex-idp to support OIDC/OAuth2 but that has no function to properly remember sessions with support for logging our or select scopes to grant.
It would probably be a lot easier if portunus would just add an OAuth2 endpoint and clients would be statically configured.

Allow mass editing members of a group

When using double bind to gate a service behind a ldap group and you want to add a new group to many users, you right now need to edit every user individually. If you have more than just a few users this does not scale.

This could be solved in several ways:

  • allow editing the members of a group
  • allow mass group edits on users, for example add this group to all selected users or remove that group from them

seeding from template

There should be a way to seed technical users from config. Something like

$ cat seed.json
{
  "users": [ {
     "name": "foo",
      "passwordFromCommand": "cat /etc/secrets/foo-password.txt",
      "groups": [ "bar" ],
      ...
   } ],
   "groups": [ ...]
}

that gets merged with database.json on startup.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.