diff --git a/doc/Sphinx/source/img/image1institutional.png b/doc/Sphinx/source/img/image1institutional.png new file mode 100755 index 00000000000..39852ea1c23 Binary files /dev/null and b/doc/Sphinx/source/img/image1institutional.png differ diff --git a/doc/Sphinx/source/img/image2institutional.png b/doc/Sphinx/source/img/image2institutional.png new file mode 100755 index 00000000000..a14fded0cc3 Binary files /dev/null and b/doc/Sphinx/source/img/image2institutional.png differ diff --git a/doc/Sphinx/source/img/image3institutional.png b/doc/Sphinx/source/img/image3institutional.png new file mode 100755 index 00000000000..bc213a656f9 Binary files /dev/null and b/doc/Sphinx/source/img/image3institutional.png differ diff --git a/doc/Sphinx/source/img/image4institutional.png b/doc/Sphinx/source/img/image4institutional.png new file mode 100755 index 00000000000..40f1c03d1d7 Binary files /dev/null and b/doc/Sphinx/source/img/image4institutional.png differ diff --git a/doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/shibtest.dataverse.org.conf b/doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/dataverse.example.edu.conf similarity index 94% rename from doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/shibtest.dataverse.org.conf rename to doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/dataverse.example.edu.conf index ee11f23a268..4e11c99291b 100644 --- a/doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/shibtest.dataverse.org.conf +++ b/doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/dataverse.example.edu.conf @@ -1,6 +1,6 @@ -ServerName shibtest.dataverse.org +ServerName dataverse.example.edu # From https://wiki.apache.org/httpd/RewriteHTTPToHTTPS diff --git a/doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/ssl.conf b/doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/ssl.conf index 13da1d80f36..880f059e63d 100644 --- a/doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/ssl.conf +++ b/doc/sphinx-guides/source/_static/installation/files/etc/httpd/conf.d/ssl.conf @@ -75,7 +75,7 @@ SSLCryptoDevice builtin # General setup for the virtual host, inherited from global configuration #DocumentRoot "/var/www/html" -ServerName shibtest.dataverse.org:443 +ServerName dataverse.example.edu:443 # Use separate log files for the SSL virtual host; note that LogLevel # is not inherited from httpd.conf. @@ -102,14 +102,14 @@ SSLCipherSuite ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW # the certificate is encrypted, then you will be prompted for a # pass phrase. Note that a kill -HUP will prompt again. A new # certificate can be generated using the genkey(1) command. -SSLCertificateFile /etc/pki/tls/certs/shibtest.dataverse.org.crt +SSLCertificateFile /etc/pki/tls/certs/dataverse.example.edu.crt # Server Private Key: # If the key is not combined with the certificate, use this # directive to point at the key file. Keep in mind that if # you've both a RSA and a DSA private key you can configure # both in parallel (to also allow the use of DSA ciphers, etc.) -SSLCertificateKeyFile /etc/pki/tls/private/shibtest.dataverse.org.key +SSLCertificateKeyFile /etc/pki/tls/private/dataverse.example.edu.key # Server Certificate Chain: # Point SSLCertificateChainFile at a file containing the @@ -118,7 +118,7 @@ SSLCertificateKeyFile /etc/pki/tls/private/shibtest.dataverse.org.key # the referenced file can be the same as SSLCertificateFile # when the CA certificates are directly appended to the server # certificate for convinience. -SSLCertificateChainFile /etc/pki/tls/certs/shibtest.dataverse.org_server-chain.crt +SSLCertificateChainFile /etc/pki/tls/certs/dataverse.example.edu_server-chain.crt # Certificate Authority (CA): # Set the CA certificate verification path where to find CA diff --git a/doc/sphinx-guides/source/_static/installation/files/etc/shibboleth/shibGroupTestShib.json b/doc/sphinx-guides/source/_static/installation/files/etc/shibboleth/shibGroupTestShib.json new file mode 100644 index 00000000000..01b2bd51b2d --- /dev/null +++ b/doc/sphinx-guides/source/_static/installation/files/etc/shibboleth/shibGroupTestShib.json @@ -0,0 +1,5 @@ +{ + "name": "All testshib.org Shibboleth Users", + "attribute": "Shib-Identity-Provider", + "pattern": "https://idp.testshib.org/idp/shibboleth" +} diff --git a/doc/sphinx-guides/source/_static/installation/files/etc/shibboleth/shibboleth2.xml b/doc/sphinx-guides/source/_static/installation/files/etc/shibboleth/shibboleth2.xml index e542d3bdcac..5b67396b2be 100644 --- a/doc/sphinx-guides/source/_static/installation/files/etc/shibboleth/shibboleth2.xml +++ b/doc/sphinx-guides/source/_static/installation/files/etc/shibboleth/shibboleth2.xml @@ -12,7 +12,7 @@ https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPConfiguration clockSkew="1800"> - @@ -30,7 +30,7 @@ https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPConfiguration diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 9fb67fc73ee..2a92244e21f 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -239,7 +239,10 @@ Remove a single role assignee from an explicit group:: DELETE http://$server/api/dataverses/$dv/groups/$groupAlias/roleAssignees/$roleAssigneeIdentifier +Shibboleth Groups +~~~~~~~~~~~~~~~~~ +Management of Shibboleth groups via API is documented in the :doc:`/installation/shibboleth` section of the Installation Guide. Metadata Blocks ~~~~~~~~~~~~~~~ @@ -309,7 +312,31 @@ Creates a global role in the Dataverse installation. The data POSTed are assumed POST http://$SERVER/api/admin/roles -Toggles superuser mode on the ``AuthenticatedUser`` whose ``identifier`` is passed. :: +List all users:: + + GET http://$SERVER/api/admin/authenticatedUsers + +List user whose ``identifier`` (without the ``@`` sign) is passed:: + + GET http://$SERVER/api/admin/authenticatedUsers/$identifier + +Sample output using "dataverseAdmin" as the ``identifier``:: + + { + "authenticationProviderId": "builtin", + "persistentUserId": "dataverseAdmin", + "position": "Admin", + "id": 1, + "identifier": "@dataverseAdmin", + "displayName": "Dataverse Admin", + "firstName": "Dataverse", + "lastName": "Admin", + "email": "dataverse@mailinator.com", + "superuser": true, + "affiliation": "Dataverse.org" + } + +Toggles superuser mode on the ``AuthenticatedUser`` whose ``identifier`` (without the ``@`` sign) is passed. :: POST http://$SERVER/api/admin/superuser/$identifier diff --git a/doc/sphinx-guides/source/installation/shibboleth.rst b/doc/sphinx-guides/source/installation/shibboleth.rst index 33802426d41..b8c2c09ef03 100644 --- a/doc/sphinx-guides/source/installation/shibboleth.rst +++ b/doc/sphinx-guides/source/installation/shibboleth.rst @@ -6,30 +6,56 @@ Shibboleth Status: Experimental -------------------- -Shibboleth support in Dataverse should be considered **experimental** until the following issue is closed: https://github.com/IQSS/dataverse/issues/2117 +Shibboleth support in Dataverse should be considered **experimental** until https://github.com/IQSS/dataverse/issues/2117 is closed (indicating that the feature has been in used in production at https://dataverse.harvard.edu for a while), but the `Dataverse development team `_ is eager to receive feedback on the Shibboleth feature (including these docs!) via any channel listed in the :doc:`intro` section. -In Dataverse 4.0, Shibboleth support requires fronting Glassfish with Apache as described below, but this configuration has not been heavily tested in a production environment and is not recommended until this issue is closed: https://github.com/IQSS/dataverse/issues/2180 +Introduction +------------ -System Requirements -------------------- +By configuring and enabling Shibboleth support in Dataverse, your users will be able to log in using the identity system managed by their institution ("single sign on", or at least "single password") rather than having to create yet another password local to your Dataverse installation. Typically, users know their login system by some sort of internal branding such as "HarvardKey" or "Touchstone" (MIT) but within the Dataverse application, the Shibboleth feature is known as "Institutional Log In" as explained to end users in the :doc:`/user/account` section of the User Guide. + +Shibboleth is an implementation of the `Security Assertion Markup Language (SAML) `_ protocol which is similar in spirit to systems used by many webapps that allow you to log in via Google, Facebook, or Twitter. + +Auth Modes: Local vs. Remote vs. Both +------------------------------------- + +There are three valid configurations or modes for authenticating users to Dataverse: -Only Red Hat Enterprise Linux (RHEL) 6 and derivatives such as CentOS have been tested and only on x86_64. RHEL 7 and Centos 7 **should** work but you'll need to adjust the yum repo config accordingly. +- Local only. +- Both local and remote (Shibboleth). +- Remote (Shibboleth) only. -Debian and Ubuntu are not recommended until this issue is closed: https://github.com/IQSS/dataverse/issues/1059 +Out of the box, Dataverse is configured in "local only" mode. The "dataverseAdmin" superuser account mentioned in the :doc:`/installation/installation-main` section is an example of a local account. + +Enabling Shibboleth support results in a second login screen appearing next to the regular login screen. This is "both local and remote" mode. This mode is especially useful if you have external collaborators or if you want to let users who have left your institution to continue to log into your installation of Dataverse. See also the section below about converting users from Shibboleth users to local users when they have left your institution. + +"Remote only" mode means that Shibboleth has been enabled and that ``:AllowSignUp`` is set to "false" per the :doc:`config` section to prevent users from creating local accounts via the web interface. Please note that local accounts can also be created via API, and the way to prevent this is to block the ``builtin-users`` endpoint or scramble (or remove) the ``BuiltinUsers.KEY`` database setting per the :doc:`config` section. The fact that preventing local users from being created does not hide the login screen for local users is being discussed at https://github.com/IQSS/dataverse/issues/2974 Installation ------------ +We assume you've already gone through a basic installation as described in the :doc:`/installation/installation-main` section. + +System Requirements +~~~~~~~~~~~~~~~~~~~ + +Support for Shibboleth in Dataverse is built on the popular `"mod_shib" Apache module, "shibd" daemon `_, and the `Embedded Discovery Service (EDS) `_ Javascript library, all of which are distributed by the `Shibboleth Consortium `_. EDS is bundled with Dataverse, but ``mod_shib`` and ``shibd`` must be installed and configured per below. + +Only Red Hat Enterprise Linux (RHEL) 6 and derivatives such as CentOS have been tested (x86_64 versions) by the Dataverse team. Newer versions of RHEL and CentOS **should** work but you'll need to adjust the yum repo config accordingly. See https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPLinuxInstall for details and note that (according to that page) as of this writing Ubuntu and Debian are not offically supported by the Shibboleth project. + Install Apache ~~~~~~~~~~~~~~ -We include ``mod_ssl`` for HTTPS support. +We will be "fronting" Glassfish with Apache so that we can make use of the ``mod_shib`` Apache module. We will also make use of the ``mod_proxy_ajp`` module built in to Apache. + +We include the ``mod_ssl`` package to enforce HTTPS per below. ``yum install httpd mod_ssl`` Install Shibboleth ~~~~~~~~~~~~~~~~~~ +Installing Shibboleth will give us both the ``shibd`` service and the ``mod_shib`` Apache module. + Enable Shibboleth Yum Repo ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -53,30 +79,32 @@ Configure Glassfish Apply GRIZZLY-1787 Patch ^^^^^^^^^^^^^^^^^^^^^^^^ -In order for the Dataverse "download as zip" feature to work well with large files without causing ``OutOfMemoryError`` problems on Glassfish 4.1, you should stop Glassfish, with ``asadmin stop-domain domain1``, make a backup of ``glassfish4/glassfish/modules/glassfish-grizzly-extra-all.jar``, replace it with a patched version of ``glassfish-grizzly-extra-all.jar`` downloaded from `here <../_static/installation/files/issues/2180/grizzly-patch/glassfish-grizzly-extra-all.jar>`_ (the md5 is in the `README <../_static/installation/files/issues/2180/grizzly-patch/readme.md>`_), and start Glassfish again with ``asadmin start-domain domain1``. +In order for the Dataverse "download as zip" feature to work well with large files without causing ``OutOfMemoryError`` problems on Glassfish 4.1 when fronted with Apache, you should stop Glassfish, with ``asadmin stop-domain domain1``, make a backup of ``glassfish4/glassfish/modules/glassfish-grizzly-extra-all.jar``, replace it with a patched version of ``glassfish-grizzly-extra-all.jar`` downloaded from `here <../_static/installation/files/issues/2180/grizzly-patch/glassfish-grizzly-extra-all.jar>`_ (the md5 is in the `README <../_static/installation/files/issues/2180/grizzly-patch/readme.md>`_), and start Glassfish again with ``asadmin start-domain domain1``. For more background on the patch, please see https://java.net/jira/browse/GRIZZLY-1787 and https://github.com/IQSS/dataverse/issues/2180 and https://github.com/payara/Payara/issues/350 -This problem has been reported to Glassfish at https://java.net/projects/glassfish/lists/users/archive/2015-07/message/1 and when Glassfish 4.2 ships the Dataverse team plans to evaulate if the version of Grizzly included is new enough to include the bug fix, obviating the need for the patch. +This problem has been reported to Glassfish at https://java.net/projects/glassfish/lists/users/archive/2015-07/message/1 and while Glassfish 4.1.1 includes a new enough version of Grizzly to fix the bug, other complicating factors prevent its adoption (look for "Glassfish 4.1.1" in the :doc:`prerequisites` section for details on why it is not recommended). Glassfish HTTP and HTTPS ports ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Ensure that the Glassfish HTTP service is listening on the default port (8080): +Apache will be listening on ports 80 and 443 so we need to make sure Glassfish isn't using them. If you've been changing the default ports used by Glassfish per the :doc:`config` section, revert the Glassfish HTTP service to listen on 8080, the default port: ``asadmin set server-config.network-config.network-listeners.network-listener.http-listener-1.port=8080`` -Ensure that the Glassfish HTTPS service is listening on the default port (8181): +Likewise, if necessary, revert the Glassfish HTTPS service to listen on port 8181: ``asadmin set server-config.network-config.network-listeners.network-listener.http-listener-2.port=8181`` AJP ^^^ -A ``jk-connector`` network listener should have already been set up at installation time and you can verify this with ``asadmin list-network-listeners`` but for reference, here is the command that is used: +A ``jk-connector`` network listener should have already been set up when you ran the installer mentioned in the :doc:`installation-main` section, but for reference, here is the command that is used: ``asadmin create-network-listener --protocol http-listener-1 --listenerport 8009 --jkenabled true jk-connector`` +You can verify this with ``asadmin list-network-listeners``. + This enables the `AJP protocol `_ used in Apache configuration files below. SSLEngine Warning Workaround @@ -92,15 +120,17 @@ Configure Apache Enforce HTTPS ^^^^^^^^^^^^^ -To prevent attacks such as `FireSheep `_, HTTPS should be enforced. https://wiki.apache.org/httpd/RewriteHTTPToHTTPS provides a good method. Here is how it looks in a `sample file <../_static/installation/files/etc/httpd/conf.d/shibtest.dataverse.org.conf>`_ at ``/etc/httpd/conf.d/shibtest.dataverse.org.conf``: +To prevent attacks such as `FireSheep `_, HTTPS should be enforced. https://wiki.apache.org/httpd/RewriteHTTPToHTTPS provides a good method. You **could** copy and paste that those "rewrite rule" lines into Apache's main config file at ``/etc/httpd/conf/httpd.conf`` but using Apache's "virtual hosts" feature is recommended so that you can leave the main configuration file alone and drop a host-specific file into place. -.. literalinclude:: ../_static/installation/files/etc/httpd/conf.d/shibtest.dataverse.org.conf +Below is an example of how "rewrite rule" lines look within a ``VirtualHost`` block. Download a `sample file <../_static/installation/files/etc/httpd/conf.d/dataverse.example.edu.conf>`_ , edit it to substitute your own hostname under ``ServerName``, and place it at ``/etc/httpd/conf.d/dataverse.example.edu.conf`` or a filename that matches your hostname. The file must be in ``/etc/httpd/conf.d`` and must end in ".conf" to be included in Apache's configuration. -Edit Apache Config Files -^^^^^^^^^^^^^^^^^^^^^^^^ -``/etc/httpd/conf.d/ssl.conf`` should contain the FQDN of your hostname like this: ``ServerName shibtest.dataverse.org:443`` +.. literalinclude:: ../_static/installation/files/etc/httpd/conf.d/dataverse.example.edu.conf + +Edit Apache ssl.conf File +^^^^^^^^^^^^^^^^^^^^^^^^^ +``/etc/httpd/conf.d/ssl.conf`` should be edited to contain the FQDN of your hostname like this: ``ServerName dataverse.example.edu:443`` (substituting your hostname). -Near the bottom of ``/etc/httpd/conf.d/ssl.conf`` but before the closing ```` directive add the following: +Near the bottom of ``/etc/httpd/conf.d/ssl.conf`` but before the closing ```` directive, add the following: .. code-block:: text @@ -120,7 +150,7 @@ Near the bottom of ``/etc/httpd/conf.d/ssl.conf`` but before the closing `` -You can download a `sample ssl.conf file <../_static/installation/files/etc/httpd/conf.d/ssl.conf>`_. +You can download a `sample ssl.conf file <../_static/installation/files/etc/httpd/conf.d/ssl.conf>`_ to compare it against the file you edited. Note that ``/etc/httpd/conf.d/shib.conf`` and ``/etc/httpd/conf.d/shibboleth-ds.conf`` are expected to be present from installing Shibboleth via yum. @@ -135,17 +165,44 @@ shibboleth2.xml .. literalinclude:: ../_static/installation/files/etc/shibboleth/shibboleth2.xml :language: xml -attribute-map.xml -^^^^^^^^^^^^^^^^^ +Specific Identity Provider(s) vs. Identity Federation +````````````````````````````````````````````````````` -By default, some attributes ``/etc/shibboleth/attribute-map.xml`` are commented out. Edit the file to enable them. +When configuring the ``MetadataProvider`` section of ``shibboleth2.xml`` you should consider if your users will all come from the same Identity Provider (IdP) or not. -You can download a `sample attribute-map.xml file <../_static/installation/files/etc/shibboleth/attribute-map.xml>`_. +Specific Identity Provider(s) ++++++++++++++++++++++++++++++ -dataverse-idp-metadata.xml -^^^^^^^^^^^^^^^^^^^^^^^^^^ +Most Dataverse installations will probably only want to authenticate users via Shibboleth using their home institution's Identity Provider (IdP). The configuration above in ``shibboleth2.xml`` looks for the metadata for the Identity Providers (IdPs) in a file at ``/etc/shibboleth/dataverse-idp-metadata.xml``. You can download a `sample dataverse-idp-metadata.xml file <../_static/installation/files/etc/shibboleth/dataverse-idp-metadata.xml>`_ and that includes the TestShib IdP from http://testshib.org but you will want to edit this file to include the metadata from the Identity Provider(s) you care about. The identity people at your institution will be able to provide you with this metadata and they will very likely ask for a list of attributes that Dataverse requires, which are listed at :ref:`shibboleth-attributes`. + +Identity Federation ++++++++++++++++++++ + +Rather than specifying individual Identity Provider(s) you may wish to broaden the number of users who can log into your Dataverse installation by registering your Dataverse installation as a Service Provider (SP) within an identity federation. For example, in the United States, users from `hundreds of institutions registered with the "InCommon" identity federation `_ will be able to log into your Dataverse installation if you register it as one of the `thousands of Service Providers registered with InCommon `_. + +The details of how to register with an identity federation are out of scope for this document, but a good starting point may be this list of identity federations across the world: http://www.protectnetwork.org/support/faq/identity-federations + +One of the benefits of using ``shibd`` is that it can be configured to periodically poll your identify federation for updates as new Identity Providers (IdPs) join the federation you've registered with. For the InCommon federation, the following page describes how to download and verify signed InCommon metadata every hour: https://spaces.internet2.edu/display/InCFederation/Shibboleth+Metadata+Config#ShibbolethMetadataConfig-ConfiguretheShibbolethSP + +.. _shibboleth-attributes: + +Shibboleth Attributes +^^^^^^^^^^^^^^^^^^^^^ + +The following attributes are required for a successful Shibboleth login: + +- Shib-Identity-Provider +- eppn +- givenName +- sn +- email -The configuration above looks for the metadata for the Identity Providers (IdPs) in a file at ``/etc/shibboleth/dataverse-idp-metadata.xml``. You can download a `sample dataverse-idp-metadata.xml file <../_static/installation/files/etc/shibboleth/dataverse-idp-metadata.xml>`_ and that includes the TestShib IdP from http://testshib.org . +See also https://www.incommon.org/federation/attributesummary.html and https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPAttributeAccess + +attribute-map.xml +^^^^^^^^^^^^^^^^^ + +By default, some attributes ``/etc/shibboleth/attribute-map.xml`` are commented out. Edit the file to enable them so that all the require attributes come through. You can download a `sample attribute-map.xml file <../_static/installation/files/etc/shibboleth/attribute-map.xml>`_. Disable SELinux ~~~~~~~~~~~~~~~ @@ -161,65 +218,148 @@ After configuration is complete: ``service httpd restart`` -As a sanity check, visit the following URLs for your hostname to make sure you see JSON and XML: +Configure Apache and shibd to Start at Boot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``chkconfig httpd on`` + +``chkconfig shibd on`` + +Verify DiscoFeed and Metadata URLs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- https://shibtest.dataverse.org/Shibboleth.sso/DiscoFeed -- https://shibtest.dataverse.org/Shibboleth.sso/Metadata +As a sanity check, visit the following URLs (substituting your hostname) to make sure you see JSON and XML: -The JSON in ``DiscoFeed`` comes from the list of IdPs in ``/etc/shibboleth/dataverse-idp-metadata.xml`` and will form a dropdown list on the Login Page. +- https://dataverse.example.edu/Shibboleth.sso/DiscoFeed +- https://dataverse.example.edu/Shibboleth.sso/Metadata + +The JSON in ``DiscoFeed`` comes from the list of IdPs you configured in the ``MetadataProvider`` section of ``shibboleth2.xml`` and will form a dropdown list on the Login Page. Enable Shibboleth ~~~~~~~~~~~~~~~~~ ``curl -X PUT -d true http://localhost:8080/api/admin/settings/:ShibEnabled`` -After enabling Shibboleth, assuming the ``DiscoFeed`` is working per above, you should see a list of institutions to log into. You will not be able to log in via these institutions, however, until you have exchanged metadata with them. +After enabling Shibboleth, assuming the ``DiscoFeed`` is working per above, you should see a list of institutions to log into. You will not be able to log in via these institutions, however, until you have exchanged metadata with them. You can change the boolean above to ``false`` while you wait for the metadata exchange to be complete since it only affects if the Shibboleth login screen is shown. -.. _shibboleth-attributes: +Exchange Metadata with Your Identity Provider +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Shibboleth Attributes ---------------------- +http://testshib.org (TestShib) is a fantastic resource for testing Shibboleth configurations. Depending on your relationship with your identity people you may want to avoid bothering them until you have tested your Dataverse configuration with the TestShib Identity Provider (IdP). This process is explained below. -The following attributes are required for a successful Shibboleth login: +If you've temporarily configured your ``MetadataProvider`` to use the TestShib Identity Provider (IdP) as outlined above, you can download your metadata like this (substituting your hostname in both places): -- Shib-Identity-Provider -- eppn -- givenName -- sn -- email +``curl https://dataverse.example.edu/Shibboleth.sso/Metadata > dataverse.example.edu`` -See also https://www.incommon.org/federation/attributesummary.html and https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPAttributeAccess +Then upload your metadata to http://testshib.org/register.html -For discussion of "eppn" vs. other attributes such as "eduPersonTargetedID" or "NameID", please see https://github.com/IQSS/dataverse/issues/1422 +Then try to log in to Dataverse using the TestShib IdP. After logging in, you can visit the https://dataverse.example.edu/Shibboleth.sso/Session (substituting your hostname) to troubleshoot which attributes are being received. You should see something like the following: -Testing -------- +.. code-block:: none -http://testshib.org is a fantastic resource for testing Shibboleth configurations. + Miscellaneous + Session Expiration (barring inactivity): 479 minute(s) + Client Address: 65.112.10.82 + SSO Protocol: urn:oasis:names:tc:SAML:2.0:protocol + Identity Provider: https://idp.testshib.org/idp/shibboleth + Authentication Time: 2016-03-08T13:45:10.922Z + Authentication Context Class: urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + Authentication Context Decl: (none) -First, download your metadata like this (substituting your hostname in both places): + Attributes + affiliation: Member@testshib.org;Staff@testshib.org + cn: Me Myself And I + entitlement: urn:mace:dir:entitlement:common-lib-terms + eppn: myself@testshib.org + givenName: Me Myself + persistent-id: https://idp.testshib.org/idp/shibboleth!https://dataverse.example.edu/sp!RuyCiLvUcgmKqyh/rOQPh+wyR7s= + sn: And I + telephoneNumber: 555-5555 + uid: myself + unscoped-affiliation: Member;Staff -``curl https://shibtest.dataverse.org/Shibboleth.sso/Metadata > shibtest.dataverse.org`` +(As of this writing the TestShib IdP does not send the "mail" attribute, a required attribute, but for testing purposes, Dataverse compensates for this for the TestShib IdP and permits login anyway.) -Then upload it to http://testshib.org/register.html +When you are done testing, you can delete the TestShib users you created like this (after you have deleted any data and permisions associated with the users): -Then try to log in. +``curl -X DELETE http://localhost:8080/api/admin/authenticatedUsers/myself`` -Please note that when you try logging in via the TestShib IdP, it is expected that you'll see errors about the "mail" attribute being null. (TestShib is aware of this but it isn't a problem for testing.) +(Of course, you are also welcome to do a fresh reinstall per the :doc:`installation-main` section.) -When you are done testing, you can delete the TestShib users you created like this: +If your Dataverse installation is working with TestShib it **should** work with your institution's Identity Provider (IdP). Next, you should: -``curl -X DELETE http://localhost:8080/api/admin/authenticatedUsers/myself`` +- Send your identity people your metadata file above (or a link to download it themselves). From their perspective you are a Service Provider (SP). +- Ask your identity people to send you the metadata for the Identity Provider (IdP) they operate. See the section above on ``shibboleth2.xml`` and ``MetadataProvider`` for what to do with the IdP metadata. Restart ``shibd`` and ``httpd`` as necessary. +- Test login to Dataverse via your institution's Identity Provider (IdP). + +Administration +-------------- + +Institution-Wide Shibboleth Groups +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Dataverse allows you to optionally define "institution-wide Shibboleth groups" based on the the entityID of the Identity Provider (IdP) used to authenticate. For example, an "institution-wide Shibboleth group" with ``https://idp.testshib.org/idp/shibboleth`` as the IdP would include everyone who logs in via the TestShib IdP mentioned above. + +To create an institution-wide Shibboleth groups, create a JSON file as below and issue this curl command: ``curl http://localhost:8080/api/admin/groups/shib -X POST -H 'Content-type:application/json' --upload-file shibGroupTestShib.json`` + +.. literalinclude:: ../_static/installation/files/etc/shibboleth/shibGroupTestShib.json + +Note that institution-wide Shibboleth groups are based on the "Shib-Identity-Provider" attribute but https://github.com/IQSS/dataverse/issues/1515 tracks adding support for arbitrary attributes such as ""eduPersonScopedAffiliation", etc. + +To list institution-wide Shibboleth groups: ``curl http://localhost:8080/api/admin/groups/shib`` + +To delete an institution-wide Shibboleth group (assuming id 1): ``curl -X DELETE http://localhost:8080/api/admin/groups/shib/1`` + +Converting Local Users to Shibboleth +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you are running in "remote and local" mode and have existing local users that you'd like to convert to Shibboleth users, give them the following steps to follow: + +- Log in with your local account to make sure you know your password, which will be needed for the account conversion process. +- Log out of your local account. +- Log in with your Shibboleth account. +- If the email address associated with your local account matches the email address asserted by the Identity Provider (IdP), you will be prompted for the password of your local account and asked to confirm the conversion of your account. You're done! Browse around to ensure you see all the data you expect to see. Permissions have been preserved. +- If the email address asserted by the Identity Provider (IdP) does not match the email address of any local user, you will be prompted to create a new account. If you were expecting account conversion, you should decline creating a new Shibboleth account, log back in to your local account, and let Support know the email on file for your local account. Support may ask you to change your email address for your local account to the one that is being asserted by the Identity Provider. Someone with access to the Glassfish logs will see this email address there. + +Converting Shibboleth Users to Local +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Whereas users convert their own accounts from local to Shibboleth as described above, conversion in the opposite direction is performed by a sysadmin. A common scenario may be as follows: + +- A user emails Support saying, "I left the university (or wherever) and can't log in to Dataverse anymore. What should I do?" +- Support replies asking the user for a new email address (Gmail, new institution email, etc.) to associate with their Dataverse account. +- The user replies with a new email address to associate with their Dataverse account. +- Support runs the curl command below, supplying the database id of the user to convert and the new email address and notes the username returned. +- Support emails the user and indicates that that they should use the password reset feature to set a new password and to make sure to take note of their username under Account Information (or the password reset confirmation email) since the user never had a username before. +- The user resets password and is able to log in with their local account. All permissions have been preserved with the exception of any permissions assigned to an institution-wide Shibboleth group to which the user formerly belonged. + +In the example below, the user has indicated that the new email address they'd like to have associated with their account is "former.shib.user@mailinator.com" and their user id from the ``authenticateduser`` database table is "2". The API token must belong to a superuser (probably the sysadmin executing the command). + +``curl -H "X-Dataverse-key: $API_TOKEN" -X PUT -d "former.shib.user@mailinator.com" http://localhost:8080/api/admin/authenticatedUsers/id/2/convertShibToBuiltIn`` + +Rather than looking up the user's id in the ``authenticateduser`` database table, you can issue this command to get a listing of all users: + +``curl -H "X-Dataverse-key: $API_TOKEN" http://localhost:8080/api/admin/authenticatedUsers`` + +Per above, you now need to tell the user to use the password reset feature to set a password for their local account. + +Debugging +~~~~~~~~~ + +The :doc:`administration` section explains how to increase Glassfish logging levels. The relevant classes and packages are: + +- edu.harvard.iq.dataverse.Shib +- edu.harvard.iq.dataverse.authorization.providers.shib +- edu.harvard.iq.dataverse.authorization.groups.impl.shib Backups -------- +~~~~~~~ -It's important to back up these files: +The installation and configuration of Shibboleth will result in the following cert and key files being created and it's important to back them up: - ``/etc/shibboleth/sp-cert.pem`` - ``/etc/shibboleth/sp-key.pem`` -Feedback --------- +If you have more than one Glassfish server, you should use the same ``sp-cert.pem`` and ``sp-key.pem`` files on all of them. If these files are compromised and you need to regenerate them, you can ``cd /etc/shibboleth`` and run ``keygen.sh`` like this (substituting you own hostname): -Given that Shibboleth support is experimental and new, feedback is very welcome at support@dataverse.org or via http://community.dataverse.org/community-groups/auth.html . +``./keygen.sh -f -u shibd -g shibd -h dataverse.example.edu -e https://dataverse.example.edu/sp`` diff --git a/doc/sphinx-guides/source/user/account.rst b/doc/sphinx-guides/source/user/account.rst index 5473a0cc5ef..4045bf58188 100755 --- a/doc/sphinx-guides/source/user/account.rst +++ b/doc/sphinx-guides/source/user/account.rst @@ -3,10 +3,10 @@ Account Creation & Management As a registered user, you can: -- Create your own dataverse and customize it. +- Create your own dataverse and customize it - Add datasets to dataverses, if available - Contribute to existing datasets, if available -- Request access to restricted files, if available. +- Request access to restricted files, if available Create User Account =================== @@ -17,18 +17,50 @@ Create User Account Edit Your Account ================== #. To edit your account after you have logged in, click on your account name in the header on the right hand side and click on Account Information. -#. On the top right of your account page, click on the "Edit Account" button and from there you can select to edit either your Account Information or your Account Password. +#. On the top right of your account page, click on the "Edit Account" button and from there you can select to edit either your Account Information or your Account Password. #. Select "Save Changes" when you are done. -Generate Your API Token +Institutional Log In ======================== -#. To generate your API token, click on your name in the hearder on right hand side and then click on Account Information. -#. On the top right of your account page, click on the "Edit Account" button and click on API Token in the list. -#. Your API Token is located on that page. + + +Institutional log in allows you to use your log in information for your university (e.g. HarvardKey at Harvard) to log into your Dataverse account. By using your institutional log in, you won't have to remember your password for Dataverse or manage another account. + +How to create a new Dataverse account using Institutional Log In +------------------------------------------------------------------------------------------------- + +#. Go to “Log In” in the upper right corner of Dataverse. +#. Find this Institutional Log In box:|image1| +#. Using the dropdown menu, select your institution then click the Continue button. +#. You will be brought to your institution's log in page. After you put in your institutional information successfully, you will be brought back to Dataverse to confirm your account. |image2| + +How to use your Institutional Log In for your Dataverse account +------------------------------------------------------------------------------------------------- + +If you already have a Dataverse account, but you want to change it to use your institutional log in, you can easily do so as long as your account uses an email address from that institution. + +#. Go to the Account Information page to confirm your account email address is the same as your institution email address. If not, you will need to update your account email address to be the same as your institution email address. +#. Log out of Dataverse. +#. Go to “Log In” in the upper right corner of Dataverse. +#. Find this Institutional Log In box: |image1| +#. Using the dropdown menu, select your institution then click the Continue button. +#. You will be brought to your institution's log in page. After you successfully input your institutional information, you will be brought back to Dataverse to review your account information. Enter your previous password for your Dataverse account to ensure that you have changed your login. |image3| +#. Once you click Change/Convert Account, you will have completed changing your Dataverse account to you use your institutional log in. + +How to change your Dataverse account to no longer use Institutional Log In +------------------------------------------------------------------------------------------------- + +If you are leaving your institution and need to change your account back to a Dataverse account, you will need to contact support for the Dataverse installation you are using. On your account page, there is a link that will open the contact form for Support: |image4| + + +Create Your API Token +======================== +#. To create your API token, click on your name in the header on right hand side and then click on API Token. +#. In this tab, you can create your API Token for the first time as well as recreate it if you need a new API Token or your API Token becomes compromised. My Data ======================== -The My Data section of your account page displays a listing of all the dataverses, datasets, and files you have either created, uploaded or that you have access to edit. You are able to filter through all the dataverses, datasets, and files listed there using the filter box or use the facets on the left side to only view a specific Publication Status or Role. +The My Data section of your account page displays a listing of all the dataverses, datasets, and files you have either created, uploaded or that you have access to edit. You are able to filter through all the dataverses, datasets, and files listed there using the filter box. You may also use the facets on the left side to only view a specific Publication Status or Role. Notifications: Setup & Maintainance =================================== @@ -44,6 +76,12 @@ Dataverse will email your unread notifications once a day. Notifications will on Reset Your Account Password ============================== -If you cannot remember the password for your Dataverse account, click on Log In in the top right corner of any page. Once on that page, click on the Forgot Password? link below where you would enter your username and password. Enter your email address and click Submit Password Request to receive an email with a link to reset your password. +If you cannot remember the password for your Dataverse account, click on Log In in the top right corner of any page. Once on that page, click on the "Forgot Password?" link below where you would enter your username and password. Enter your email address and click "Submit Password Request" to receive an email with a link to reset your password. \*Note: if you have forgotten your username, you can do this same process to receive your username in an email. + + +.. |image1| image:: ./img/image1institutional.png +.. |image2| image:: ./img/image2institutional.png +.. |image3| image:: ./img/image3institutional.png +.. |image4| image:: ./img/image4institutional.png diff --git a/doc/sphinx-guides/source/user/img/image1institutional.png b/doc/sphinx-guides/source/user/img/image1institutional.png new file mode 100755 index 00000000000..39852ea1c23 Binary files /dev/null and b/doc/sphinx-guides/source/user/img/image1institutional.png differ diff --git a/doc/sphinx-guides/source/user/img/image2institutional.png b/doc/sphinx-guides/source/user/img/image2institutional.png new file mode 100755 index 00000000000..a14fded0cc3 Binary files /dev/null and b/doc/sphinx-guides/source/user/img/image2institutional.png differ diff --git a/doc/sphinx-guides/source/user/img/image3institutional.png b/doc/sphinx-guides/source/user/img/image3institutional.png new file mode 100755 index 00000000000..bc213a656f9 Binary files /dev/null and b/doc/sphinx-guides/source/user/img/image3institutional.png differ diff --git a/doc/sphinx-guides/source/user/img/image4institutional.png b/doc/sphinx-guides/source/user/img/image4institutional.png new file mode 100755 index 00000000000..40f1c03d1d7 Binary files /dev/null and b/doc/sphinx-guides/source/user/img/image4institutional.png differ diff --git a/pom.xml b/pom.xml index 31c1f634327..288f2f7fb62 100644 --- a/pom.xml +++ b/pom.xml @@ -318,6 +318,11 @@ jacoco-maven-plugin 0.7.5.201505241946 + + org.mockito + mockito-core + 1.10.19 + org.slf4j diff --git a/src/main/java/Bundle.properties b/src/main/java/Bundle.properties index 6038574424c..b1bdfa477e7 100755 --- a/src/main/java/Bundle.properties +++ b/src/main/java/Bundle.properties @@ -72,7 +72,9 @@ header.logOut=Log Out header.accountInfo=Account Information header.user.selectTab.dataRelated=My Data header.user.selectTab.notifications=Notifications +header.user.selectTab.accountInfo=Account Information header.user.selectTab.groupsAndRoles=Groups + Roles +header.user.selectTab.apiToken=API Token # dataverse_template.xhtml @@ -116,8 +118,10 @@ contact.contact=Contact account.info=Account Information account.edit=Edit Account -apiTaken=API Token -user.toEditDetail=You are logged in through your institution. If you need to update any of this information, please contact your institution. +account.apiToken=API Token +user.isShibUser=Account information cannot be edited when logged in through an institutional account. +user.helpShibUserMigrateOffShibBeforeLink=Leaving your institution? Please contact +user.helpShibUserMigrateOffShibAfterLink=for assistance. user.lostPasswdTip=If you have lost or forgotten your password, please enter your username or email address below and click Submit. We will send you an e-mail with your new password. user.dataRelatedToMe=My Data wasCreatedIn=, was created in @@ -181,7 +185,11 @@ user.updatePassword.password=Create a password that is minimum six characters lo authenticationProvidersAvailable.tip={0}There are no active authentication providers{1}If you are a system administrator, please enable one using the API.{2}If you are not a system administrator, please contact the one for your institution. login.System=Login System login.forgot.text=Forgot your password? -login.institution=Institution Log In +login.builtin=Dataverse Account Log In +login.institution=Institutional Log In +login.institution.blurb=Use Dataverse with your institutional log in instead of creating an account. Learn More. +login.institution.support.beforeLink=Leaving your institution? Please contact +login.institution.support.afterLink=for assistance. login.builtin.credential.usernameOrEmail=Username/Email login.builtin.credential.password=Password login.builtin.invalidUsernameEmailOrPassword=The username, email address, or password you entered is invalid. Need assistance accessing your account? @@ -189,21 +197,25 @@ login.builtin.invalidUsernameEmailOrPassword=The username, email address, or pas login.error=Error validating the username, email address, or password. Please try again. If the problem persists, contact an administrator. #shib.xhtml -shib.btn.acceptAndConvert=Accept Terms and Convert Account -shib.btn.acceptAndCreate=Accept Terms and Create Account -shib.welcome=Welcome, -shib.welcomeExistingUserMessage=The email provided by {0} authentication matches an existing Dataverse account. If you would like to associate your existing Dataverse account with {0} authentication, please enter the password of your existing Dataverse account, review the General Terms of Use, and then click the Accept Terms and Convert Account button. +shib.btn.convertAccount=Convert Account +shib.btn.createAccount=Create Account +shib.askToConvert=Would you like to convert your Dataverse account to always use your institutional log in? +# Bundle file editors, please note that "shib.welcomeExistingUserMessage" is used in a unit test +shib.welcomeExistingUserMessage=Your institutional log in for {0} matches an email address already being used for a Dataverse account. By entering your current Dataverse password below, your existing Dataverse account can be converted to use your institutional log in. After converting, you will only need to use your institutional log in. +# Bundle file editors, please note that "shib.welcomeExistingUserMessageDefaultInstitution" is used in a unit test +shib.welcomeExistingUserMessageDefaultInstitution=your institution shib.dataverseUsername=Dataverse Username shib.currentDataversePassword=Current Dataverse Password shib.accountInformation=Account Information -shib.offerToCreateNewAccount=Please agree to the Dataverse Terms of Use to create your account. -shib.passwordRejected=Your account can only be converted if you provide the correct password for your existing account. +shib.offerToCreateNewAccount=This information is provided by your institution and will be used to create your Dataverse account. +shib.passwordRejected=Validation Error - Your account can only be converted if you provide the correct password for your existing account. -#apitoken.xhtml +# tab on dataverseuser.xhtml apitoken.title=API Token -apitoken.message=Here is your API Token. Check out our {0}API Guide{1} for more information. -apitoken.generateBtn=Generate Token -apitoken.regenerateBtn=Regenerate Token +apitoken.message=Your API Token is displayed below after it has been created. Check out our {0}API Guide{1} for more information on using your API Token with the Dataverse APIs. +apitoken.notFound=API Token for {0} has not been created. +apitoken.generateBtn=Create Token +apitoken.regenerateBtn=Recreate Token #MailServiceBean.java diff --git a/src/main/java/edu/harvard/iq/dataverse/ApiTokenPage.java b/src/main/java/edu/harvard/iq/dataverse/ApiTokenPage.java index 2170d01bb10..b2ee4299c2a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/ApiTokenPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/ApiTokenPage.java @@ -3,16 +3,21 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import static edu.harvard.iq.dataverse.util.JsfHelper.JH; +import edu.harvard.iq.dataverse.util.BundleUtil; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Calendar; +import java.util.List; import java.util.logging.Logger; import javax.ejb.EJB; -import javax.faces.application.FacesMessage; import javax.faces.view.ViewScoped; import javax.inject.Inject; import javax.inject.Named; +/** + * @todo Rename this to ApiTokenFragment? The separate page is being taken out + * per https://github.com/IQSS/dataverse/issues/3086 + */ @ViewScoped @Named("ApiTokenPage") public class ApiTokenPage implements java.io.Serializable { @@ -45,9 +50,12 @@ public String getApiToken() { if (apiToken != null) { return apiToken.getTokenString(); } else { - return "API token for " + au.getName() + " not found"; + List arguments = new ArrayList<>(); + arguments.add(au.getName()); + return BundleUtil.getStringFromBundle("apitoken.notFound", arguments); } } else { + // It should be impossible to get here from the UI. return "Only authenticated users can have API tokens."; } diff --git a/src/main/java/edu/harvard/iq/dataverse/DataverseHeaderFragment.java b/src/main/java/edu/harvard/iq/dataverse/DataverseHeaderFragment.java index d12378efa9f..7e5f38005b7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DataverseHeaderFragment.java +++ b/src/main/java/edu/harvard/iq/dataverse/DataverseHeaderFragment.java @@ -265,31 +265,4 @@ public DvObject getDvObject() { } } - - private Boolean debugShibboleth = null; - - public boolean isDebugShibboleth() { - if (debugShibboleth != null) { - return debugShibboleth; - } - debugShibboleth = systemConfig.isDebugEnabled(); - return debugShibboleth; - } - - public List getGroups(User user) { - List groups = new ArrayList<>(); - Set groupsForUser = groupService.groupsFor(user, null); - for (Group group : groupsForUser) { - groups.add(group.getDisplayName() + " (" + group.getIdentifier() + ")"); - } - return groups; - } - - public List getPermissions(User user, Dataverse dataverse) { - List permissions = new ArrayList<>(); - for (Permission permission : permissionService.permissionsFor(user, dataverse)) { - permissions.add(permission.name()); - } - return permissions; - } } diff --git a/src/main/java/edu/harvard/iq/dataverse/EMailValidator.java b/src/main/java/edu/harvard/iq/dataverse/EMailValidator.java index a0bf2319dca..a42453473ec 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EMailValidator.java +++ b/src/main/java/edu/harvard/iq/dataverse/EMailValidator.java @@ -33,9 +33,18 @@ public static boolean isEmailValid(String value, ConstraintValidatorContext cont //we'll let someone else decide if it's required return true; } + /** + * @todo Why are we validating the trimmed value rather than the value + * itself? Which are we persisting to the database, the trimmed value or + * the non-trimmed value? See also + * https://github.com/IQSS/dataverse/issues/2945 and + * https://github.com/IQSS/dataverse/issues/3044 + */ boolean isValid = EmailValidator.getInstance().isValid(value.trim()); if (!isValid) { - context.buildConstraintViolationWithTemplate(value + " is not a valid email address.").addConstraintViolation(); + if (context != null) { + context.buildConstraintViolationWithTemplate(value + " is not a valid email address.").addConstraintViolation(); + } return false; } return true; diff --git a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java index de286c1dec1..b8998932429 100644 --- a/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/RoleAssigneeServiceBean.java @@ -11,6 +11,8 @@ import edu.harvard.iq.dataverse.authorization.groups.impl.explicit.ExplicitGroupServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; +import edu.harvard.iq.dataverse.search.IndexServiceBean; +import edu.harvard.iq.dataverse.search.SearchFields; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -43,10 +45,10 @@ public class RoleAssigneeServiceBean { @EJB GroupServiceBean groupSvc; - + @EJB - ExplicitGroupServiceBean explicitGroupSvc; - + ExplicitGroupServiceBean explicitGroupSvc; + @EJB DataverseRoleServiceBean dataverseRoleService; @@ -72,7 +74,7 @@ public RoleAssignee getRoleAssignee(String identifier) { throw new IllegalArgumentException("Unsupported assignee identifier '" + identifier + "'"); } } - + public List getAssignmentsFor(String roleAssigneeIdentifier) { return em.createNamedQuery("RoleAssignment.listByAssigneeIdentifier", RoleAssignment.class) .setParameter("assigneeIdentifier", roleAssigneeIdentifier) @@ -92,141 +94,157 @@ public List getExplicitUsers(RoleAssignee ra) { return explicitUsers; } - - private String getRoleIdListClause(List roleIdList){ - if (roleIdList == null){ + + private String getRoleIdListClause(List roleIdList) { + if (roleIdList == null) { return ""; } List outputList = new ArrayList<>(); - - for(Long r : roleIdList){ - if (r != null){ + + for (Long r : roleIdList) { + if (r != null) { outputList.add(r.toString()); } } - if (outputList.isEmpty()){ + if (outputList.isEmpty()) { return ""; - } - return " AND r.role_id IN (" + StringUtils.join(outputList, ",") + ")"; + } + return " AND r.role_id IN (" + StringUtils.join(outputList, ",") + ")"; } - - public List getAssigneeDataverseRoleFor(String roleAssigneeIdentifier){ - if (roleAssigneeIdentifier==null){ + public List getAssigneeDataverseRoleFor(AuthenticatedUser au) { + String roleAssigneeIdentifier = "@" + au.getUserIdentifier(); + if (roleAssigneeIdentifier == null) { return null; } - List retList = new ArrayList(); - roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s",""); // remove spaces from string - List userGroups = getUserGroups(roleAssigneeIdentifier.replace("@", "")); + List retList = new ArrayList(); + roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s", ""); // remove spaces from string + List userGroups = getUserExplicitGroups(roleAssigneeIdentifier.replace("@", "")); + List userRunTimeGroups = getUserRuntimeGroups(au); String identifierClause = " WHERE r.assigneeIdentifier= '" + roleAssigneeIdentifier + "'"; - if (userGroups != null && !userGroups.isEmpty()){ - identifierClause = getGroupIdentifierClause(roleAssigneeIdentifier, userGroups); - } - + if (userGroups != null || userRunTimeGroups != null) { + + identifierClause = getGroupIdentifierClause(roleAssigneeIdentifier, userGroups, userRunTimeGroups); + } + String qstr = "SELECT distinct r.role_id"; qstr += " FROM RoleAssignment r"; qstr += identifierClause; qstr += ";"; msg("qstr: " + qstr); - - for (Object o :em.createNativeQuery(qstr).getResultList()){ - retList.add(dataverseRoleService.find((Long) o)); + for (Object o : em.createNativeQuery(qstr).getResultList()) { + retList.add(dataverseRoleService.find((Long) o)); } return retList; } - - - - - - public List getAssigneeAndRoleIdListFor(String roleAssigneeIdentifier, List roleIdList){ - - if (roleAssigneeIdentifier==null){ + + public List getAssigneeAndRoleIdListFor(AuthenticatedUser au, List roleIdList) { + + String roleAssigneeIdentifier = "@" + au.getUserIdentifier(); + + if (roleAssigneeIdentifier == null) { return null; } - roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s",""); // remove spaces from string - List userGroups = getUserGroups(roleAssigneeIdentifier.replace("@", "")); + roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s", ""); // remove spaces from string + List userExplicitGroups = getUserExplicitGroups(roleAssigneeIdentifier.replace("@", "")); + List userRunTimeGroups = getUserRuntimeGroups(au); String identifierClause = " WHERE r.assigneeIdentifier= '" + roleAssigneeIdentifier + "'"; - if (userGroups != null && !userGroups.isEmpty()){ - identifierClause = getGroupIdentifierClause(roleAssigneeIdentifier, userGroups); - } - + if (userExplicitGroups != null || userRunTimeGroups != null) { + identifierClause = getGroupIdentifierClause(roleAssigneeIdentifier, userExplicitGroups, userRunTimeGroups); + } + String qstr = "SELECT r.definitionpoint_id, r.role_id"; qstr += " FROM RoleAssignment r"; qstr += identifierClause; qstr += getRoleIdListClause(roleIdList); qstr += ";"; msg("qstr: " + qstr); - return em.createNativeQuery(qstr) - .getResultList(); - + .getResultList(); + } - - public List getRoleIdListForGivenAssigneeDvObject(String roleAssigneeIdentifier, List roleIdList, Long defPointId){ - if (roleAssigneeIdentifier==null){ + public List getRoleIdListForGivenAssigneeDvObject(AuthenticatedUser au, List roleIdList, Long defPointId) { + String roleAssigneeIdentifier = "@" + au.getUserIdentifier(); + if (roleAssigneeIdentifier == null) { return null; } - roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s",""); // remove spaces from string - List userGroups = getUserGroups(roleAssigneeIdentifier.replace("@", "")); + roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s", ""); // remove spaces from string + List userGroups = getUserExplicitGroups(roleAssigneeIdentifier.replace("@", "")); + List userRunTimeGroups = getUserRuntimeGroups(au); String identifierClause = " WHERE r.assigneeIdentifier= '" + roleAssigneeIdentifier + "'"; - if (userGroups != null && !userGroups.isEmpty()){ - identifierClause = getGroupIdentifierClause(roleAssigneeIdentifier, userGroups); - } - + if (userGroups != null || userRunTimeGroups != null) { + identifierClause = getGroupIdentifierClause(roleAssigneeIdentifier, userGroups, userRunTimeGroups); + } + String qstr = "SELECT r.role_id"; qstr += " FROM RoleAssignment r"; qstr += identifierClause; qstr += getRoleIdListClause(roleIdList); - qstr += " and r.definitionpoint_id = " + defPointId; + qstr += " and r.definitionpoint_id = " + defPointId; qstr += ";"; msg("qstr: " + qstr); return em.createNativeQuery(qstr) - .getResultList(); - + .getResultList(); + } - - - private String getGroupIdentifierClause(String roleAssigneeIdentifier, List userGroups) { - if (userGroups == null) { + private String getGroupIdentifierClause(String roleAssigneeIdentifier, List userExplicitGroups, List userRunTimeGroups) { + + if (userExplicitGroups == null && userRunTimeGroups == null) { return ""; } - List outputList = new ArrayList<>(); + List outputExplicitList = new ArrayList<>(); + String explicitString = ""; - for (String r : userGroups) { - if (r != null) { - outputList.add(r); + if (userExplicitGroups != null) { + for (String r : userExplicitGroups) { + if (r != null) { + outputExplicitList.add(r); + } } + + if (!outputExplicitList.isEmpty()) { + explicitString = ",'&explicit/" + StringUtils.join(outputExplicitList, "','&explicit/") + "'"; + } + } - if (outputList.isEmpty()) { - return ""; - } - return " WHERE r.assigneeIdentifier in ( '" + roleAssigneeIdentifier + "', '&explicit/" + StringUtils.join(outputList, "','&explicit/") + "')"; - } + List outputRuntimeList = new ArrayList<>(); + String runTimeString = ""; + if (userRunTimeGroups != null) { + for (String r : userRunTimeGroups) { + if (r != null) { + outputRuntimeList.add(r); + } + } - public List getRoleIdsFor(String roleAssigneeIdentifier, List dvObjectIdList){ + if (!outputRuntimeList.isEmpty()) { + runTimeString = ",'" + StringUtils.join(outputRuntimeList, "','") + "'"; + } - if (roleAssigneeIdentifier==null){ - return null; } - if ((dvObjectIdList==null)||(dvObjectIdList.isEmpty())){ + return " WHERE r.assigneeIdentifier in ( '" + roleAssigneeIdentifier + "'" + explicitString + runTimeString + ")"; + + } + + public List getRoleIdsFor(AuthenticatedUser au, List dvObjectIdList) { + String roleAssigneeIdentifier = "@" + au.getUserIdentifier(); + if (roleAssigneeIdentifier == null) { return null; } - roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s",""); // remove spaces from string - List userGroups = getUserGroups(roleAssigneeIdentifier.replace("@", "")); - + + roleAssigneeIdentifier = roleAssigneeIdentifier.replaceAll("\\s", ""); // remove spaces from string + List userGroups = getUserExplicitGroups(roleAssigneeIdentifier.replace("@", "")); + List userRunTimeGroups = getUserRuntimeGroups(au); String identifierClause = " WHERE r.assigneeIdentifier= '" + roleAssigneeIdentifier + "'"; - if (userGroups != null && !userGroups.isEmpty()){ - identifierClause = getGroupIdentifierClause(roleAssigneeIdentifier, userGroups); - } - - + if (userGroups != null || userRunTimeGroups != null) { + identifierClause = getGroupIdentifierClause(roleAssigneeIdentifier, userGroups, userRunTimeGroups); + } + String qstr = "SELECT r.definitionpoint_id, r.role_id"; qstr += " FROM RoleAssignment r"; qstr += identifierClause; @@ -235,31 +253,29 @@ public List getRoleIdsFor(String roleAssigneeIdentifier, List dv //msg("qstr: " + qstr); return em.createNativeQuery(qstr) - .getResultList(); - + .getResultList(); + } - - - private String getDvObjectIdListClause(List dvObjectIdList){ - if (dvObjectIdList == null){ + + private String getDvObjectIdListClause(List dvObjectIdList) { + if (dvObjectIdList == null) { return ""; } List outputList = new ArrayList<>(); - - for(Long r : dvObjectIdList){ - if (r != null){ + + for (Long r : dvObjectIdList) { + if (r != null) { outputList.add(r.toString()); } } - if (outputList.isEmpty()){ + if (outputList.isEmpty()) { return ""; - } - return " AND r.definitionpoint_id IN (" + StringUtils.join(outputList, ",") + ")"; + } + return " AND r.definitionpoint_id IN (" + StringUtils.join(outputList, ",") + ")"; } - - - private List getUserGroups(String roleAssigneeIdentifier){ - + + private List getUserExplicitGroups(String roleAssigneeIdentifier) { + String qstr = "select groupalias from explicitgroup"; qstr += " where id in "; qstr += " (select explicitgroup_id from explicitgroup_authenticateduser where containedauthenticatedusers_id = "; @@ -268,10 +284,26 @@ private List getUserGroups(String roleAssigneeIdentifier){ //msg("qstr: " + qstr); return em.createNativeQuery(qstr) - .getResultList(); + .getResultList(); } - - + + private List getUserRuntimeGroups(AuthenticatedUser au) { + List retVal = new ArrayList(); + + Set groups = groupSvc.groupsFor(au, null); + StringBuilder sb = new StringBuilder(); + for (Group group : groups) { + logger.fine("found group " + group.getIdentifier() + " with alias " + group.getAlias()); + if (group.getGroupProvider().getGroupProviderAlias().equals("shib") || group.getGroupProvider().getGroupProviderAlias().equals("ip")) { + String groupAlias = group.getAlias(); + if (groupAlias != null && !groupAlias.isEmpty()) { + retVal.add('&' + groupAlias); + } + } + } + return retVal; + } + public List filterRoleAssignees(String query, DvObject dvObject, List roleAssignSelectedRoleAssignees) { List roleAssigneeList = new ArrayList<>(); @@ -283,7 +315,7 @@ public List filterRoleAssignees(String query, DvObject dvObject, L .filter(ra -> roleAssignSelectedRoleAssignees == null || !roleAssignSelectedRoleAssignees.contains(ra)) .forEach((ra) -> { roleAssigneeList.add(ra); - }); + }); // now we add groups to the list, both global and explicit Set groups = groupSvc.findGlobalGroups(); @@ -298,15 +330,15 @@ public List filterRoleAssignees(String query, DvObject dvObject, L return roleAssigneeList; } - - private void msg(String s){ + + private void msg(String s) { //System.out.println(s); } - - private void msgt(String s){ + + private void msgt(String s) { msg("-------------------------------"); msg(s); msg("-------------------------------"); } - + } diff --git a/src/main/java/edu/harvard/iq/dataverse/Shib.java b/src/main/java/edu/harvard/iq/dataverse/Shib.java index 74a539e3357..6767ff59ec7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Shib.java +++ b/src/main/java/edu/harvard/iq/dataverse/Shib.java @@ -1,39 +1,26 @@ package edu.harvard.iq.dataverse; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonIOException; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserIdentifier; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.groups.impl.shib.ShibGroupServiceBean; +import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibServiceBean; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUserNameFields; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUtil; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.JsfHelper; -import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; +import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; +import java.util.Date; import java.util.List; -import java.util.UUID; -import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.EJB; +import javax.ejb.EJBException; import javax.faces.application.FacesMessage; import javax.faces.context.ExternalContext; import javax.faces.context.FacesContext; @@ -41,7 +28,6 @@ import javax.inject.Inject; import javax.inject.Named; import javax.servlet.http.HttpServletRequest; -import org.apache.commons.lang.StringUtils; @ViewScoped @Named("Shib") @@ -57,107 +43,16 @@ public class Shib implements java.io.Serializable { @EJB ShibServiceBean shibService; @EJB - ShibGroupServiceBean shibGroupService; - @EJB - SettingsServiceBean settingsService; + DataverseServiceBean dataverseService; @EJB - SystemConfig systemConfig; + GroupServiceBean groupService; @EJB - DataverseServiceBean dataverseService; + UserNotificationServiceBean userNotificationService; HttpServletRequest request; - /** - * @todo these are the attributes we are getting from the IdP at - * testshib.org. What other attributes should we expect? - * - * Here is a dump from https://pdurbin.pagekite.me/Shibboleth.sso/Session - * - * Miscellaneous - * - * Session Expiration (barring inactivity): 479 minute(s) - * - * Client Address: 10.0.2.2 - * - * SSO Protocol: urn:oasis:names:tc:SAML:2.0:protocol - * - * Identity Provider: https://idp.testshib.org/idp/shibboleth - * - * Authentication Time: 2014-09-12T17:07:36.137Z - * - * Authentication Context Class: - * urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport - * - * Authentication Context Decl: (none) - * - * - * - * Attributes - * - * affiliation: Member@testshib.org;Staff@testshib.org - * - * cn: Me Myself And I - * - * entitlement: urn:mace:dir:entitlement:common-lib-terms - * - * eppn: myself@testshib.org - * - * givenName: Me Myself - * - * persistent-id: - * https://idp.testshib.org/idp/shibboleth!https://pdurbin.pagekite.me/shibboleth!zylzL+NruovU5OOGXDOL576jxfo= - * - * sn: And I - * - * telephoneNumber: 555-5555 - * - * uid: myself - * - * unscoped-affiliation: Member;Staff - */ - /** - * @todo Resolve potential confusing of having attibutes like "eppn" defined - * twice in this class. - * - * This was used early on in development and should be removed at some - * point. - */ - @Deprecated - List shibAttrs = Arrays.asList( - "Shib-Identity-Provider", - "uid", - "cn", - "sn", - "givenName", - "telephoneNumber", - "eppn", - "affiliation", - "unscoped-affiliation", - "entitlement", - "persistent-id" - ); - - List shibValues = new ArrayList<>(); - /** - * @todo make this configurable? - */ - private final String shibIdpAttribute = "Shib-Identity-Provider"; - /** - * @todo Make attribute used (i.e. "eppn") configurable: - * https://github.com/IQSS/dataverse/issues/1422 - * - * OR *maybe* we can rely on people installing Dataverse to configure shibd - * to always send "eppn" as an attribute, via attribute mappings or what - * have you. - */ - private final String uniquePersistentIdentifier = "eppn"; private String userPersistentId; private String internalUserIdentifer; - private final String usernameAttribute = "uid"; - private final String displayNameAttribute = "cn"; - private final String firstNameAttribute = "givenName"; - private final String lastNameAttribute = "sn"; - private final String emailAttribute = "mail"; AuthenticatedUserDisplayInfo displayInfo; /** * @todo Remove this boolean some day? Now the mockups show a popup. Should @@ -192,31 +87,19 @@ public class Shib implements java.io.Serializable { private String existingEmail; private String existingDisplayName; private boolean passwordRejected; - private String displayNameToPersist = "(Blank: display name not received from Institution Log In)"; -// private String firstNameToPersist = "(Blank: first name not received from Institution Log In)"; -// private String lastNameToPersist = "(Blank: last name not received from Institution Log In)"; - private String emailToPersist = "(Blank: email received from Institution Log In)"; - /** - * @todo We're not really doing anything with affiliation yet, even though - * the mockups show it. The plan is to parse the JSON from - * https://dataverse.harvard.edu/Shibboleth.sso/DiscoFeed for example. Check - * the "ShibUtil" class - */ - private String affiliationToDisplayAtConfirmation = "Affiliation not provided by institution log in"; - /** - * @todo Once we can persist "position" to the authenticateduser table, we - * can revisit this. Maybe we'll use ORCID instead. Dunno. - */ -// private String positionToPersist = "Position not provided by institution log in"; - /** - * @todo localize this - */ - private String friendlyNameForInstitution = "your institution"; + private String displayNameToPersist; + private String emailToPersist; + private String affiliationToDisplayAtConfirmation = null; + private String friendlyNameForInstitution = BundleUtil.getStringFromBundle("shib.welcomeExistingUserMessageDefaultInstitution"); private State state; private String debugSummary; + /** + * After a successful login, we will redirect users to this page (unless + * it's a new account). + */ + private String redirectPage; // private boolean debug = false; private String emailAddress; - private boolean useHeaders; public enum State { @@ -228,17 +111,24 @@ public enum State { public void init() { state = State.INIT; - useHeaders = systemConfig.isShibUseHeaders(); - if (isDebug()) { - printHeaders(); - } ExternalContext context = FacesContext.getCurrentInstance().getExternalContext(); request = (HttpServletRequest) context.getRequest(); + ShibUtil.printAttributes(request); + + /** + * @todo Investigate why JkEnvVar is null since it may be useful for + * debugging per https://github.com/IQSS/dataverse/issues/2916 . See + * also + * http://stackoverflow.com/questions/30193117/iterate-through-all-servletrequest-attributes#comment49933342_30193117 + * and + * http://shibboleth.1660669.n2.nabble.com/Why-doesn-t-Java-s-request-getAttributeNames-show-Shibboleth-attributes-tp7616427p7616591.html + */ + logger.fine("JkEnvVar: " + System.getenv("JkEnvVar")); - possiblyMutateRequestInDev(); + shibService.possiblyMutateRequestInDev(request); try { - shibIdp = getRequiredValueFromAssertion(shibIdpAttribute); + shibIdp = getRequiredValueFromAssertion(ShibUtil.shibIdpAttribute); } catch (Exception ex) { /** * @todo is in an antipattern to throw exceptions to control flow? @@ -251,19 +141,19 @@ public void init() { } String shibUserIdentifier; try { - shibUserIdentifier = getRequiredValueFromAssertion(uniquePersistentIdentifier); + shibUserIdentifier = getRequiredValueFromAssertion(ShibUtil.uniquePersistentIdentifier); } catch (Exception ex) { return; } String firstName; try { - firstName = getRequiredValueFromAssertion(firstNameAttribute); + firstName = getRequiredValueFromAssertion(ShibUtil.firstNameAttribute); } catch (Exception ex) { return; } String lastName; try { - lastName = getRequiredValueFromAssertion(lastNameAttribute); + lastName = getRequiredValueFromAssertion(ShibUtil.lastNameAttribute); } catch (Exception ex) { return; } @@ -278,40 +168,46 @@ public void init() { lastName = betterLastName; } } + String emailAddressInAssertion = null; try { - emailAddress = getRequiredValueFromAssertion(emailAttribute); + emailAddressInAssertion = getRequiredValueFromAssertion(ShibUtil.emailAttribute); } catch (Exception ex) { - String testShibIdpEntityId = "https://idp.testshib.org/idp/shibboleth"; - if (shibIdp.equals(testShibIdpEntityId)) { - logger.info("For " + testShibIdpEntityId + " (which as of this writing doesn't provide the " + emailAttribute + " attribute) setting email address to value of eppn: " + shibUserIdentifier); - emailAddress = shibUserIdentifier; + if (shibIdp.equals(ShibUtil.testShibIdpEntityId)) { + logger.info("For " + shibIdp + " (which as of this writing doesn't provide the " + ShibUtil.emailAttribute + " attribute) setting email address to value of eppn: " + shibUserIdentifier); + emailAddressInAssertion = shibUserIdentifier; } else { // forcing all other IdPs to send us an an email return; } } - String usernameAssertion = getValueFromAssertion(usernameAttribute); + + if (!EMailValidator.isEmailValid(emailAddressInAssertion, null)) { + String msg = "The SAML assertion contained an invalid email address: \"" + emailAddressInAssertion + "\"."; + logger.info(msg); + String singleEmailAddress = ShibUtil.findSingleValue(emailAddressInAssertion); + if (EMailValidator.isEmailValid(singleEmailAddress, null)) { + msg = "Multiple email addresses were asserted by the Identity Provider (" + emailAddressInAssertion + " ). These were sorted and the first was chosen: " + singleEmailAddress; + logger.info(msg); + emailAddress = singleEmailAddress; + } else { + msg += " A single valid address could not be found."; + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, identityProviderProblem, msg)); + return; + } + } else { + emailAddress = emailAddressInAssertion; + } + + String usernameAssertion = getValueFromAssertion(ShibUtil.usernameAttribute); internalUserIdentifer = ShibUtil.generateFriendlyLookingUserIdentifer(usernameAssertion, emailAddress); - logger.info("friendly looking identifer (backend will enforce uniqueness):" + internalUserIdentifer); + logger.fine("friendly looking identifer (backend will enforce uniqueness):" + internalUserIdentifer); - /** - * @todo Remove, longer term. For now, commenting out special logic for - * always showing Terms of Use for TestShib accounts. The Terms of Use - * workflow is captured at - * http://datascience.iq.harvard.edu/blog/try-out-single-sign-shibboleth-40-beta - */ -// if (shibIdp.equals("https://idp.testshib.org/idp/shibboleth")) { -// StringBuilder sb = new StringBuilder(); -// String freshNewShibUser = sb.append(userIdentifier).append(UUID.randomUUID()).toString(); -// logger.info("Will create a new, unique user so the account Terms of Use will be displayed."); -// userIdentifier = freshNewShibUser; -// } - /** - * @todo Shouldn't we persist the displayName too? It still exists on - * the authenticateduser table. - */ -// String displayName = getDisplayName(displayNameAttribute, firstNameAttribute, lastNameAttribute); - String affiliation = getAffiliation(); + String affiliation = shibService.getAffiliation(shibIdp, shibService.getDevShibAccountType()); + if (affiliation != null) { + affiliationToDisplayAtConfirmation = affiliation; + friendlyNameForInstitution = affiliation; + } +// emailAddress = "willFailBeanValidation"; // for testing createAuthenticatedUser exceptions displayInfo = new AuthenticatedUserDisplayInfo(firstName, lastName, emailAddress, affiliation, null); userPersistentId = shibIdp + persistentUserIdSeparator + shibUserIdentifier; @@ -319,8 +215,8 @@ public void init() { AuthenticatedUser au = authSvc.lookupUser(shibAuthProvider.getId(), userPersistentId); if (au != null) { state = State.REGULAR_LOGIN_INTO_EXISTING_SHIB_ACCOUNT; - logger.info("Found user based on " + userPersistentId + ". Logging in."); - logger.info("Updating display info for " + au.getName()); + logger.fine("Found user based on " + userPersistentId + ". Logging in."); + logger.fine("Updating display info for " + au.getName()); authSvc.updateAuthenticatedUser(au, displayInfo); logInUserAndSetShibAttributes(au); String prettyFacesHomePageString = getPrettyFacesHomePageString(false); @@ -332,15 +228,7 @@ public void init() { } else { state = State.PROMPT_TO_CREATE_NEW_ACCOUNT; displayNameToPersist = displayInfo.getTitle(); -// firstNameToPersist = "foo"; -// lastNameToPersist = "bar"; emailToPersist = emailAddress; - /** - * @todo For Harvard at least, we plan to use "Harvard University" - * for affiliation because it's what we get from - * https://dataverse.harvard.edu/Shibboleth.sso/DiscoFeed - */ -// affiliationToPersist = "FIXME"; /** * @todo for Harvard we plan to use the value(s) from * eduPersonScopedAffiliation which @@ -355,7 +243,7 @@ public void init() { * eduPersonScopedAffiliation? */ // positionToPersist = "FIXME"; - logger.info("Couldn't find authenticated user based on " + userPersistentId); + logger.fine("Couldn't find authenticated user based on " + userPersistentId); visibleTermsOfUse = true; /** * Using the email address from the IdP, try to find an existing @@ -387,182 +275,33 @@ public void init() { } } -// if (debug) { -// printAttributes(request); -// } - } - - /** - * @todo Move this to the shib service bean. - */ - private String getAffiliation() { - JsonArray emptyJsonArray = new JsonArray(); - String discoFeedJson = emptyJsonArray.toString(); - String discoFeedUrl; - if (getDevShibAccountType().equals(DevShibAccountType.PRODUCTION)) { - discoFeedUrl = systemConfig.getDataverseSiteUrl() + "/Shibboleth.sso/DiscoFeed"; - } else { - String devUrl = "http://localhost:8080/resources/dev/sample-shib-identities.json"; - discoFeedUrl = devUrl; - } - logger.info("Trying to get affiliation from disco feed URL: " + discoFeedUrl); - URL url = null; - try { - url = new URL(discoFeedUrl); - } catch (MalformedURLException ex) { - logger.info(ex.toString()); - return null; - } - if (url == null) { - logger.info("url object was null after parsing " + discoFeedUrl); - return null; - } - HttpURLConnection discoFeedRequest = null; - try { - discoFeedRequest = (HttpURLConnection) url.openConnection(); - } catch (IOException ex) { - logger.info(ex.toString()); - return null; - } - if (discoFeedRequest == null) { - logger.info("disco feed request was null"); - return null; - } - try { - discoFeedRequest.connect(); - } catch (IOException ex) { - logger.info(ex.toString()); - return null; - } - JsonParser jp = new JsonParser(); - JsonElement root = null; - try { - root = jp.parse(new InputStreamReader((InputStream) discoFeedRequest.getInputStream())); - } catch (IOException ex) { - logger.info(ex.toString()); - return null; - } - if (root == null) { - logger.info("root was null"); - return null; - } - JsonArray rootArray = root.getAsJsonArray(); - if (rootArray == null) { - logger.info("Couldn't get JSON Array from URL"); - return null; - } - discoFeedJson = rootArray.toString(); - logger.fine("Dump of disco feed:" + discoFeedJson); - String affiliation = ShibUtil.getDisplayNameFromDiscoFeed(shibIdp, discoFeedJson); - if (affiliation != null) { - affiliationToDisplayAtConfirmation = affiliation; - friendlyNameForInstitution = affiliation; - return affiliation; - } else { - logger.info("Couldn't find an affiliation from " + shibIdp); - return null; - } - } - - /** - * "Production" means "don't mess with the HTTP request". - */ - public enum DevShibAccountType { - - PRODUCTION, - RANDOM, - TESTSHIB1, - HARVARD1, - HARVARD2, - TWO_EMAILS, - }; - - private DevShibAccountType getDevShibAccountType() { - DevShibAccountType saneDefault = DevShibAccountType.PRODUCTION; - String settingReturned = settingsService.getValueForKey(SettingsServiceBean.Key.DebugShibAccountType); - logger.fine("setting returned: " + settingReturned); - if (settingReturned != null) { - try { - DevShibAccountType parsedValue = DevShibAccountType.valueOf(settingReturned); - return parsedValue; - } catch (IllegalArgumentException ex) { - logger.info("Couldn't parse value: " + ex + " - returning a sane default: " + saneDefault); - return saneDefault; - } - } else { - logger.fine("Shibboleth dev mode has not been configured. Returning a sane default: " + saneDefault); - return saneDefault; - } - - } - - /** - * This method exists so developers don't have to run Shibboleth locally. - * You can populate the request with Shibboleth attributes by changing a - * setting like this: - * - * curl -X PUT -d RANDOM - * http://localhost:8080/api/admin/settings/:DebugShibAccountType - * - * When you're done, feel free to delete the setting: - * - * curl -X DELETE - * http://localhost:8080/api/admin/settings/:DebugShibAccountType - * - * Note that setting ShibUseHeaders to true will make this "dev mode" stop - * working. - */ - private void possiblyMutateRequestInDev() { - switch (getDevShibAccountType()) { - case PRODUCTION: - logger.fine("Request will not be mutated"); - break; - - case RANDOM: - mutateRequestForDevRandom(); - break; - - case TESTSHIB1: - mutateRequestForDevConstantTestShib1(); - break; - - case HARVARD1: - mutateRequestForDevConstantHarvard1(); - break; - - case HARVARD2: - mutateRequestForDevConstantHarvard2(); - break; - - case TWO_EMAILS: - mutateRequestForDevConstantTwoEmails(); - break; - - default: - logger.info("Should never reach here"); - break; - } - } - - private void printHeaders() { - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String headerName = (String) headerNames.nextElement(); - logger.info(headerName + " = " + request.getHeader(headerName)); - } + logger.fine("Debug summary: " + debugSummary + " (state: " + state + ")."); + logger.fine("redirectPage: " + redirectPage); } public String confirmAndCreateAccount() { ShibAuthenticationProvider shibAuthProvider = new ShibAuthenticationProvider(); String lookupStringPerAuthProvider = userPersistentId; - AuthenticatedUser au = authSvc.createAuthenticatedUser( - new UserRecordIdentifier(shibAuthProvider.getId(), lookupStringPerAuthProvider), internalUserIdentifer, displayInfo, true); + AuthenticatedUser au = null; + try { + au = authSvc.createAuthenticatedUser( + new UserRecordIdentifier(shibAuthProvider.getId(), lookupStringPerAuthProvider), internalUserIdentifer, displayInfo, true); + } catch (EJBException ex) { + /** + * @todo Show the ConstraintViolationException, if any. + */ + logger.info("Couldn't create user " + userPersistentId + " due to exception: " + ex.getCause()); + } if (au != null) { - logger.info("created user " + au.getIdentifier()); + logger.fine("created user " + au.getIdentifier()); + logInUserAndSetShibAttributes(au); + userNotificationService.sendNotification(au, + new Timestamp(new Date().getTime()), + UserNotification.Type.CREATEACC, null); + return "/dataverseuser.xhtml?selectTab=accountInfo&faces-redirect=true"; } else { - logger.info("couldn't create user " + userPersistentId); + JsfHelper.addErrorMessage("Couldn't create user."); } - logInUserAndSetShibAttributes(au); return getPrettyFacesHomePageString(true); } @@ -571,7 +310,7 @@ public String confirmAndConvertAccount() { ShibAuthenticationProvider shibAuthProvider = new ShibAuthenticationProvider(); String lookupStringPerAuthProvider = userPersistentId; UserIdentifier userIdentifier = new UserIdentifier(lookupStringPerAuthProvider, internalUserIdentifer); - logger.info("builtin username: " + builtinUsername); + logger.fine("builtin username: " + builtinUsername); AuthenticatedUser builtInUserToConvert = shibService.canLogInAsBuiltinUser(builtinUsername, builtinPassword); if (builtInUserToConvert != null) { AuthenticatedUser au = authSvc.convertBuiltInToShib(builtInUserToConvert, shibAuthProvider.getId(), userIdentifier); @@ -580,7 +319,7 @@ public String confirmAndConvertAccount() { logInUserAndSetShibAttributes(au); debugSummary = "Local account validated and successfully converted to a Shibboleth account. The old account username was " + builtinUsername; JsfHelper.addSuccessMessage("Your Dataverse account is now associated with your institutional account."); - return getPrettyFacesHomePageString(true); + return "/dataverseuser.xhtml?selectTab=accountInfo&faces-redirect=true"; } else { debugSummary = "Local account validated but unable to convert to Shibboleth account."; } @@ -594,6 +333,15 @@ public String confirmAndConvertAccount() { private void logInUserAndSetShibAttributes(AuthenticatedUser au) { au.setShibIdentityProvider(shibIdp); session.setUser(au); + logger.fine("Groups for user " + au.getId() + " (" + au.getIdentifier() + "): " + getGroups(au)); + } + + public List getGroups(AuthenticatedUser au) { + List groups = new ArrayList<>(); + groupService.groupsFor(au, null).stream().forEach((group) -> { + groups.add(group.getDisplayName() + " (" + group.getIdentifier() + ")"); + }); + return groups; } /** @@ -608,72 +356,44 @@ public String cancel() { return loginpage + "?faces-redirect=true"; } - public List getShibValues() { - return shibValues; - } - -// private void printAttributes(HttpServletRequest request) { -// for (String attr : shibAttrs) { -// -// /** -// * @todo explain in Installers Guide that in order for these -// * attributes to be found attributePrefix="AJP_" must be added to -// * /etc/shibboleth/shibboleth2.xml like this: -// * -// * -// * -// */ -// Object attrObject = request.getAttribute(attr); -// if (attrObject != null) { -// shibValues.add(attr + ": " + attrObject.toString()); -// } -// } -// logger.info("shib values: " + shibValues); -// } /** * @return The value of a Shib attribute (if non-empty) or null. */ private String getValueFromAssertion(String key) { - Object attributeOrHeader = getAttributeOrHeader(key); - if (attributeOrHeader != null) { - String attributeValue = attributeOrHeader.toString(); + Object attribute = request.getAttribute(key); + if (attribute != null) { + String attributeValue = attribute.toString(); + logger.fine("The SAML assertion for \"" + key + "\" (optional) was \"" + attributeValue + "\"."); if (!attributeValue.isEmpty()) { return attributeValue; } } + logger.info("The SAML assertion for \"" + key + "\" (optional) was null."); return null; } private String getRequiredValueFromAssertion(String key) throws Exception { - Object attributeOrHeader = getAttributeOrHeader(key); - if (attributeOrHeader == null) { + Object attribute = request.getAttribute(key); + if (attribute == null) { String msg = "The SAML assertion for \"" + key + "\" was null. Please contact support."; logger.info(msg); - FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, identityProviderProblem, msg)); + boolean showMessage = true; + if (shibIdp.equals(ShibUtil.testShibIdpEntityId) && key.equals(ShibUtil.emailAttribute)) { + showMessage = false; + } + if (showMessage) { + FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, identityProviderProblem, msg)); + } throw new Exception(msg); } - String attributeValue = attributeOrHeader.toString(); + String attributeValue = attribute.toString(); if (attributeValue.isEmpty()) { throw new Exception(key + " was empty"); } + logger.fine("The SAML assertion for \"" + key + "\" (required) was \"" + attributeValue + "\"."); return attributeValue; } - private Object getAttributeOrHeader(String attribute) { - /** - * @todo Should the prefix be configurable? - */ - String prefix = "ajp_"; - Object attributeOrHeader; - if (useHeaders) { - attributeOrHeader = request.getHeader(prefix + attribute); - } else { - attributeOrHeader = request.getAttribute(attribute); - } - return attributeOrHeader; - } - public String getRootDataverseAlias() { Dataverse rootDataverse = dataverseService.findRootDataverse(); if (rootDataverse != null) { @@ -696,6 +416,9 @@ public String getRootDataverseAlias() { * logic per https://github.com/IQSS/dataverse/issues/1551 */ public String getPrettyFacesHomePageString(boolean includeFacetDashRedirect) { + if (redirectPage != null) { + return redirectPage; + } String plainHomepageString = "/dataverse.xhtml"; String rootDvAlias = getRootDataverseAlias(); if (includeFacetDashRedirect) { @@ -704,23 +427,17 @@ public String getPrettyFacesHomePageString(boolean includeFacetDashRedirect) { } else { return plainHomepageString + "?faces-redirect=true"; } + } else if (rootDvAlias != null) { + /** + * @todo Is there a constant for "/dataverse/" anywhere? I guess + * we'll just hard-code it here. + */ + return "/dataverse/" + rootDvAlias; } else { - if (rootDvAlias != null) { - /** - * @todo Is there a constant for "/dataverse/" anywhere? I guess - * we'll just hard-code it here. - */ - return "/dataverse/" + rootDvAlias; - } else { - return plainHomepageString; - } + return plainHomepageString; } } - public boolean isDebug() { - return systemConfig.isDebugEnabled(); - } - public boolean isInit() { return state.equals(State.INIT); } @@ -737,12 +454,6 @@ public String getDisplayNameToPersist() { return displayNameToPersist; } -// public String getFirstNameToPersist() { -// return firstNameToPersist; -// } -// public String getLastNameToPersist() { -// return lastNameToPersist; -// } public String getEmailToPersist() { return emailToPersist; } @@ -751,9 +462,6 @@ public String getAffiliationToDisplayAtConfirmation() { return affiliationToDisplayAtConfirmation; } -// public String getPositionToPersist() { -// return positionToPersist; -// } public String getExistingEmail() { return existingEmail; } @@ -810,110 +518,12 @@ public void setDebugSummary(String debugSummary) { this.debugSummary = debugSummary; } - private void mutateRequestForDevRandom() throws JsonSyntaxException, JsonIOException { - // set *something*, at least, even if it's just shortened UUIDs -// for (String attr : shibAttrs) { - // in dev we don't care if a new, random user is created each time -// request.setAttribute(attr, UUID.randomUUID().toString().substring(0, 8)); -// } - - String sURL = "http://api.randomuser.me"; - URL url = null; - try { - url = new URL(sURL); - } catch (MalformedURLException ex) { - Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex); - } - HttpURLConnection randomUserRequest = null; - try { - randomUserRequest = (HttpURLConnection) url.openConnection(); - } catch (IOException ex) { - Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex); - } - try { - randomUserRequest.connect(); - } catch (IOException ex) { - Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex); - } + public String getRedirectPage() { + return redirectPage; + } - JsonParser jp = new JsonParser(); //from gson - JsonElement root = null; - try { - root = jp.parse(new InputStreamReader((InputStream) randomUserRequest.getContent())); //convert the input stream to a json element - } catch (IOException ex) { - Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex); - } - JsonObject rootObject = root.getAsJsonObject(); - logger.fine(rootObject.toString()); - JsonElement results = rootObject.get("results"); - logger.fine(results.toString()); - JsonElement firstResult = results.getAsJsonArray().get(0); - logger.fine(firstResult.toString()); - JsonElement user = firstResult.getAsJsonObject().get("user"); - JsonElement username = user.getAsJsonObject().get("username"); - JsonElement email = user.getAsJsonObject().get("email"); - JsonElement password = user.getAsJsonObject().get("password"); - JsonElement name = user.getAsJsonObject().get("name"); - JsonElement firstName = name.getAsJsonObject().get("first"); - JsonElement lastName = name.getAsJsonObject().get("last"); - /** - * @todo Does Harvard really send displayName? At one point they didn't. - * Let's simulate the non-sending of displayName here. - */ -// request.setAttribute(displayNameAttribute, StringUtils.capitalise(firstName.getAsString()) + " " + StringUtils.capitalise(lastName.getAsString())); - request.setAttribute(lastNameAttribute, StringUtils.capitalise(lastName.getAsString())); - request.setAttribute(firstNameAttribute, StringUtils.capitalise(firstName.getAsString())); - request.setAttribute(emailAttribute, email.getAsString()); - // random IDP - request.setAttribute(shibIdpAttribute, "https://idp." + password.getAsString() + ".com/idp/shibboleth"); - /** - * Harvard's IdP doesn't send a username so let's test without it by - * commenting it out here. - */ -// request.setAttribute(usernameAttribute, username.getAsString()); - // eppn - request.setAttribute(uniquePersistentIdentifier, UUID.randomUUID().toString().substring(0, 8)); - } - - private void mutateRequestForDevConstantTestShib1() { - request.setAttribute(shibIdpAttribute, "https://idp.testshib.org/idp/shibboleth"); - // the TestShib "eppn" looks like an email address - request.setAttribute(uniquePersistentIdentifier, "saml@testshib.org"); -// request.setAttribute(displayNameAttribute, "Sam El"); - request.setAttribute(firstNameAttribute, "Samuel;Sam"); - request.setAttribute(lastNameAttribute, "El"); - // TestShib doesn't send "mail" attribute so let's mimic that. -// request.setAttribute(emailAttribute, "saml@mailinator.com"); - request.setAttribute(usernameAttribute, "saml"); - } - - private void mutateRequestForDevConstantHarvard1() { - request.setAttribute(shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth"); - request.setAttribute(uniquePersistentIdentifier, "constantHarvard"); -// request.setAttribute(displayNameAttribute, "John Harvard"); - request.setAttribute(firstNameAttribute, "John"); - request.setAttribute(lastNameAttribute, "Harvard"); - request.setAttribute(emailAttribute, "jharvard@mailinator.com"); - request.setAttribute(usernameAttribute, "jharvard"); - } - - private void mutateRequestForDevConstantHarvard2() { - request.setAttribute(shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth"); - request.setAttribute(uniquePersistentIdentifier, "constantHarvard2"); -// request.setAttribute(displayNameAttribute, "Grace Hopper"); - request.setAttribute(firstNameAttribute, "Grace"); - request.setAttribute(lastNameAttribute, "Hopper"); - request.setAttribute(emailAttribute, "ghopper@mailinator.com"); - request.setAttribute(usernameAttribute, "ghopper"); - } - - private void mutateRequestForDevConstantTwoEmails() { - request.setAttribute(shibIdpAttribute, "https://fake.example.com/idp/shibboleth"); - request.setAttribute(uniquePersistentIdentifier, "twoEmails"); - request.setAttribute(firstNameAttribute, "Eric"); - request.setAttribute(lastNameAttribute, "Allman"); - request.setAttribute(emailAttribute, "eric1@mailinator.com;eric2@mailinator.com"); - request.setAttribute(usernameAttribute, "eallman"); + public void setRedirectPage(String redirectPage) { + this.redirectPage = redirectPage; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java index c6d48ec5fde..3bff0f09c76 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/Admin.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/Admin.java @@ -1,13 +1,24 @@ package edu.harvard.iq.dataverse.api; import edu.harvard.iq.dataverse.Dataverse; +import edu.harvard.iq.dataverse.EMailValidator; import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord; +import static edu.harvard.iq.dataverse.api.AbstractApiBean.errorResponse; import edu.harvard.iq.dataverse.api.dto.RoleDTO; +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserLookup; import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; +import edu.harvard.iq.dataverse.authorization.UserIdentifier; import edu.harvard.iq.dataverse.authorization.exceptions.AuthenticationProviderFactoryNotFoundException; import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationSetupException; import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderFactory; import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderRow; +import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider; +import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; +import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; +import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider; +import edu.harvard.iq.dataverse.authorization.providers.shib.ShibServiceBean; +import edu.harvard.iq.dataverse.authorization.providers.shib.ShibUtil; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.engine.command.impl.PublishDataverseCommand; import edu.harvard.iq.dataverse.settings.Setting; @@ -24,8 +35,11 @@ import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; +import java.sql.SQLException; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import javax.ejb.EJB; import javax.ejb.Stateless; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; @@ -40,7 +54,12 @@ public class Admin extends AbstractApiBean { private static final Logger logger = Logger.getLogger(Admin.class.getName()); - + + @EJB + BuiltinUserServiceBean builtinUserService; + @EJB + ShibServiceBean shibService; + @Path("settings") @GET public Response listAllSettings() { @@ -228,6 +247,251 @@ public Response publishDataverseAsCreator(@PathParam("id") long id) { } } + @GET + @Path("authenticatedUsers") + public Response listAuthenticatedUsers() { + try { + AuthenticatedUser user = findAuthenticatedUserOrDie(); + if (!user.isSuperuser()) { + return errorResponse(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse ex) { + return errorResponse(Response.Status.FORBIDDEN, "Superusers only."); + } + JsonArrayBuilder userArray = Json.createArrayBuilder(); + authSvc.findAllAuthenticatedUsers().stream().forEach((user) -> { + userArray.add(jsonForAuthUser(user)); + }); + return okResponse(userArray); + } + + /** + * @todo Refactor more of this business logic into ShibServiceBean in case + * we want to build a superuser GUI around this some day. + * + * curl -X PUT -d "shib@mailinator.com" + * http://localhost:8080/api/admin/authenticatedUsers/id/11/convertShibToBuiltIn + */ + @PUT + @Path("authenticatedUsers/id/{id}/convertShibToBuiltIn") + public Response convertShibUserToBuiltin(@PathParam("id") Long id, String newEmailAddress) { + try { + AuthenticatedUser user = findAuthenticatedUserOrDie(); + if (!user.isSuperuser()) { + return errorResponse(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse ex) { + return errorResponse(Response.Status.FORBIDDEN, "Superusers only."); + } + AuthenticatedUser userToConvert = authSvc.findByID(id); + if (userToConvert == null) { + return errorResponse(Response.Status.BAD_REQUEST, "User id " + id + " not found."); + } + AuthenticatedUserLookup lookup = userToConvert.getAuthenticatedUserLookup(); + if (lookup == null) { + return errorResponse(Response.Status.BAD_REQUEST, "User id " + id + " does not have an 'authenticateduserlookup' row"); + } + String providerId = lookup.getAuthenticationProviderId(); + if (providerId == null) { + return errorResponse(Response.Status.BAD_REQUEST, "User id " + id + " provider id is null."); + } + String shibProviderId = ShibAuthenticationProvider.PROVIDER_ID; + if (!providerId.equals(shibProviderId)) { + return errorResponse(Response.Status.BAD_REQUEST, "User id " + id + " cannot be converted because current provider id is '" + providerId + "' rather than '" + shibProviderId + "'."); + } + BuiltinUser builtinUser = null; + try { + /** + * @todo Refactor more of the logic and error checking into this + * convertShibToBuiltIn method. + */ + builtinUser = authSvc.convertShibToBuiltIn(userToConvert, newEmailAddress); + } catch (Throwable ex) { + while (ex.getCause() != null) { + ex = ex.getCause(); + } + if (ex instanceof ConstraintViolationException) { + ConstraintViolationException constraintViolationException = (ConstraintViolationException) ex; + StringBuilder userMsg = new StringBuilder(); + StringBuilder logMsg = new StringBuilder(); + logMsg.append("User id " + id + " cannot be converted from Shibboleth to BuiltIn. "); + for (ConstraintViolation violation : constraintViolationException.getConstraintViolations()) { + logMsg.append(" Invalid value: <<<").append(violation.getInvalidValue()).append(">>> for ").append(violation.getPropertyPath()).append(" at ").append(violation.getLeafBean()).append(" - ").append(violation.getMessage()); + userMsg.append(" Invalid value: <<<").append(violation.getInvalidValue()).append(">>> for ").append(violation.getPropertyPath()).append(" - ").append(violation.getMessage()); + } + logger.warning(logMsg.toString()); + return errorResponse(Response.Status.BAD_REQUEST, "User id " + id + " could not be converted from Shibboleth to BuiltIn: " + userMsg.toString()); + } else { + return errorResponse(Response.Status.INTERNAL_SERVER_ERROR, "User id " + id + " cannot be converted due to unexpected exception: " + ex); + } + } + if (builtinUser == null) { + return errorResponse(Response.Status.BAD_REQUEST, "User id " + id + " could not be converted from Shibboleth to BuiltIn"); + } + try { + /** + * @todo Should this logic be moved to the + * authSvc.convertShibToBuiltIn() method? + */ + lookup.setAuthenticationProviderId(BuiltinAuthenticationProvider.PROVIDER_ID); + lookup.setPersistentUserId(userToConvert.getUserIdentifier()); + em.persist(lookup); + userToConvert.setEmail(newEmailAddress); + em.persist(userToConvert); + em.flush(); + } catch (Throwable ex) { + while (ex.getCause() != null) { + ex = ex.getCause(); + } + if (ex instanceof SQLException) { + String msg = "User id " + id + " only half converted from Shibboleth to BuiltIn and may not be able to log in. Manual changes may be necessary on 'authenticationproviderid' and 'authenticationproviderid' on 'authenticateduserlookup' table and 'email' on 'authenticateduser' table."; + logger.warning(msg); + return errorResponse(Response.Status.BAD_REQUEST, msg); + } else { + return errorResponse(Response.Status.INTERNAL_SERVER_ERROR, "User id " + id + " only half converted from Shibboleth to BuiltIn and may not be able to log in due to unexpected exception: " + ex.getClass().getName()); + } + } + JsonObjectBuilder output = Json.createObjectBuilder(); + output.add("email", builtinUser.getEmail()); + output.add("username", builtinUser.getUserName()); + return okResponse(output); + } + + /** + * This is used in testing via AdminIT.java but we don't expect sysadmins to + * use this. + */ + @Path("authenticatedUsers/convert/builtin2shib") + @PUT + public Response builtin2shib(String content) { + logger.info("entering builtin2shib..."); + try { + AuthenticatedUser userToRunThisMethod = findAuthenticatedUserOrDie(); + if (!userToRunThisMethod.isSuperuser()) { + return errorResponse(Response.Status.FORBIDDEN, "Superusers only."); + } + } catch (WrappedResponse ex) { + return errorResponse(Response.Status.FORBIDDEN, "Superusers only."); + } + boolean disabled = false; + if (disabled) { + return errorResponse(Response.Status.BAD_REQUEST, "API endpoint disabled."); + } + AuthenticatedUser builtInUserToConvert = null; + String emailToFind; + String password; + String authuserId = "0"; // could let people specify id on authuser table. probably better to let them tell us their + String newEmailAddressToUse; + try { + String[] args = content.split(":"); + emailToFind = args[0]; + password = args[1]; + newEmailAddressToUse = args[2]; +// authuserId = args[666]; + } catch (ArrayIndexOutOfBoundsException ex) { + return errorResponse(Response.Status.BAD_REQUEST, "Problem with content <<<" + content + ">>>: " + ex.toString()); + } + AuthenticatedUser existingAuthUserFoundByEmail = shibService.findAuthUserByEmail(emailToFind); + String existing = "NOT FOUND"; + if (existingAuthUserFoundByEmail != null) { + builtInUserToConvert = existingAuthUserFoundByEmail; + existing = existingAuthUserFoundByEmail.getIdentifier(); + } else { + long longToLookup = Long.parseLong(authuserId); + AuthenticatedUser specifiedUserToConvert = authSvc.findByID(longToLookup); + if (specifiedUserToConvert != null) { + builtInUserToConvert = specifiedUserToConvert; + } else { + return errorResponse(Response.Status.BAD_REQUEST, "No user to convert. We couldn't find a *single* existing user account based on " + emailToFind + " and no user was found using specified id " + longToLookup); + } + } + String shibProviderId = ShibAuthenticationProvider.PROVIDER_ID; + Map randomUser = shibService.getRandomUser(); +// String eppn = UUID.randomUUID().toString().substring(0, 8); + String eppn = randomUser.get("eppn"); + String idPEntityId = randomUser.get("idp"); + String notUsed = null; + String separator = "|"; + UserIdentifier newUserIdentifierInLookupTable = new UserIdentifier(idPEntityId + separator + eppn, notUsed); + String overwriteFirstName = randomUser.get("firstName"); + String overwriteLastName = randomUser.get("lastName"); + String overwriteEmail = randomUser.get("email"); + overwriteEmail = newEmailAddressToUse; + logger.info("overwriteEmail: " + overwriteEmail); + boolean validEmail = EMailValidator.isEmailValid(overwriteEmail, null); + if (!validEmail) { + // See https://github.com/IQSS/dataverse/issues/2998 + return errorResponse(Response.Status.BAD_REQUEST, "invalid email: " + overwriteEmail); + } + /** + * @todo If affiliation is not null, put it in RoleAssigneeDisplayInfo + * constructor. + */ + /** + * Here we are exercising (via an API test) shibService.getAffiliation + * with the TestShib IdP and a non-production DevShibAccountType. + */ + idPEntityId = ShibUtil.testShibIdpEntityId; + String overwriteAffiliation = shibService.getAffiliation(idPEntityId, ShibServiceBean.DevShibAccountType.RANDOM); + logger.info("overwriteAffiliation: " + overwriteAffiliation); + /** + * @todo Find a place to put "position" in the authenticateduser table: + * https://github.com/IQSS/dataverse/issues/1444#issuecomment-74134694 + */ + String overwritePosition = "staff;student"; + AuthenticatedUserDisplayInfo displayInfo = new AuthenticatedUserDisplayInfo(overwriteFirstName, overwriteLastName, overwriteEmail, overwriteAffiliation, overwritePosition); + JsonObjectBuilder response = Json.createObjectBuilder(); + JsonArrayBuilder problems = Json.createArrayBuilder(); + if (password != null) { + response.add("password supplied", password); + boolean knowsExistingPassword = false; + BuiltinUser oldBuiltInUser = builtinUserService.findByUserName(builtInUserToConvert.getUserIdentifier()); + if (oldBuiltInUser != null) { + String usernameOfBuiltinAccountToConvert = oldBuiltInUser.getUserName(); + response.add("old username", usernameOfBuiltinAccountToConvert); + AuthenticatedUser authenticatedUser = shibService.canLogInAsBuiltinUser(usernameOfBuiltinAccountToConvert, password); + if (authenticatedUser != null) { + knowsExistingPassword = true; + AuthenticatedUser convertedUser = authSvc.convertBuiltInToShib(builtInUserToConvert, shibProviderId, newUserIdentifierInLookupTable); + if (convertedUser != null) { + /** + * @todo Display name is not being overwritten. Logic + * must be in Shib backing bean + */ + AuthenticatedUser updatedInfoUser = authSvc.updateAuthenticatedUser(convertedUser, displayInfo); + if (updatedInfoUser != null) { + response.add("display name overwritten with", updatedInfoUser.getName()); + } else { + problems.add("couldn't update display info"); + } + } else { + problems.add("unable to convert user"); + } + } + } else { + problems.add("couldn't find old username"); + } + if (!knowsExistingPassword) { + String message = "User doesn't know password."; + problems.add(message); + return errorResponse(Status.BAD_REQUEST, message); + } +// response.add("knows existing password", knowsExistingPassword); + } + + response.add("user to convert", builtInUserToConvert.getIdentifier()); + response.add("existing user found by email (prompt to convert)", existing); + response.add("changing to this provider", shibProviderId); + response.add("value to overwrite old first name", overwriteFirstName); + response.add("value to overwrite old last name", overwriteLastName); + response.add("value to overwrite old email address", overwriteEmail); + if (overwriteAffiliation != null) { + response.add("affiliation", overwriteAffiliation); + } + response.add("problems", problems); + return okResponse(response); + } + @DELETE @Path("authenticatedUsers/id/{id}/") public Response deleteAuthenticatedUserById(@PathParam("id") Long id) { diff --git a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java index 704c3d13d94..3483a0c790c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/TestApi.java @@ -1,15 +1,8 @@ package edu.harvard.iq.dataverse.api; import edu.harvard.iq.dataverse.DvObject; -import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.RoleAssignee; -import edu.harvard.iq.dataverse.authorization.UserIdentifier; -import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; -import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; import edu.harvard.iq.dataverse.authorization.providers.builtin.PasswordEncryption; -import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider; -import edu.harvard.iq.dataverse.authorization.providers.shib.ShibServiceBean; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import javax.ejb.Stateless; import javax.ws.rs.GET; @@ -17,30 +10,28 @@ import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; import static edu.harvard.iq.dataverse.util.json.JsonPrinter.*; -import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; -import javax.ejb.EJB; import javax.json.Json; -import javax.json.JsonArrayBuilder; import javax.json.JsonObjectBuilder; -import javax.ws.rs.PUT; import javax.ws.rs.QueryParam; import org.mindrot.jbcrypt.BCrypt; /** * An API to test internal models without the need to deal with UI etc. * + * @todo Can this entire class be removed and its methods be moved to Admin.java + * if they are still needed? Once this is done we can remove this warning: + * "There is a “test” API endpoint used for development and troubleshooting that + * has some potentially dangerous methods." + * http://guides.dataverse.org/en/4.2.4/installation/config.html#blocking-api-endpoints + * * @author michael */ @Stateless @Path("test") public class TestApi extends AbstractApiBean { private static final Logger logger = Logger.getLogger(TestApi.class.getName()); - @EJB - BuiltinUserServiceBean builtinUserService; - @EJB - ShibServiceBean shibService; @Path("echo/{whatever}") @GET @@ -96,110 +87,7 @@ public String test( @PathParam("w1") String w1 ) { return sb.toString(); } - - @Path("user/convert/builtin2shib") - @PUT - public Response builtin2shib(String content) { - boolean disabled = false; - if (disabled) { - return errorResponse(Response.Status.BAD_REQUEST, "API endpoint disabled."); - } - AuthenticatedUser builtInUserToConvert = null; - String emailToFind; - String password; - String authuserId = "0"; // could let people specify id on authuser table. probably better to let them tell us their - try { - String[] args = content.split(":"); - emailToFind = args[0]; - password = args[1]; -// authuserId = args[666]; - } catch (ArrayIndexOutOfBoundsException ex) { - return errorResponse(Response.Status.BAD_REQUEST, ex.toString()); - } - AuthenticatedUser existingAuthUserFoundByEmail = shibService.findAuthUserByEmail(emailToFind); - String existing = "NOT FOUND"; - if (existingAuthUserFoundByEmail != null) { - builtInUserToConvert = existingAuthUserFoundByEmail; - existing = existingAuthUserFoundByEmail.getIdentifier(); - } else { - long longToLookup = Long.parseLong(authuserId); - AuthenticatedUser specifiedUserToConvert = authSvc.findByID(longToLookup); - if (specifiedUserToConvert != null) { - builtInUserToConvert = specifiedUserToConvert; - } else { - return errorResponse(Response.Status.BAD_REQUEST, "No user to convert. We couldn't find a *single* existing user account based on " + emailToFind + " and no user was found using specified id " + longToLookup); - } - } - ShibAuthenticationProvider shibProvider = new ShibAuthenticationProvider(); - String shibProviderId = shibProvider.getId(); - Map randomUser = shibService.getRandomUser(); -// String eppn = UUID.randomUUID().toString().substring(0, 8); - String eppn = randomUser.get("eppn"); - String idPEntityId = randomUser.get("idp"); - String notUsed = null; - String separator = "|"; - UserIdentifier newUserIdentifierInLookupTable = new UserIdentifier(idPEntityId + separator + eppn, notUsed); - String overwriteFirstName = randomUser.get("firstName"); - String overwriteLastName = randomUser.get("lastName"); - String overwriteEmail = randomUser.get("email"); - /** - * @todo If affiliation is not null, put it in RoleAssigneeDisplayInfo - * constructor. - */ - String overwriteAffiliation = shibService.getFriendlyInstitutionName(idPEntityId); - /** - * @todo Find a place to put "position" in the authenticateduser table: - * https://github.com/IQSS/dataverse/issues/1444#issuecomment-74134694 - */ - String overwritePosition = "staff;student"; - AuthenticatedUserDisplayInfo displayInfo = new AuthenticatedUserDisplayInfo(overwriteFirstName, overwriteLastName, overwriteEmail, overwriteAffiliation,overwritePosition); - JsonObjectBuilder response = Json.createObjectBuilder(); - JsonArrayBuilder problems = Json.createArrayBuilder(); - if (password != null) { - response.add("password supplied", password); - boolean knowsExistingPassword = false; - BuiltinUser oldBuiltInUser = builtinUserService.findByUserName(builtInUserToConvert.getUserIdentifier()); - if (oldBuiltInUser != null) { - String usernameOfBuiltinAccountToConvert = oldBuiltInUser.getUserName(); - response.add("old username", usernameOfBuiltinAccountToConvert); - AuthenticatedUser authenticatedUser = shibService.canLogInAsBuiltinUser(usernameOfBuiltinAccountToConvert, password); - if (authenticatedUser != null) { - knowsExistingPassword = true; - AuthenticatedUser convertedUser = authSvc.convertBuiltInToShib(builtInUserToConvert, shibProviderId, newUserIdentifierInLookupTable); - if (convertedUser != null) { - /** - * @todo Display name is not being overwritten. Logic - * must be in Shib backing bean - */ - AuthenticatedUser updatedInfoUser = authSvc.updateAuthenticatedUser(convertedUser, displayInfo); - if (updatedInfoUser != null) { - response.add("display name overwritten with", updatedInfoUser.getName()); - } else { - problems.add("couldn't update display info"); - } - } else { - problems.add("unable to convert user"); - } - } - } else { - problems.add("couldn't find old username"); - } - if (!knowsExistingPassword) { - problems.add("doesn't know password"); - } -// response.add("knows existing password", knowsExistingPassword); - } - response.add("user to convert", builtInUserToConvert.getIdentifier()); - response.add("existing user found by email (prompt to convert)", existing); - response.add("changing to this provider", shibProviderId); - response.add("value to overwrite old first name", overwriteFirstName); - response.add("value to overwrite old last name", overwriteLastName); - response.add("value to overwrite old email address", overwriteEmail); - response.add("problems", problems); - return okResponse(response); - } - @Path("apikey") @GET public Response testUserLookup() { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java index 168fc7b4a47..8303bc32701 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/AuthenticationServiceBean.java @@ -10,6 +10,7 @@ import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationSetupException; import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderFactory; import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderRow; +import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProviderFactory; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; @@ -36,6 +37,10 @@ import javax.persistence.NonUniqueResultException; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; /** * The AuthenticationManager is responsible for registering and listing @@ -498,4 +503,32 @@ public AuthenticatedUser convertBuiltInToShib(AuthenticatedUser builtInUserToCon return null; } + /** + * @param authenticatedUser The AuthenticatedUser (Shibboleth user) to + * convert to a BuiltinUser. + * @param newEmailAddress The new email address that will be used instead of + * the user's old email address from the institution that they have left. + * @return BuiltinUser + * @throws java.lang.Exception You must catch a potential + * ConstraintViolationException due to Bean Validation (non-null, etc.) on + * the entities. Report these back to the superuser. + */ + public BuiltinUser convertShibToBuiltIn(AuthenticatedUser authenticatedUser, String newEmailAddress) throws Exception { + BuiltinUser builtinUser = new BuiltinUser(); + builtinUser.setUserName(authenticatedUser.getUserIdentifier()); + builtinUser.setFirstName(authenticatedUser.getFirstName()); + builtinUser.setLastName(authenticatedUser.getLastName()); + builtinUser.setEmail(newEmailAddress); + /** + * @todo If there are violations, don't even try to persist the user. + * Report the violations. Write tests around this. + */ + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + Validator validator = factory.getValidator(); + Set> violations = validator.validate(builtinUser); + logger.fine("constraint violation count: " + violations.size()); + builtinUser = builtinUserServiceBean.save(builtinUser); + return builtinUser; + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/MyDataQueryHelperServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/MyDataQueryHelperServiceBean.java index 41139fb8fd6..2bd577ceb5d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/MyDataQueryHelperServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/MyDataQueryHelperServiceBean.java @@ -123,7 +123,7 @@ public List getRolesOnDVO(AuthenticatedUser user, Long dvoId, List } - List roles = roleAssigneeService.getRoleIdListForGivenAssigneeDvObject(user.getIdentifier(), idsForSelect, dvoId); + List roles = roleAssigneeService.getRoleIdListForGivenAssigneeDvObject(user, idsForSelect, dvoId); /* List results = em.createNativeQuery("Select distinct role.role_id FROM roleassignment role WHERE " + " role.definitionpoint_id = " + dvoId + " " @@ -155,7 +155,7 @@ public List getRolesOnDVO(AuthenticatedUser user, Long dvoId, List + ")" + roleClause + ";").getResultList();*/ - List resultsParent = roleAssigneeService.getRoleIdListForGivenAssigneeDvObject(user.getIdentifier(), idsForSelect, parentId); + List resultsParent = roleAssigneeService.getRoleIdListForGivenAssigneeDvObject(user, idsForSelect, parentId); if (resultsParent != null && !resultsParent.isEmpty()) { for (Object result : resultsParent) { Long role_id = (Long) result; @@ -181,7 +181,7 @@ public List getRolesOnDVO(AuthenticatedUser user, Long dvoId, List + roleClause + ";").getResultList(); */ - List resultsGrandParent = roleAssigneeService.getRoleIdListForGivenAssigneeDvObject(user.getIdentifier(), idsForSelect, grandParentId); + List resultsGrandParent = roleAssigneeService.getRoleIdListForGivenAssigneeDvObject(user, idsForSelect, grandParentId); if (resultsGrandParent != null && !resultsGrandParent.isEmpty()) { for (Object result : resultsGrandParent) { Long role_id = (Long) result; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserPage.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserPage.java index 8234a88f6ca..484c1fa324a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/builtin/BuiltinUserPage.java @@ -17,6 +17,7 @@ import edu.harvard.iq.dataverse.DvObject; import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.RoleAssignment; +import edu.harvard.iq.dataverse.SettingsWrapper; import edu.harvard.iq.dataverse.UserNotification; import static edu.harvard.iq.dataverse.UserNotification.Type.CREATEDV; import edu.harvard.iq.dataverse.UserNotificationServiceBean; @@ -27,6 +28,8 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.mydata.MyDataPage; import edu.harvard.iq.dataverse.passwordreset.PasswordValidator; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.JsfHelper; import static edu.harvard.iq.dataverse.util.JsfHelper.JH; import java.io.UnsupportedEncodingException; @@ -87,6 +90,8 @@ public enum EditMode { @EJB GroupServiceBean groupService; @Inject + SettingsWrapper settingsWrapper; + @Inject MyDataPage mydatapage; @EJB @@ -134,10 +139,6 @@ public EditMode getEditMode() { public void setEditMode(EditMode editMode) { this.editMode = editMode; - - if (editMode == EditMode.CREATE) { - JH.addMessage(FacesMessage.SEVERITY_INFO, JH.localize("user.signup.tip")); - } } public String getRedirectPage() { @@ -211,8 +212,19 @@ public void setUsernameField(UIInput usernameField) { } public String init() { + + // prevent creating a user if signup not allowed. + boolean safeDefaultIfKeyNotFound = true; + boolean signupAllowed = settingsWrapper.isTrueForKey(SettingsServiceBean.Key.AllowSignUp.toString(), safeDefaultIfKeyNotFound); + logger.fine("signup is allowed: " + signupAllowed); + + if (editMode == EditMode.CREATE && !signupAllowed) { + return "/403.xhtml"; + } + if (editMode == EditMode.CREATE) { if (!session.getUser().isAuthenticated()) { // in create mode for new user + JH.addMessage(FacesMessage.SEVERITY_INFO, BundleUtil.getStringFromBundle("user.signup.tip")); builtinUser = new BuiltinUser(); return ""; } else { @@ -241,6 +253,9 @@ public String init() { activeIndex = 2; // activeIndex = 3; break; + case "apiTokenTab": + activeIndex = 3; + break; default: activeIndex = 0; break; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibAuthenticationProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibAuthenticationProvider.java index 46ca0c5cb01..0ca36a36625 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibAuthenticationProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibAuthenticationProvider.java @@ -7,9 +7,11 @@ public class ShibAuthenticationProvider implements AuthenticationProvider { + public static final String PROVIDER_ID = "shib"; + @Override public String getId() { - return "shib"; + return PROVIDER_ID; } @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibServiceBean.java index 9cf179fc135..f94d21a3ab2 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibServiceBean.java @@ -1,5 +1,6 @@ package edu.harvard.iq.dataverse.authorization.providers.shib; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonIOException; import com.google.gson.JsonObject; @@ -12,8 +13,11 @@ import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; +import static edu.harvard.iq.dataverse.authorization.providers.shib.ShibUtil.getRandomUserStatic; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.User; +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; +import edu.harvard.iq.dataverse.util.SystemConfig; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -23,42 +27,140 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -import java.util.logging.Level; import java.util.logging.Logger; import javax.ejb.EJB; +import javax.ejb.EJBException; import javax.ejb.Stateless; import javax.inject.Named; +import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; @Named @Stateless public class ShibServiceBean { - + private static final Logger logger = Logger.getLogger(ShibServiceBean.class.getCanonicalName()); - + @EJB AuthenticationServiceBean authSvc; @EJB BuiltinUserServiceBean builtinUserService; - + @EJB + SystemConfig systemConfig; + @EJB + SettingsServiceBean settingsService; + + /** + * "Production" means "don't mess with the HTTP request". + */ + public enum DevShibAccountType { + + PRODUCTION, + RANDOM, + TESTSHIB1, + HARVARD1, + HARVARD2, + TWO_EMAILS, + INVALID_EMAIL, + MISSING_REQUIRED_ATTR, + }; + + public DevShibAccountType getDevShibAccountType() { + DevShibAccountType saneDefault = DevShibAccountType.PRODUCTION; + String settingReturned = settingsService.getValueForKey(SettingsServiceBean.Key.DebugShibAccountType); + logger.fine("setting returned: " + settingReturned); + if (settingReturned != null) { + try { + DevShibAccountType parsedValue = DevShibAccountType.valueOf(settingReturned); + return parsedValue; + } catch (IllegalArgumentException ex) { + logger.info("Couldn't parse value: " + ex + " - returning a sane default: " + saneDefault); + return saneDefault; + } + } else { + logger.fine("Shibboleth dev mode has not been configured. Returning a sane default: " + saneDefault); + return saneDefault; + } + } + + /** + * This method exists so developers don't have to run Shibboleth locally. + * You can populate the request with Shibboleth attributes by changing a + * setting like this: + * + * curl -X PUT -d RANDOM + * http://localhost:8080/api/admin/settings/:DebugShibAccountType + * + * When you're done, feel free to delete the setting: + * + * curl -X DELETE + * http://localhost:8080/api/admin/settings/:DebugShibAccountType + * + * Note that setting ShibUseHeaders to true will make this "dev mode" stop + * working. + */ + public void possiblyMutateRequestInDev(HttpServletRequest request) { + switch (getDevShibAccountType()) { + case PRODUCTION: + logger.fine("Request will not be mutated"); + break; + + case RANDOM: + mutateRequestForDevRandom(request); + break; + + case TESTSHIB1: + ShibUtil.mutateRequestForDevConstantTestShib1(request); + break; + + case HARVARD1: + ShibUtil.mutateRequestForDevConstantHarvard1(request); + break; + + case HARVARD2: + ShibUtil.mutateRequestForDevConstantHarvard2(request); + break; + + case TWO_EMAILS: + ShibUtil.mutateRequestForDevConstantTwoEmails(request); + break; + + case INVALID_EMAIL: + ShibUtil.mutateRequestForDevConstantInvalidEmail(request); + break; + + case MISSING_REQUIRED_ATTR: + ShibUtil.mutateRequestForDevConstantMissingRequiredAttributes(request); + break; + + default: + logger.info("Should never reach here"); + break; + } + } + public AuthenticatedUser findAuthUserByEmail(String emailToFind) { return authSvc.getAuthenticatedUserByEmail(emailToFind); } - + public BuiltinUser findBuiltInUserByAuthUserIdentifier(String authUserIdentifier) { return builtinUserService.findByUserName(authUserIdentifier); } - + public AuthenticatedUser canLogInAsBuiltinUser(String username, String password) { - logger.info("checking to see if " + username + " knows the password..."); + logger.fine("checking to see if " + username + " knows the password..."); if (password == null) { logger.info("password was null"); return null; } - + AuthenticationRequest authReq = new AuthenticationRequest(); - authReq.putCredential("Username", username); - authReq.putCredential("Password", password); + /** + * @todo Should this really be coming from a bundle like this? Added + * because that's what BuiltinAuthenticationProvider does. + */ + authReq.putCredential(BundleUtil.getStringFromBundle("login.builtin.credential.usernameOrEmail"), username); + authReq.putCredential(BundleUtil.getStringFromBundle("login.builtin.credential.password"), password); /** * @todo Should probably set IP address here. */ @@ -67,34 +169,117 @@ public AuthenticatedUser canLogInAsBuiltinUser(String username, String password) String credentialsAuthProviderId = BuiltinAuthenticationProvider.PROVIDER_ID; try { AuthenticatedUser au = authSvc.authenticate(credentialsAuthProviderId, authReq); - logger.log(Level.INFO, "User authenticated: {0}", au.getEmail()); + logger.fine("User authenticated:" + au.getEmail()); return au; } catch (AuthenticationFailedException ex) { - logger.info("The username and/or password you entered is invalid. Need assistance accessing your account?" + ex.getResponse().getMessage()); + logger.info("The username and/or password entered is invalid: " + ex.getResponse().getMessage()); + return null; + } catch (EJBException ex) { + Throwable cause = ex; + StringBuilder sb = new StringBuilder(); + sb.append(ex + " "); + while (cause.getCause() != null) { + cause = cause.getCause(); + sb.append(cause.getClass().getCanonicalName() + " "); + sb.append(cause.getMessage()).append(" "); + /** + * @todo Investigate why authSvc.authenticate is throwing + * NullPointerException. If you convert a Shib user to a Builtin + * user, the password may be null. + */ + if (cause instanceof NullPointerException) { + for (int i = 0; i < 2; i++) { + StackTraceElement stacktrace = cause.getStackTrace()[i]; + if (stacktrace != null) { + String classCanonicalName = stacktrace.getClass().getCanonicalName(); + String methodName = stacktrace.getMethodName(); + int lineNumber = stacktrace.getLineNumber(); + String error = "at " + stacktrace.getClassName() + "." + stacktrace.getMethodName() + "(" + stacktrace.getFileName() + ":" + lineNumber + ") "; + sb.append(error); + } + } + } + } + logger.info("When trying to validate password, exception calling authSvc.authenticate: " + sb.toString()); return null; } } - /** - * @todo Move the getAffiliation method from the Shib JSF backing bean to - * here. - */ - public String getFriendlyInstitutionName(String entityId) { - /** - * @todo Look for the entityId (i.e. - * "https://idp.testshib.org/idp/shibboleth") for find "TestShib Test - * IdP" in (for example) - * https://demo.dataverse.org/Shibboleth.sso/DiscoFeed - * - * It looks something like this: [ { "entityID": - * "https://idp.testshib.org/idp/shibboleth", "DisplayNames": [ { - * "value": "TestShib Test IdP", "lang": "en" } ], "Descriptions": [ { - * "value": "TestShib IdP. Use this as a source of attributes\n for your - * test SP.", "lang": "en" } ], "Logos": [ { "value": - * "https://www.testshib.org/testshibtwo.jpg", "height": "88", "width": - * "253" } ] } ] - */ - return null; + public String getAffiliation(String shibIdp, DevShibAccountType devShibAccountType) { + JsonArray emptyJsonArray = new JsonArray(); + String discoFeedJson = emptyJsonArray.toString(); + String discoFeedUrl; + if (devShibAccountType.equals(DevShibAccountType.PRODUCTION)) { + discoFeedUrl = systemConfig.getDataverseSiteUrl() + "/Shibboleth.sso/DiscoFeed"; + } else { + String devUrl = "http://localhost:8080/resources/dev/sample-shib-identities.json"; + discoFeedUrl = devUrl; + } + logger.fine("Trying to get affiliation from disco feed URL: " + discoFeedUrl); + URL url = null; + try { + url = new URL(discoFeedUrl); + } catch (MalformedURLException ex) { + logger.info(ex.toString()); + return null; + } + if (url == null) { + logger.info("url object was null after parsing " + discoFeedUrl); + return null; + } + HttpURLConnection discoFeedRequest = null; + try { + discoFeedRequest = (HttpURLConnection) url.openConnection(); + } catch (IOException ex) { + logger.info(ex.toString()); + return null; + } + if (discoFeedRequest == null) { + logger.info("disco feed request was null"); + return null; + } + try { + discoFeedRequest.connect(); + } catch (IOException ex) { + logger.info(ex.toString()); + return null; + } + JsonParser jp = new JsonParser(); + JsonElement root = null; + try { + root = jp.parse(new InputStreamReader((InputStream) discoFeedRequest.getInputStream())); + } catch (IOException ex) { + logger.info(ex.toString()); + return null; + } + if (root == null) { + logger.info("root was null"); + return null; + } + JsonArray rootArray = root.getAsJsonArray(); + if (rootArray == null) { + logger.info("Couldn't get JSON Array from URL"); + return null; + } + discoFeedJson = rootArray.toString(); + logger.fine("Dump of disco feed:" + discoFeedJson); + String affiliation = ShibUtil.getDisplayNameFromDiscoFeed(shibIdp, discoFeedJson); + if (affiliation != null) { + return affiliation; + } else { + logger.info("Couldn't find an affiliation from " + shibIdp); + return null; + } + } + + private void mutateRequestForDevRandom(HttpServletRequest request) { + Map randomUser = getRandomUser(); + request.setAttribute(ShibUtil.lastNameAttribute, randomUser.get("lastName")); + request.setAttribute(ShibUtil.firstNameAttribute, randomUser.get("firstName")); + request.setAttribute(ShibUtil.emailAttribute, randomUser.get("email")); + request.setAttribute(ShibUtil.shibIdpAttribute, randomUser.get("idp")); + // eppn + request.setAttribute(ShibUtil.uniquePersistentIdentifier, UUID.randomUUID().toString().substring(0, 8)); } /** @@ -102,31 +287,34 @@ public String getFriendlyInstitutionName(String entityId) { */ public Map getRandomUser() throws JsonSyntaxException, JsonIOException { Map fakeUser = new HashMap<>(); - String sURL = "http://api.randomuser.me"; + String sURL = "http://api.randomuser.me/0.8"; URL url = null; try { url = new URL(sURL); } catch (MalformedURLException ex) { - Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex); + logger.info("Exception: " + ex); } HttpURLConnection randomUserRequest = null; try { randomUserRequest = (HttpURLConnection) url.openConnection(); } catch (IOException ex) { - Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex); + logger.info("Exception: " + ex); } try { randomUserRequest.connect(); } catch (IOException ex) { - Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex); + logger.info("Exception: " + ex); } - + JsonParser jp = new JsonParser(); JsonElement root = null; try { root = jp.parse(new InputStreamReader((InputStream) randomUserRequest.getContent())); } catch (IOException ex) { - Logger.getLogger(Shib.class.getName()).log(Level.SEVERE, null, ex); + logger.info("Exception: " + ex); + } + if (root == null) { + return getRandomUserStatic(); } JsonObject rootObject = root.getAsJsonObject(); logger.fine(rootObject.toString()); @@ -142,13 +330,16 @@ public Map getRandomUser() throws JsonSyntaxException, JsonIOExc JsonElement name = user.getAsJsonObject().get("name"); JsonElement firstName = name.getAsJsonObject().get("first"); JsonElement lastName = name.getAsJsonObject().get("last"); - fakeUser.put("firstName", firstName.getAsString()); - fakeUser.put("lastName", lastName.getAsString()); - fakeUser.put("displayName", StringUtils.capitalise(firstName.getAsString()) + " " + StringUtils.capitalise(lastName.getAsString())); + String firstNameString = StringUtils.capitalize(firstName.getAsString()); + String lastNameString = StringUtils.capitalize(lastName.getAsString()); + fakeUser.put("firstName", firstNameString); + fakeUser.put("lastName", lastNameString); + fakeUser.put("displayName", firstNameString + " " + lastNameString); fakeUser.put("email", email.getAsString()); fakeUser.put("idp", "https://idp." + password.getAsString() + ".com/idp/shibboleth"); fakeUser.put("username", username.getAsString()); fakeUser.put("eppn", salt.getAsString()); return fakeUser; } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibUtil.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibUtil.java index 0b73895ee74..a698498aa16 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibUtil.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/shib/ShibUtil.java @@ -4,16 +4,43 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import edu.harvard.iq.dataverse.EMailValidator; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.logging.Logger; +import javax.servlet.http.HttpServletRequest; public class ShibUtil { private static final Logger logger = Logger.getLogger(ShibUtil.class.getCanonicalName()); /** - * @todo Use this to display "Harvard University", for example, based on + * @todo make this configurable? See + * https://github.com/IQSS/dataverse/issues/2129 + */ + public static final String shibIdpAttribute = "Shib-Identity-Provider"; + /** + * @todo Make attribute used (i.e. "eppn") configurable: + * https://github.com/IQSS/dataverse/issues/1422 + * + * OR *maybe* we can rely on people installing Dataverse to configure shibd + * to always send "eppn" as an attribute, via attribute mappings or what + * have you. + */ + public static final String uniquePersistentIdentifier = "eppn"; + public static final String usernameAttribute = "uid"; + public static final String displayNameAttribute = "cn"; + public static final String firstNameAttribute = "givenName"; + public static final String lastNameAttribute = "sn"; + public static final String emailAttribute = "mail"; + public static final String testShibIdpEntityId = "https://idp.testshib.org/idp/shibboleth"; + + /** + * Used to display "Harvard University", for example, based on * https://dataverse.harvard.edu/Shibboleth.sso/DiscoFeed */ public static String getDisplayNameFromDiscoFeed(String entityIdToFind, String discoFeed) { @@ -61,13 +88,17 @@ public static String getDisplayNameFromDiscoFeed(String entityIdToFind, String d * "displayName" so we'll hold off on implementing anything for now. */ public static ShibUserNameFields findBestFirstAndLastName(String firstName, String lastName, String displayName) { - firstName = getSingleName(firstName); - lastName = getSingleName(lastName); + firstName = findSingleValue(firstName); + lastName = findSingleValue(lastName); return new ShibUserNameFields(firstName, lastName); } - private static String getSingleName(String name) { - String[] parts = name.split(";"); + public static String findSingleValue(String mayHaveMultipleValues) { + if (mayHaveMultipleValues == null) { + return null; + } + String singleValue = mayHaveMultipleValues; + String[] parts = mayHaveMultipleValues.split(";"); if (parts.length != 1) { logger.fine("parts (before sorting): " + Arrays.asList(parts)); // predictable order (sorted alphabetically) @@ -75,12 +106,16 @@ private static String getSingleName(String name) { logger.fine("parts (after sorting): " + Arrays.asList(parts)); try { String first = parts[0]; - name = first; + singleValue = first; } catch (ArrayIndexOutOfBoundsException ex) { - logger.info("Couldn't find first part of " + name); + /** + * @todo Is it possible to reach this line via a test? If not, + * remove this try/catch. + */ + logger.fine("Couldn't find first part of " + singleValue); } } - return name; + return singleValue; } public static String generateFriendlyLookingUserIdentifer(String usernameAssertion, String email) { @@ -94,16 +129,198 @@ public static String generateFriendlyLookingUserIdentifer(String usernameAsserti String firstPart = parts[0]; return firstPart; } catch (ArrayIndexOutOfBoundsException ex) { - logger.info(ex + " parsing " + email); + /** + * @todo Is it possible to reach this line via a test? If + * not, remove this try/catch. + */ + logger.fine(ex + " parsing " + email); } } else { - logger.info("Odd email address. No @ sign: " + email); + boolean passedValidation = EMailValidator.isEmailValid(email, null); + logger.fine("Odd email address. No @ sign ('" + email + "'). Passed email validation: " + passedValidation); } } else { - logger.info("email attribute not sent by IdP"); + logger.fine("email attribute not sent by IdP"); } - logger.info("the best we can do is generate a random UUID"); + logger.fine("the best we can do is generate a random UUID"); return UUID.randomUUID().toString(); } + static void mutateRequestForDevConstantTestShib1(HttpServletRequest request) { + request.setAttribute(ShibUtil.shibIdpAttribute, ShibUtil.testShibIdpEntityId); + // the TestShib "eppn" looks like an email address + request.setAttribute(ShibUtil.uniquePersistentIdentifier, "saml@testshib.org"); +// request.setAttribute(displayNameAttribute, "Sam El"); + request.setAttribute(ShibUtil.firstNameAttribute, "Samuel;Sam"); + request.setAttribute(ShibUtil.lastNameAttribute, "El"); + // TestShib doesn't send "mail" attribute so let's mimic that. +// request.setAttribute(emailAttribute, "saml@mailinator.com"); + request.setAttribute(ShibUtil.usernameAttribute, "saml"); + } + + static void mutateRequestForDevConstantHarvard1(HttpServletRequest request) { + /** + * Harvard's IdP doesn't send a username (uid). + */ + request.setAttribute(ShibUtil.shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth"); + request.setAttribute(ShibUtil.uniquePersistentIdentifier, "constantHarvard"); + /** + * @todo Does Harvard really send displayName? At one point they didn't. + * Let's simulate the non-sending of displayName here. + */ +// request.setAttribute(displayNameAttribute, "John Harvard"); + request.setAttribute(ShibUtil.firstNameAttribute, "John"); + request.setAttribute(ShibUtil.lastNameAttribute, "Harvard"); + request.setAttribute(ShibUtil.emailAttribute, "jharvard@mailinator.com"); + request.setAttribute(ShibUtil.usernameAttribute, "jharvard"); + } + + static void mutateRequestForDevConstantHarvard2(HttpServletRequest request) { + request.setAttribute(ShibUtil.shibIdpAttribute, "https://fed.huit.harvard.edu/idp/shibboleth"); + request.setAttribute(ShibUtil.uniquePersistentIdentifier, "constantHarvard2"); +// request.setAttribute(displayNameAttribute, "Grace Hopper"); + request.setAttribute(ShibUtil.firstNameAttribute, "Grace"); + request.setAttribute(ShibUtil.lastNameAttribute, "Hopper"); + request.setAttribute(ShibUtil.emailAttribute, "ghopper@mailinator.com"); + request.setAttribute(ShibUtil.usernameAttribute, "ghopper"); + } + + static void mutateRequestForDevConstantTwoEmails(HttpServletRequest request) { + request.setAttribute(ShibUtil.shibIdpAttribute, "https://fake.example.com/idp/shibboleth"); + request.setAttribute(ShibUtil.uniquePersistentIdentifier, "twoEmails"); + request.setAttribute(ShibUtil.firstNameAttribute, "Eric"); + request.setAttribute(ShibUtil.lastNameAttribute, "Allman"); + request.setAttribute(ShibUtil.emailAttribute, "eric1@mailinator.com;eric2@mailinator.com"); + request.setAttribute(ShibUtil.usernameAttribute, "eallman"); + } + + static void mutateRequestForDevConstantInvalidEmail(HttpServletRequest request) { + request.setAttribute(ShibUtil.shibIdpAttribute, "https://fake.example.com/idp/shibboleth"); + request.setAttribute(ShibUtil.uniquePersistentIdentifier, "invalidEmail"); + request.setAttribute(ShibUtil.firstNameAttribute, "Invalid"); + request.setAttribute(ShibUtil.lastNameAttribute, "Email"); + request.setAttribute(ShibUtil.emailAttribute, "invalidEmail"); + request.setAttribute(ShibUtil.usernameAttribute, "invalidEmail"); + } + + static void mutateRequestForDevConstantMissingRequiredAttributes(HttpServletRequest request) { + request.setAttribute(ShibUtil.shibIdpAttribute, "https://fake.example.com/idp/shibboleth"); + /** + * @todo When shibIdpAttribute is set to null why don't we see the error + * in the GUI? + */ +// request.setAttribute(shibIdpAttribute, null); + request.setAttribute(ShibUtil.uniquePersistentIdentifier, "missing"); + request.setAttribute(ShibUtil.uniquePersistentIdentifier, null); + request.setAttribute(ShibUtil.firstNameAttribute, "Missing"); + request.setAttribute(ShibUtil.lastNameAttribute, "Required"); + request.setAttribute(ShibUtil.emailAttribute, "missing@mailinator.com"); + request.setAttribute(ShibUtil.usernameAttribute, "missing"); + } + + public static Map getRandomUserStatic() { + Map fakeUser = new HashMap<>(); + String shortRandomString = UUID.randomUUID().toString().substring(0, 8); + fakeUser.put("firstName", shortRandomString); + fakeUser.put("lastName", shortRandomString); + fakeUser.put("displayName", shortRandomString + " " + shortRandomString); + fakeUser.put("email", shortRandomString + "@mailinator.com"); + fakeUser.put("idp", "https://idp." + shortRandomString + ".com/idp/shibboleth"); + fakeUser.put("username", shortRandomString); + fakeUser.put("eppn", shortRandomString); + return fakeUser; + } + + /** + * These are attributes that were found to be interesting while developing + * the Shibboleth feature. Only the ones that are defined elsewhere are + * actually used. + */ + static List shibAttrs = Arrays.asList( + ShibUtil.shibIdpAttribute, + ShibUtil.uniquePersistentIdentifier, + ShibUtil.usernameAttribute, + ShibUtil.displayNameAttribute, + ShibUtil.firstNameAttribute, + ShibUtil.lastNameAttribute, + ShibUtil.emailAttribute, + "telephoneNumber", + "affiliation", + "unscoped-affiliation", + "entitlement", + "persistent-id" + ); + + /** + * These are the attributes we are getting from the IdP at testshib.org, a + * dump from https://pdurbin.pagekite.me/Shibboleth.sso/Session + * + * Miscellaneous + * + * Session Expiration (barring inactivity): 479 minute(s) + * + * Client Address: 10.0.2.2 + * + * SSO Protocol: urn:oasis:names:tc:SAML:2.0:protocol + * + * Identity Provider: https://idp.testshib.org/idp/shibboleth + * + * Authentication Time: 2014-09-12T17:07:36.137Z + * + * Authentication Context Class: + * urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + * + * Authentication Context Decl: (none) + * + * + * + * Attributes + * + * affiliation: Member@testshib.org;Staff@testshib.org + * + * cn: Me Myself And I + * + * entitlement: urn:mace:dir:entitlement:common-lib-terms + * + * eppn: myself@testshib.org + * + * givenName: Me Myself + * + * persistent-id: + * https://idp.testshib.org/idp/shibboleth!https://pdurbin.pagekite.me/shibboleth!zylzL+NruovU5OOGXDOL576jxfo= + * + * sn: And I + * + * telephoneNumber: 555-5555 + * + * uid: myself + * + * unscoped-affiliation: Member;Staff + * + */ + public static void printAttributes(HttpServletRequest request) { + List shibValues = new ArrayList<>(); + if (request == null) { + logger.fine("HttpServletRequest was null. No shib values to print."); + return; + } + for (String attr : shibAttrs) { + + /** + * @todo explain in Installers Guide that in order for these + * attributes to be found attributePrefix="AJP_" must be added to + * /etc/shibboleth/shibboleth2.xml like this: + * + * + * + */ + Object attrObject = request.getAttribute(attr); + if (attrObject != null) { + shibValues.add(attr + ": " + attrObject.toString()); + } + } + logger.fine("shib values: " + shibValues); + } + } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java index bc0e59b0e2d..4c9236c0b02 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/users/AuthenticatedUser.java @@ -53,11 +53,7 @@ public class AuthenticatedUser implements User, Serializable { @Column(nullable = false, unique=true) private String userIdentifier; - /** - * @todo Uncomment the ValidateEmail annotation below for consistency with - * the annotation on BuiltinUser. - */ -// @ValidateEmail(message = "Please enter a valid email address.") + @ValidateEmail(message = "Please enter a valid email address.") @NotNull @Column(nullable = false, unique=true) private String email; diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java index 3343c0f1b0f..3744a90241a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/DataRetrieverAPI.java @@ -17,6 +17,7 @@ import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.DataverseRolePermissionHelper; import edu.harvard.iq.dataverse.authorization.MyDataQueryHelperServiceBean; +import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.search.SearchConstants; import edu.harvard.iq.dataverse.search.SearchException; @@ -67,8 +68,10 @@ public class DataRetrieverAPI extends AbstractApiBean { SearchServiceBean searchService; @EJB AuthenticationServiceBean authenticationService; - @EJB + @EJB MyDataQueryHelperServiceBean myDataQueryHelperServiceBean; + @EJB + GroupServiceBean groupService; private List roleList; private DataverseRolePermissionHelper rolePermissionHelper; @@ -196,7 +199,7 @@ private SolrQueryResponse getTotalCountsFromSolr(AuthenticatedUser searchUser, M // ------------------------------------------------------- // Create new filter params that only check by the User // ------------------------------------------------------- - MyDataFilterParams filterParams = new MyDataFilterParams(searchUser.getIdentifier(), myDataFinder.getRolePermissionHelper()); + MyDataFilterParams filterParams = new MyDataFilterParams(searchUser, myDataFinder.getRolePermissionHelper()); if (filterParams.hasError()){ logger.severe("getTotalCountsFromSolr. filterParams error: " + filterParams.getErrorMessage()); return null; @@ -348,7 +351,7 @@ public String retrieveMyDataAsJsonString(@QueryParam("dvobject_types") List dvObjectTypes; private List publicationStatuses; @@ -94,16 +96,19 @@ public class MyDataFilterParams { /** * Constructor used to get total counts * + * @param authenticatedUser * @param userIdentifier */ - public MyDataFilterParams(String userIdentifier, DataverseRolePermissionHelper roleHelper){ - if ((userIdentifier==null)||(userIdentifier.isEmpty())){ - throw new NullPointerException("MyDataFilterParams constructor: userIdentifier cannot be null or an empty string"); + public MyDataFilterParams(AuthenticatedUser authenticatedUser, DataverseRolePermissionHelper roleHelper){ + if (authenticatedUser==null){ + throw new NullPointerException("MyDataFilterParams constructor: authenticatedIUser cannot be null "); } + this.authenticatedUser = authenticatedUser; + this.userIdentifier = authenticatedUser.getIdentifier(); + if (roleHelper==null){ throw new NullPointerException("MyDataFilterParams constructor: roleHelper cannot be null"); } - this.userIdentifier = userIdentifier; this.dvObjectTypes = MyDataFilterParams.allDvObjectTypes; this.publicationStatuses = MyDataFilterParams.allPublishedStates; this.searchTerm = MyDataFilterParams.defaultSearchTerm; @@ -116,16 +121,17 @@ public MyDataFilterParams(String userIdentifier, DataverseRolePermissionHelper r * @param publicationStatuses * @param searchTerm */ - public MyDataFilterParams(String userIdentifier, List dvObjectTypes, List publicationStatuses, List roleIds, String searchTerm){ - if ((userIdentifier==null)||(userIdentifier.isEmpty())){ - throw new NullPointerException("MyDataFilterParams constructor: userIdentifier cannot be null or an empty string"); + public MyDataFilterParams(AuthenticatedUser authenticatedUser, List dvObjectTypes, List publicationStatuses, List roleIds, String searchTerm){ + if (authenticatedUser==null){ + throw new NullPointerException("MyDataFilterParams constructor: authenticatedIUser cannot be null "); } + this.authenticatedUser = authenticatedUser; + this.userIdentifier = authenticatedUser.getIdentifier(); if (dvObjectTypes==null){ throw new NullPointerException("MyDataFilterParams constructor: dvObjectTypes cannot be null"); } - this.userIdentifier = userIdentifier; this.dvObjectTypes = dvObjectTypes; if (publicationStatuses == null){ @@ -192,6 +198,11 @@ public String getUserIdentifier(){ return this.userIdentifier; } + + public AuthenticatedUser getAuthenticatedUser() { + return authenticatedUser; + } + public String getErrorMessage(){ return this.errorMessage; } diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFinder.java b/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFinder.java index a50cf93c11f..548878f5814 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFinder.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataFinder.java @@ -9,6 +9,7 @@ import edu.harvard.iq.dataverse.DvObjectServiceBean; import edu.harvard.iq.dataverse.RoleAssigneeServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRolePermissionHelper; +import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.search.SearchFields; import java.util.ArrayList; import java.util.HashMap; @@ -31,7 +32,7 @@ */ //@Stateless public class MyDataFinder { - + private static final Logger logger = Logger.getLogger(MyDataFinder.class.getCanonicalName()); private String userIdentifier; @@ -45,6 +46,7 @@ public class MyDataFinder { private DataverseRolePermissionHelper rolePermissionHelper; private RoleAssigneeServiceBean roleAssigneeService; private DvObjectServiceBean dvObjectServiceBean; + private GroupServiceBean groupService; //private RoleAssigneeServiceBean roleService = new RoleAssigneeServiceBean(); //private MyDataQueryHelperServiceBean myDataQueryHelperService; // -------------------- @@ -83,11 +85,12 @@ public class MyDataFinder { private List fileGrandparentFileIds = new ArrayList<>(); // dataverse has file permissions - public MyDataFinder(DataverseRolePermissionHelper rolePermissionHelper, RoleAssigneeServiceBean roleAssigneeService, DvObjectServiceBean dvObjectServiceBean) { + public MyDataFinder(DataverseRolePermissionHelper rolePermissionHelper, RoleAssigneeServiceBean roleAssigneeService, DvObjectServiceBean dvObjectServiceBean, GroupServiceBean groupService) { this.msgt("MyDataFinder, constructor"); this.rolePermissionHelper = rolePermissionHelper; this.roleAssigneeService = roleAssigneeService; this.dvObjectServiceBean = dvObjectServiceBean; + this.groupService = groupService; this.loadHarvestedDataverseIds(); } @@ -234,7 +237,6 @@ private List getSolrFilterQueries(boolean totalCountsOnly){ return null; } filterQueries.add(dvObjectFQ); - // ----------------------------------------------------------------- // For total counts, don't filter by publicationStatus or DvObjectType // ----------------------------------------------------------------- @@ -255,12 +257,12 @@ private List getSolrFilterQueries(boolean totalCountsOnly){ // ----------------------------------------------------------------- filterQueries.add(this.filterParams.getSolrFragmentForPublicationStatus()); //fq=publicationStatus:"Unpublished"&fq=publicationStatus:"Draft" - + return filterQueries; } - - + + @@ -444,7 +446,7 @@ public JsonArrayBuilder getListofSelectedRoles(){ private boolean runStep1RoleAssignments(){ - List results = this.roleAssigneeService.getAssigneeAndRoleIdListFor(MyDataUtil.formatUserIdentifierAsAssigneeIdentifier(this.userIdentifier) + List results = this.roleAssigneeService.getAssigneeAndRoleIdListFor(filterParams.getAuthenticatedUser() , this.filterParams.getRoleIds()); //msgt("runStep1RoleAssignments results: " + results.toString()); diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataPage.java b/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataPage.java index c082cb906fd..f280cb1ff22 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/MyDataPage.java @@ -182,7 +182,7 @@ public String init() { // Initialize a filterParams object to buid the Publication Status checkboxes // - this.filterParams = new MyDataFilterParams(authUser.getIdentifier(), MyDataFilterParams.defaultDvObjectTypes, null, null, null); + this.filterParams = new MyDataFilterParams(authUser, MyDataFilterParams.defaultDvObjectTypes, null, null, null); // Temp DataverseRolePermissionHelper -- not in its normal role but for creating initial checkboxes @@ -259,7 +259,7 @@ private List getRolesUsedToCreateCheckboxes(AuthenticatedUser aut roleList = dataverseRoleService.findAll(); }else{ // (2) For a regular users - roleList = roleAssigneeService.getAssigneeDataverseRoleFor(this.filterParams.getUserIdentifier()); + roleList = roleAssigneeService.getAssigneeDataverseRoleFor(authUser); // If there are no assigned roles, show them all? // This may not make sense diff --git a/src/main/java/edu/harvard/iq/dataverse/mydata/RoleTagRetriever.java b/src/main/java/edu/harvard/iq/dataverse/mydata/RoleTagRetriever.java index 5160152b698..17477dda014 100644 --- a/src/main/java/edu/harvard/iq/dataverse/mydata/RoleTagRetriever.java +++ b/src/main/java/edu/harvard/iq/dataverse/mydata/RoleTagRetriever.java @@ -11,6 +11,7 @@ import edu.harvard.iq.dataverse.search.SolrQueryResponse; import edu.harvard.iq.dataverse.search.SolrSearchResult; import edu.harvard.iq.dataverse.authorization.DataverseRolePermissionHelper; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.search.SearchConstants; import java.util.ArrayList; import java.util.Collections; @@ -68,8 +69,13 @@ public RoleTagRetriever(DataverseRolePermissionHelper rolePermissionHelper this.dvObjectServiceBean = dvObjectServiceBean; } - public void loadRoles(String userIdentifier, SolrQueryResponse solrQueryResponse){ - + public void loadRoles(AuthenticatedUser au , SolrQueryResponse solrQueryResponse){ + + if (au == null){ + throw new NullPointerException("RoleTagRetriever.constructor. au cannot be null"); + } + + String userIdentifier = au.getUserIdentifier(); if (userIdentifier == null){ throw new NullPointerException("RoleTagRetriever.constructor. userIdentifier cannot be null"); } @@ -88,7 +94,7 @@ public void loadRoles(String userIdentifier, SolrQueryResponse solrQueryResponse findDataverseIdsForFiles(); // (4) Retrieve the role ids - retrieveRoleIdsForDvObjects(userIdentifier); + retrieveRoleIdsForDvObjects(au); // (5) Prepare final role lists prepareFinalRoleLists(); @@ -343,8 +349,9 @@ private void findDataverseIdsForFiles(){ } - private boolean retrieveRoleIdsForDvObjects(String userIdentifier){ - + private boolean retrieveRoleIdsForDvObjects(AuthenticatedUser au ){ + + String userIdentifier = au.getUserIdentifier(); if (userIdentifier == null){ throw new NullPointerException("RoleTagRetriever.constructor. userIdentifier cannot be null"); } @@ -358,8 +365,8 @@ private boolean retrieveRoleIdsForDvObjects(String userIdentifier){ return true; } //msg("dvObjectIdList: " + dvObjectIdList.toString()); - String assigneeIdentifer = MyDataUtil.formatUserIdentifierAsAssigneeIdentifier(userIdentifier); - List results = this.roleAssigneeService.getRoleIdsFor(assigneeIdentifer, dvObjectIdList); + + List results = this.roleAssigneeService.getRoleIdsFor(au, dvObjectIdList); //msgt("runStep1RoleAssignments results: " + results.toString()); diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java index befc5a8a607..2856df3a613 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/SettingsServiceBean.java @@ -96,8 +96,6 @@ public enum Key { * to from the footer. */ ApplicationPrivacyPolicyUrl, - /** Expose debug information in the UI that users shouldn't normally see. */ - Debug, /** * A boolean defining if indexing and search should respect the concept * of "permission root". @@ -129,8 +127,6 @@ public enum Key { DdiExportEnabled, /** Key for if Shibboleth is enabled or disabled. */ ShibEnabled, - /** Key for if Shibboleth is enabled or disabled. */ - ShibUseHeaders, /** Key for if ScrubMigrationData is enabled or disabled. */ ScrubMigrationData, /** Key for the url to send users who want to sign up to. */ diff --git a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java index 32f1615563c..ab762a16daf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/SystemConfig.java @@ -411,19 +411,6 @@ public boolean isShibEnabled() { return settingsService.isTrueForKey(SettingsServiceBean.Key.ShibEnabled, safeDefaultIfKeyNotFound); } - public boolean isShibUseHeaders() { - boolean safeDefaultIfKeyNotFound = false; - return settingsService.isTrueForKey(SettingsServiceBean.Key.ShibUseHeaders, safeDefaultIfKeyNotFound); - } - - // TODO: - // remove these method! - // pages should be using settingsWrapper.isTrueForKey(":Debug", false) instead. -- 4.2.1 - public boolean isDebugEnabled() { - boolean safeDefaultIfKeyNotFound = false; - return settingsService.isTrueForKey(SettingsServiceBean.Key.Debug, safeDefaultIfKeyNotFound); - } - public boolean myDataDoesNotUsePermissionDocs() { boolean safeDefaultIfKeyNotFound = false; return settingsService.isTrueForKey(SettingsServiceBean.Key.MyDataDoesNotUseSolrPermissionDocs, safeDefaultIfKeyNotFound); diff --git a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java index 57d9e7d8d5e..dd9957297e7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java +++ b/src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java @@ -37,6 +37,7 @@ import java.util.TreeSet; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; +import java.math.BigDecimal; import java.util.Collection; import java.util.Deque; import java.util.LinkedList; @@ -77,9 +78,17 @@ public static JsonObjectBuilder json( User u ) { */ public static JsonObjectBuilder jsonForAuthUser(AuthenticatedUser authenticatedUser) { return jsonObjectBuilder() + .add("id", authenticatedUser.getId()) .add("identifier", authenticatedUser.getIdentifier()) - .add("id", authenticatedUser.getId() - ); + .add("displayName", authenticatedUser.getDisplayInfo().getTitle()) + .add("firstName", authenticatedUser.getFirstName()) + .add("lastName", authenticatedUser.getLastName()) + .add("email", authenticatedUser.getEmail()) + .add("superuser", authenticatedUser.isSuperuser()) + .add("affiliation", authenticatedUser.getAffiliation()) + .add("position", authenticatedUser.getPosition()) + .add("persistentUserId", authenticatedUser.getAuthenticatedUserLookup().getPersistentUserId()) + .add("authenticationProviderId", authenticatedUser.getAuthenticatedUserLookup().getAuthenticationProviderId()); } public static JsonObjectBuilder json( RoleAssignment ra ) { diff --git a/src/main/webapp/WEB-INF/pretty-config.xml b/src/main/webapp/WEB-INF/pretty-config.xml index 487e7f9aed2..f8954c970ea 100644 --- a/src/main/webapp/WEB-INF/pretty-config.xml +++ b/src/main/webapp/WEB-INF/pretty-config.xml @@ -17,9 +17,4 @@ - - - - - \ No newline at end of file diff --git a/src/main/webapp/apitoken.xhtml b/src/main/webapp/apitoken.xhtml deleted file mode 100644 index eaf5f63fe3b..00000000000 --- a/src/main/webapp/apitoken.xhtml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - -

-   - - - - -

-
-
-                                    ${ApiTokenPage.apiToken}
-                                 
-
-
- -
-
-
-
-
-
-
- diff --git a/src/main/webapp/dataset.xhtml b/src/main/webapp/dataset.xhtml index 733bec67f58..495db3be6eb 100755 --- a/src/main/webapp/dataset.xhtml +++ b/src/main/webapp/dataset.xhtml @@ -1010,8 +1010,8 @@

-   - +   +

diff --git a/src/main/webapp/dataverse_header.xhtml b/src/main/webapp/dataverse_header.xhtml index 3a6f2acacca..7ec8e7337fc 100644 --- a/src/main/webapp/dataverse_header.xhtml +++ b/src/main/webapp/dataverse_header.xhtml @@ -139,6 +139,12 @@ +
  • + + + + +
  • @@ -154,11 +160,6 @@ - - -
    - -
    diff --git a/src/main/webapp/dataverseuser.xhtml b/src/main/webapp/dataverseuser.xhtml index be63a4bbd87..9bc5e653f3b 100644 --- a/src/main/webapp/dataverseuser.xhtml +++ b/src/main/webapp/dataverseuser.xhtml @@ -23,32 +23,12 @@ - - - - - - - - - - - - - - - - - - - -
    -
    +
    @@ -83,27 +58,12 @@ - - - - - - - - - - - - - - -
    @@ -304,77 +264,73 @@
    - -
    - #{bundle['user.toEditDetail']} -
    -
    + +
    +
    +
    +

    + + + + + + + + + +

    +
    +
    -

    #{DataverseUserPage.builtinUser.userName}

    +

    #{DataverseUserPage.currentUser.identifier.replaceFirst("@", "")}

    +

    #{DataverseUserPage.builtinUser.userName}

    -
    -

    #{DataverseUserPage.builtinUser.firstName}

    +

    #{DataverseUserPage.currentUser.firstName}

    -
    -

    #{DataverseUserPage.builtinUser.lastName}

    +

    #{DataverseUserPage.currentUser.lastName}

    -
    -

    #{DataverseUserPage.builtinUser.email}

    +

    #{DataverseUserPage.currentUser.email}

    - -
    +
    -

    #{DataverseUserPage.builtinUser.affiliation}

    +

    #{DataverseUserPage.currentUser.affiliation}

    - - -
    + +

    #{DataverseUserPage.builtinUser.position}

    @@ -382,6 +338,24 @@
    + +

    +   + + + + +

    +
    +
    +                                    ${ApiTokenPage.apiToken}
    +                                 
    +
    +
    + +
    +
    #{bundle['groupAndRoles.manageTips']} @@ -391,180 +365,155 @@ -
    - -

    -
    -
    - - -
    - -
    - - - - -
    -
    -
    - - - -
    - -
    - - - - -
    -
    -
    - - -
    - -
    -

    #{bundle['user.updatePassword.password']}

    - - - - -
    -
    -
    - - -
    - -
    - - - - -
    -
    -
    - - - -
    - -
    - - - - -
    -
    -
    - - -
    - -
    - - - - -
    -
    -
    - - - -
    - -
    - - - - -
    -
    -
    - - - -
    - -
    - - - - -
    -
    -
    - - - -
    - -
    - - - - -
    -
    -
    - - - - - + +

    +
    +
    +
    + + +
    + + + + +
    - -
    - - - - +
    + + +
    + + + + +
    +
    +
    + + +
    +

    #{bundle['user.updatePassword.password']}

    + + + + +
    +
    +
    + + +
    + + + + +
    +
    +
    + + +
    + + + + +
    +
    +
    + + +
    + + + + +
    +
    +
    + + +
    + + + + +
    +
    +
    + + +
    + + + + +
    +
    +
    + + +
    + + + + +
    +
    + + + + +
    +
    + + + + +
    diff --git a/src/main/webapp/loginpage.xhtml b/src/main/webapp/loginpage.xhtml index 0fe0e10fb7e..ae96aedccdc 100644 --- a/src/main/webapp/loginpage.xhtml +++ b/src/main/webapp/loginpage.xhtml @@ -35,6 +35,7 @@
    +

    #{bundle['login.builtin']}

    @@ -85,8 +86,29 @@

    #{bundle['login.institution']}

    +

    + + + + + + + + + + + + + + +

    + diff --git a/src/main/webapp/resources/js/shib/idpselect.js b/src/main/webapp/resources/js/shib/idpselect.js index b1ff42539cb..cf0afea643f 100644 --- a/src/main/webapp/resources/js/shib/idpselect.js +++ b/src/main/webapp/resources/js/shib/idpselect.js @@ -1774,7 +1774,7 @@ function IdPSelectUI() { aval.href = helpURL; aval.appendChild(document.createTextNode(getLocalizedMessage('helpText'))); setClass(aval, 'HelpButton'); - containerDiv.appendChild(aval); +// containerDiv.appendChild(aval); } ; /** diff --git a/src/main/webapp/resources/js/shib/idpselect_config.js b/src/main/webapp/resources/js/shib/idpselect_config.js index 4502a698be2..220ec34fbe9 100644 --- a/src/main/webapp/resources/js/shib/idpselect_config.js +++ b/src/main/webapp/resources/js/shib/idpselect_config.js @@ -14,9 +14,9 @@ function IdPSelectUIParms() { // Approaching via the Discovery Protocol for example //this.defaultReturn = "https://example.org/Shibboleth.sso/DS?SAMLDS=1&target=https://example.org/secure"; - this.defaultReturn = window.location.protocol + "//" + window.location.hostname + "/Shibboleth.sso/Login?SAMLDS=1&target=" + window.location.protocol + "//" + window.location.hostname + "/shib.xhtml"; + this.defaultReturn = window.location.protocol + "//" + window.location.hostname + "/Shibboleth.sso/Login?SAMLDS=1&target=" + window.location.protocol + "//" + window.location.hostname + "/shib.xhtml" + shibRedirectPage; this.defaultReturnIDParam = null; - this.helpURL = '/guides/user/account.html'; + this.helpURL = 'http://guides.dataverse.org/en/latest/user/account.html'; this.ie6Hack = null; // An array of structures to disable when drawing the pull down (needed to // handle the ie6 z axis problem this.insertAtDiv = 'idpSelect'; // The div where we will insert the data diff --git a/src/main/webapp/shib.xhtml b/src/main/webapp/shib.xhtml index 3d7efcc3d09..a9c8ba64937 100644 --- a/src/main/webapp/shib.xhtml +++ b/src/main/webapp/shib.xhtml @@ -3,135 +3,140 @@ xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" - xmlns:p="http://primefaces.org/ui"> + xmlns:p="http://primefaces.org/ui" + xmlns:jsf="http://xmlns.jcp.org/jsf"> - - + + - - - - - - - - - - - - - - - -
    - - -
    -
    - - - - -
    - #{bundle['shib.accountInformation']} - - - #{bundle['shib.offerToCreateNewAccount']} - -
    - - - -
    - -
    -

    - #{Shib.displayNameToPersist} -

    -
    -
    -
    - -
    -

    - #{Shib.emailToPersist} + + + + + + + + + + + + + + + +

    #{bundle['shib.askToConvert']}

    + +
    +

    + + + +

    -
    -
    - -
    -

    - #{Shib.affiliationToDisplayAtConfirmation} +

    +

    +

    -
    - - - -
    - -
    - - - - - - -

    #{bundle['shib.welcome']} #{Shib.existingDisplayName}

    -
    -
    -

    - - - - -

    -
    - - -
    - #{bundle['shib.passwordRejected']} -
    -
    - -
    -
    - -
    -

    - #{Shib.builtinUsername} -

    -
    -
    -
    - -
    - -
    -
    - - - -
    - -
    -
    - #{bundle['login.forgot.text']} + + + + +
    + #{bundle['shib.accountInformation']} – #{bundle['shib.offerToCreateNewAccount']}
    -
    - - - - - + + + + + + +
    + +
    +

    + #{Shib.displayNameToPersist} +

    +
    +
    +
    + +
    +

    + #{Shib.emailToPersist} +

    +
    +
    +
    + +
    +

    + #{Shib.affiliationToDisplayAtConfirmation} +

    +
    +
    + + + +
    +
    + + Cancel +
    +
    +
    + + +
    + +
    +

    + #{Shib.builtinUsername} +

    +
    +
    +
    + + +
    +
    + +
    +

    + #{Shib.emailToPersist} +

    +
    +
    + + + +
    +
    + +
    +
    +
    +
    +
    + + + \ No newline at end of file diff --git a/src/main/webapp/termsofuse.xhtml b/src/main/webapp/termsofuse.xhtml index 7a8e1a96d5f..fc059de505e 100644 --- a/src/main/webapp/termsofuse.xhtml +++ b/src/main/webapp/termsofuse.xhtml @@ -4,7 +4,7 @@ xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:p="http://primefaces.org/ui"> -
    +