Skip to content
Snippets Groups Projects
Commit 584faeab authored by Andy Summers's avatar Andy Summers
Browse files

Merge branch 'uwmadison-attr' into 'master'

Add UDDS attribute and refactor

Apologies for the large and hard to read PR--it should hopefully make the code more easy to read and navigate.

This PR adds UDDS as an attribute for `UserDetails`, along with a new class, `PreauthUserDetailsProvider`, which is an implementation of `UserDetailsProvider` for UW-Madison login.

Notable changes:
1. 'HTTP' is now assumed by default. This is the flag that can be passed to `Preauth` constructors for the 'HTTP' versions of attributes. Example: `wiscEduPVI` becomes `HTTP_WISCEDUPVI`.
2. `UserDetailsProvider` is an abstract class now, not an interface.
3. Constants used for header keys are much more consistent. Keys for UW-System login no longer have the `FED_` prefix and you will get the correct key for your particular implementation (`FederatedPreauth...` vs. `Preauth`) by using `static::<CONSTANT_NAME>`. *The only exception:* UWMSN and UW-System differ on their name for 'PVI'--UWMSN uses "PVI" and UW-System uses "SPVI", so `PVI` is only a constant in `PreauthUserDetailsProvider` while `FederatedPreauthUserDetailsProvider` has an `SPVI` constant.
4. *Namespace reorganization*. Rather than lumping everything into the `edu\wisc\doit` namespace, the namespace structure is now much  more like [uw-spring-security](https://git.doit.wisc.edu/adi-ia/uw-spring-security). General classes now exist in the `edu\wisc\doit\uwphps` namespace, with `local` and `preauth` namespaces existing for the local and preauth implementations. This change necessitated the bump to 2.0.0.

Please review: @ahoffmann @weizhong-wang @KJOYNER

See merge request !8
parents 661a01b1 1e198930
No related branches found
No related tags found
1 merge request!8Add UDDS attribute and refactor
Showing
with 266 additions and 174 deletions
......@@ -4,7 +4,7 @@
"type": "library",
"homepage": "https://git.doit.wisc.edu/adi-ia/uw-php-security",
"license": "Apache-2.0",
"version": "1.0.3",
"version": "2.0.0",
"authors": [{
"name": "UW-Madison DoIT ADI Integrated Applications",
"email": "adi-ia@lists.wisc.edu",
......@@ -19,7 +19,7 @@
},
"autoload": {
"psr-4": {
"edu\\wisc\\doit\\": "src/main/edu/wisc/doit/"
"edu\\wisc\\doit\\uwphps\\": "src/main/edu/wisc/doit/uwphps"
}
}
}
<?php
namespace edu\wisc\doit;
/**
* FederatedPreauthUserDetailsProvider provides an application with a {@link UWUserDetails} from Shibboleth attributes.
* The attributes use the keys as defined by UW System, which are distinct from those used specifically by UWMSN.
*/
class FederatedPreauthUserDetailsProvider implements UserDetailsProvider
{
/** @var bool */
private $httpHeaders;
/**
* FederatedPreauthUserDetailsProvider constructor.
*
* @param bool $http flag indicating if Shibboleth attributes are forwarded as HTTP headers
*/
public function __construct($http = false)
{
$this->httpHeaders = $http;
}
/**
* {@inheritdoc}
*/
public function loadUser()
{
// Return null if no Shib session is found
if ($this->httpHeaders && !getenv(FederatedPreauthUserDetailsProvider::SHIB_SESSION_ID_HTTP) ||
!$this->httpHeaders && !getenv(FederatedPreauthUserDetailsProvider::SHIB_SESSION_ID)) {
return null;
}
if ($this->httpHeaders) {
$userDetails = new UWUserDetails(
getenv($this->mapAttribute(UserDetailsProvider::FED_EPPN)),
getenv($this->mapAttribute(UserDetailsProvider::FED_SPVI)),
getenv($this->mapAttribute(UserDetailsProvider::FED_FULLNAME)),
getenv($this->mapAttribute(UserDetailsProvider::UDDS)),
getenv($this->mapAttribute(UserDetailsProvider::FED_EMAIL)),
getenv($this->mapAttribute(UserDetailsProvider::SOURCE)),
getenv($this->mapAttribute(UserDetailsProvider::ISIS_EMPLID)),
getenv($this->mapAttribute(UserDetailsProvider::FED_FIRST_NAME)),
getenv($this->mapAttribute(UserDetailsProvider::FED_LAST_NAME))
);
} else {
$userDetails = new UWUserDetails(
getenv(UserDetailsProvider::FED_EPPN),
getenv(UserDetailsProvider::FED_SPVI),
getenv(UserDetailsProvider::FED_FULLNAME),
getenv(UserDetailsProvider::UDDS),
getenv(UserDetailsProvider::FED_EMAIL),
getenv(UserDetailsProvider::SOURCE),
getenv(UserDetailsProvider::ISIS_EMPLID),
getenv(UserDetailsProvider::FED_FIRST_NAME),
getenv(UserDetailsProvider::FED_LAST_NAME)
);
}
return $userDetails;
}
/**
* Map a Shibboleth attribute to its associated HTTP header name.
*
* @param string $attribute attribute to map
* @return string Shibboleth attribute name mapped to its equivalent HTTP header name
*/
private function mapAttribute($attribute)
{
return 'HTTP_' . strtoupper($attribute);
}
}
<?php
namespace edu\wisc\doit;
/**
* TODO: Write documentation
*/
interface UserDetailsProvider
{
// Constants representing UW Federated login Shibboleth headers
const FED_EPPN = "eppn";
const FED_SPVI = "eduWisconsinSPVI";
const FED_FULLNAME = "eduWisconsinCommonName";
const FED_FIRST_NAME = "eduWisconsinGivenName";
const FED_LAST_NAME = "eduWisconsinSurname";
const FED_EMAIL = "eduWisconsinEmailAddress";
// Generic and/or UWMSN constants
const SHIB_SESSION_ID = 'Shib-Session-Id';
const SHIB_SESSION_ID_HTTP = 'HTTP_SHIB_SESSION_ID';
const UDDS = "udds";
const SOURCE = "source";
const ISIS_EMPLID = "isisEmplid";
/**
* Map Shibboleth header values to an associative array.
*
* @return UserDetails
*/
public function loadUser();
}
\ No newline at end of file
<?php
namespace edu\wisc\doit;
namespace edu\wisc\doit\uwphps;
/**
* Default, concrete implementation of {@link UserDetails} to represent a UW user.
......@@ -14,13 +14,13 @@ class UWUserDetails implements UserDetails
private $pvi;
/** @var string */
private $fullName;
/** @var string[] */
/** @var array */
private $udds;
/** @var string */
private $emailAddress;
/** @var string */
private $source;
/** @var isisEmplid */
/** @var string */
private $isisEmplid;
/** @var string */
private $firstName;
......@@ -32,15 +32,24 @@ class UWUserDetails implements UserDetails
* @param string $eppn
* @param string $pvi
* @param string $fullName
* @param string[] $udds
* @param array $udds
* @param string $emailAddress
* @param string $source
* @param string $isisEmplid
* @param string $firstName
* @param string $lastName
*/
public function __construct($eppn, $pvi, $fullName, $udds, $emailAddress, $source, $isisEmplid, $firstName, $lastName)
{
public function __construct(
$eppn,
$pvi,
$fullName,
$udds,
$emailAddress,
$source,
$isisEmplid,
$firstName,
$lastName
) {
$this->eppn = $eppn;
$this->pvi = $pvi;
$this->fullName = $fullName;
......
<?php
namespace edu\wisc\doit;
namespace edu\wisc\doit\uwphps;
/**
* UserDetails defines a minimal representation of a user associated with UW.
......@@ -34,7 +34,7 @@ interface UserDetails
/**
* A never null, but possibly empty, array of strings each representing the UDDS IDs of the group this user is a member of.
*
* @return string[]
* @return array
*/
public function getUddsMembership();
......@@ -74,4 +74,4 @@ interface UserDetails
*/
public function getLastName();
}
\ No newline at end of file
}
<?php
namespace edu\wisc\doit\uwphps;
/**
* UseDetailsProvider is an interface defining how to load an {@link UserDetails} and provides constants for
* supplied request header keys.
*/
abstract class UserDetailsProvider
{
/** @var bool Flag indicating if headers are passed prefixed with 'HTTP_' */
protected $httpHeaders;
// General, shared constants relevant to UWMSN and UW-System login
const EPPN = "eppn";
const SHIB_SESSION_ID = 'Shib-Session-Id';
const SHIB_SESSION_ID_HTTP = 'HTTP_SHIB_SESSION_ID';
const UDDS = "udds";
const SOURCE = "source";
const ISIS_EMPLID = "isisEmplid";
// Delimiter used by multi-valued headers
const DELIMITER = ';';
/**
* UserDetailsProvider constructor.
* @param $http
*/
public function __construct($http = false)
{
$this->httpHeaders = $http;
}
/**
* Map Shibboleth header values to an associative array.
*
* @return UserDetails
*/
abstract public function loadUser();
/**
* Map a Shibboleth attribute to its associated HTTP header name.
*
* @param string $attribute attribute to map
* @return string Shibboleth attribute name mapped to its equivalent HTTP header name
*/
protected function mapAttribute($attribute)
{
return 'HTTP_' . strtoupper($attribute);
}
}
<?php
namespace edu\wisc\doit;
namespace edu\wisc\doit\uwphps\local;
use edu\wisc\doit\uwphps\preauth\PreauthUserDetailsProvider;
use edu\wisc\doit\uwphps\UWUserDetails;
/**
* LocalUserDetailsProvider provides a developer with a {@link UWUserDetails} suitable for use in local development.
*/
class LocalUserDetailsProvider implements UserDetailsProvider
class LocalUserDetailsProvider extends PreauthUserDetailsProvider
{
/** @var string */
......@@ -14,10 +17,11 @@ class LocalUserDetailsProvider implements UserDetailsProvider
/**
* LocalUserDetailsProvider constructor.
*
* @param $filePath path to JSON file defining a local user.
* @param $filePath string Path to JSON file defining a local user.
*/
public function __construct($filePath)
{
parent::__construct(false);
$this->filePath = $filePath;
}
......@@ -34,15 +38,15 @@ class LocalUserDetailsProvider implements UserDetailsProvider
$attributes = json_decode($jsonString, true);
return new UWUserDetails(
$attributes[UserDetailsProvider::FED_EPPN],
$attributes[UserDetailsProvider::FED_SPVI],
$attributes[UserDetailsProvider::FED_FULLNAME],
$attributes[UserDetailsProvider::UDDS],
$attributes[UserDetailsProvider::FED_EMAIL],
$attributes[UserDetailsProvider::SOURCE],
$attributes[UserDetailsProvider::ISIS_EMPLID],
$attributes[UserDetailsProvider::FED_FIRST_NAME],
$attributes[UserDetailsProvider::FED_LAST_NAME]
$attributes[static::EPPN],
$attributes[static::PVI],
$attributes[static::FULL_NAME],
explode(static::DELIMITER, $attributes[static::UDDS]),
$attributes[static::EMAIL],
$attributes[static::SOURCE],
$attributes[static::ISIS_EMPLID],
$attributes[static::FIRST_NAME],
$attributes[static::LAST_NAME]
);
}
......
<?php
namespace edu\wisc\doit\uwphps\preauth;
use edu\wisc\doit\uwphps\UserDetailsProvider;
use edu\wisc\doit\uwphps\UWUserDetails;
/**
* FederatedPreauthUserDetailsProvider is an implementation of {@link UserDetailsProvider} for loading users
* authenticated with UW-System Federated login.
*
* {@see PreauthUserDetailsProvider} for loading users authenticated through UW-Madison login.
*/
class FederatedPreauthUserDetailsProvider extends UserDetailsProvider
{
// Constants defining common header values
const SPVI = "eduWisconsinSPVI";
const EMAIL = "eduWisconsinEmailAddress";
const FULL_NAME = "eduWisconsinCommonName";
const FIRST_NAME = "eduWisconsinGivenName";
const LAST_NAME = "eduWisconsinSurname";
/**
* {@inheritdoc}
*/
public function loadUser()
{
// Return null if no Shib session is found
if ($this->httpHeaders && !getenv(static::SHIB_SESSION_ID_HTTP) ||
!$this->httpHeaders && !getenv(static::SHIB_SESSION_ID)) {
return null;
}
if ($this->httpHeaders) {
$userDetails = new UWUserDetails(
getenv($this->mapAttribute(static::EPPN)),
getenv($this->mapAttribute(static::SPVI)),
getenv($this->mapAttribute(static::FULL_NAME)),
explode(static::DELIMITER, getenv($this->mapAttribute(static::UDDS))),
getenv($this->mapAttribute(static::EMAIL)),
getenv($this->mapAttribute(static::SOURCE)),
getenv($this->mapAttribute(static::ISIS_EMPLID)),
getenv($this->mapAttribute(static::FIRST_NAME)),
getenv($this->mapAttribute(static::LAST_NAME))
);
} else {
$userDetails = new UWUserDetails(
getenv(static::EPPN),
getenv(static::SPVI),
getenv(static::FULL_NAME),
getenv(static::UDDS),
getenv(static::EMAIL),
getenv(static::SOURCE),
getenv(static::ISIS_EMPLID),
getenv(static::FIRST_NAME),
getenv(static::LAST_NAME)
);
}
return $userDetails;
}
}
<?php
namespace edu\wisc\doit\uwphps\preauth;
use edu\wisc\doit\uwphps\UserDetailsProvider;
use edu\wisc\doit\uwphps\UWUserDetails;
/**
* PreauthUserDetailsProvider is an implementation of {@link UserDetailsProvider} for loading users authenticated
* with UW-Madison login.
*
* {@see FederatedPreauthUserDetailsProvider} for loading users authenticated through UW-System Federated login.
*/
class PreauthUserDetailsProvider extends UserDetailsProvider
{
// Constants defining common header values
const PVI = 'wiscEduPVI';
const FULL_NAME = 'cn';
const EMAIL = 'mail';
const UDDS = 'wiscEduUDDS';
const ISIS_EMPLID = 'wiscEduIsisEmplid';
const FIRST_NAME = 'givenName';
const LAST_NAME = 'sn';
/**
* {@inheritdoc}
*/
public function loadUser()
{
// Return null if no Shib session is found
if ($this->httpHeaders && !getenv(static::SHIB_SESSION_ID_HTTP) ||
!$this->httpHeaders && !getenv(static::SHIB_SESSION_ID)) {
return null;
}
if ($this->httpHeaders) {
$userDetails = new UWUserDetails(
getenv($this->mapAttribute(static::EPPN)),
getenv($this->mapAttribute(static::PVI)),
getenv($this->mapAttribute(static::FULL_NAME)),
explode(static::DELIMITER, getenv($this->mapAttribute(static::UDDS))),
getenv($this->mapAttribute(static::EPPN)),
getenv($this->mapAttribute(static::SOURCE)),
getenv($this->mapAttribute(static::ISIS_EMPLID)),
getenv($this->mapAttribute(static::FIRST_NAME)),
getenv($this->mapAttribute(static::LAST_NAME))
);
} else {
$userDetails = new UWUserDetails(
getenv(static::EPPN),
getenv(static::PVI),
getenv(static::FULL_NAME),
getenv(static::UDDS),
getenv(static::EPPN),
getenv(static::SOURCE),
getenv(static::ISIS_EMPLID),
getenv(static::FIRST_NAME),
getenv(static::LAST_NAME)
);
}
return $userDetails;
}
}
{
"eppn": "bbadger@wisc.edu",
"eduWisconsinSPVI": "UW123A456",
"wiscEduPVI": "UW123A456",
"cn": "BUCKINGHAM BADGER",
"eduWisconsinCommonName": "BUCKINGHAM BADGER",
"eduWisconsinGivenName": "BUCKINGHAM",
"eduWisconsinSurname": "BADGER",
"udds": [
"UW123A456",
"UW234A567"
],
"eduWisconsinEmailAddress": "bucky.badger@wisc.edu",
"cn": "BUCKINGHAM BADGER",
"givenName": "BUCKINGHAM",
"sn": "BADGER",
"wiscEduUDDS": "A061234;A072345",
"mail": "bucky.badger@wisc.edu",
"source": "a_source",
"isisEmplid": "123456789"
}
\ No newline at end of file
<?php
use edu\wisc\doit\LocalUserDetailsProvider;
use edu\wisc\doit\uwphps\local\LocalUserDetailsProvider;
/**
* Tests for {@link LocalUserDetailsProvider}.
......@@ -8,9 +8,12 @@ use edu\wisc\doit\LocalUserDetailsProvider;
class LocalUserDetailsProviderTest extends \PHPUnit_Framework_TestCase
{
public function testLoadUser()
/**
* @test
*/
public function loadUser()
{
$userDetailsService = new LocalUserDetailsProvider(__DIR__ . "/../../../resources/localuser.json");
$userDetailsService = new LocalUserDetailsProvider(__DIR__ . "/../../../../../resources/localuser.json");
$user = $userDetailsService->loadUser();
$this->assertEquals("bbadger@wisc.edu", $user->getEppn());
$this->assertEquals("UW123A456", $user->getPvi());
......@@ -21,4 +24,5 @@ class LocalUserDetailsProviderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals("BUCKINGHAM", $user->getFirstName());
$this->assertEquals("BADGER", $user->getLastName());
}
}
<?php
use edu\wisc\doit\UserDetailsProvider;
use edu\wisc\doit\FederatedPreauthUserDetailsProvider;
use edu\wisc\doit\uwphps\UserDetailsProvider;
use edu\wisc\doit\uwphps\preauth\FederatedPreauthUserDetailsProvider;
/**
* Tests for {@link FederatedPreauthUserDetailsProvider}.
......@@ -16,12 +16,11 @@ class HTTPFederatedPreauthUserDetailsProviderTest extends \PHPUnit_Framework_Tes
private $userProvider;
/**
* Populate putenv with Shib attributes to simulate a logged in user
* Populate $_SERVER with Shib attributes to simulate a logged in user
*/
protected function setUp()
{
parent::setUp();
$jsonString = file_get_contents(__DIR__ . "/../../../resources/testuser_http.json");
$jsonString = file_get_contents(__DIR__ . "/../../../../../resources/testuser_http.json");
if ($jsonString === false) {
return null;
}
......@@ -29,19 +28,22 @@ class HTTPFederatedPreauthUserDetailsProviderTest extends \PHPUnit_Framework_Tes
$this->attributes = json_decode($jsonString, true);
$this->userProvider = new FederatedPreauthUserDetailsProvider(true);
putenv($this->mapAttribute(UserDetailsProvider::FED_EPPN) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::FED_EPPN)]);
putenv($this->mapAttribute(UserDetailsProvider::FED_SPVI) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::FED_SPVI)]);
putenv($this->mapAttribute(UserDetailsProvider::FED_FULLNAME) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::FED_FULLNAME)]);
putenv($this->mapAttribute(UserDetailsProvider::FED_FIRST_NAME) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::FED_FIRST_NAME)]);
putenv($this->mapAttribute(UserDetailsProvider::FED_LAST_NAME) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::FED_LAST_NAME)]);
putenv($this->mapAttribute(UserDetailsProvider::UDDS) . '=' . implode(",", $this->attributes[$this->mapAttribute(UserDetailsProvider::UDDS)]));
putenv($this->mapAttribute(UserDetailsProvider::FED_EMAIL) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::FED_EMAIL)]);
putenv($this->mapAttribute(UserDetailsProvider::EPPN) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::EPPN)]);
putenv($this->mapAttribute(FederatedPreauthUserDetailsProvider::SPVI) . '=' . $this->attributes[$this->mapAttribute(FederatedPreauthUserDetailsProvider::SPVI)]);
putenv($this->mapAttribute(FederatedPreauthUserDetailsProvider::FULL_NAME) . '=' . $this->attributes[$this->mapAttribute(FederatedPreauthUserDetailsProvider::FULL_NAME)]);
putenv($this->mapAttribute(FederatedPreauthUserDetailsProvider::FIRST_NAME) . '=' . $this->attributes[$this->mapAttribute(FederatedPreauthUserDetailsProvider::FIRST_NAME)]);
putenv($this->mapAttribute(FederatedPreauthUserDetailsProvider::LAST_NAME) . '=' . $this->attributes[$this->mapAttribute(FederatedPreauthUserDetailsProvider::LAST_NAME)]);
putenv($this->mapAttribute(UserDetailsProvider::UDDS) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::UDDS)]);
putenv($this->mapAttribute(FederatedPreauthUserDetailsProvider::EMAIL) . '=' . $this->attributes[$this->mapAttribute(FederatedPreauthUserDetailsProvider::EMAIL)]);
putenv($this->mapAttribute(UserDetailsProvider::SOURCE) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::SOURCE)]);
putenv($this->mapAttribute(UserDetailsProvider::ISIS_EMPLID) . '=' . $this->attributes[$this->mapAttribute(UserDetailsProvider::ISIS_EMPLID)]);
putenv(UserDetailsProvider::SHIB_SESSION_ID_HTTP . '=' . $this->attributes[UserDetailsProvider::SHIB_SESSION_ID_HTTP]);
}
public function testLoadUser()
/**
* @test
*/
public function loadUser()
{
$user = $this->userProvider->loadUser();
$this->assertNotNull($user);
......@@ -53,9 +55,13 @@ class HTTPFederatedPreauthUserDetailsProviderTest extends \PHPUnit_Framework_Tes
$this->assertEquals("123456789", $user->getIsisEmplid());
$this->assertEquals("BUCKINGHAM", $user->getFirstName());
$this->assertEquals("BADGER", $user->getLastName());
$this->assertEquals(["A061234", "A072345"], $user->getUddsMembership());
}
public function testLoadUserWithNoEPPN()
/**
* @test
*/
public function loadUserWithNoEPPN()
{
// Clear Shib session ID to simulate no session
putenv(UserDetailsProvider::SHIB_SESSION_ID_HTTP);
......@@ -63,10 +69,13 @@ class HTTPFederatedPreauthUserDetailsProviderTest extends \PHPUnit_Framework_Tes
$this->assertNull($user);
}
public function testLoadUserWithNoEmail()
/**
* @test
*/
public function loadUserWithNoEmail()
{
// Clear email to simulate no email
putenv('HTTP_' . strtoupper(UserDetailsProvider::FED_EMAIL));
putenv('HTTP_' . strtoupper(FederatedPreauthUserDetailsProvider::EMAIL));
$user = $this->userProvider->loadUser();
$this->assertFalse($user->getEmailAddress());
}
......
{
"eppn": "bbadger@wisc.edu",
"eduWisconsinSPVI": "UW123A456",
"wiscEduPVI": "UW123A456",
"cn": "BUCKINGHAM BADGER",
"eduWisconsinCommonName": "BUCKINGHAM BADGER",
"eduWisconsinGivenName": "BUCKINGHAM",
"eduWisconsinSurname": "BADGER",
"udds": [
"UW123A456",
"UW234A567"
],
"eduWisconsinEmailAddress": "bucky.badger@wisc.edu",
"cn": "BUCKINGHAM BADGER",
"givenName": "BUCKINGHAM",
"sn": "BADGER",
"wiscEduUDDS": "A061234;A072345",
"mail": "bucky.badger@wisc.edu",
"source": "a_source",
"isisEmplid": "123456789"
"wiscEduIsisEmplid": "123456789"
}
\ No newline at end of file
......@@ -5,10 +5,7 @@
"HTTP_EDUWISCONSINCOMMONNAME": "BUCKINGHAM BADGER",
"HTTP_EDUWISCONSINGIVENNAME": "BUCKINGHAM",
"HTTP_EDUWISCONSINSURNAME": "BADGER",
"HTTP_UDDS": [
"UW123A456",
"UW234A567"
],
"HTTP_UDDS": "A061234;A072345",
"HTTP_EDUWISCONSINEMAILADDRESS": "bucky.badger@wisc.edu",
"HTTP_SOURCE": "a_source",
"HTTP_ISISEMPLID": "123456789",
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment