Skip to content
Snippets Groups Projects
Commit 3f428903 authored by Andrew Hoffmann's avatar Andrew Hoffmann
Browse files

Merge branch 'provider-refactor' into 'master'

Refactor uw-php-security to use 'Provider' classes

After a discussion about the project's design and requirements moving
forward, it was decided to refactor the project to remove
`AttributeMapper` classes and have `Provider` classes handle attribute
mapping as well as user loading. This also adds a third provider,
`PreauthHTTPUserDetailsProvider`, which can be used in environments
where Shibboleth attributes are sent as HTTP headers.

Apologies for the big PR!

Please review: @ahoffmann 

See merge request !2
parents fdbef3b2 a5738b5a
No related branches found
No related tags found
1 merge request!2Refactor uw-php-security to use 'Provider' classes
Showing
with 246 additions and 280 deletions
......@@ -2,15 +2,15 @@
uw-php-security is a PHP companion to [uw-spring-security](https://git.doit.wisc.edu/adi-ia/uw-spring-security) for Java. Its purpose is to ease development of PHP applications needing details about UW users provided through Shibboleth.
Like uw-spring-security, uw-php-security provides a class called `UWUserDetails` for easily accessing common UW user attributes. This model is provided to applications through a `UserDetailsService`, and uw-php-security provides implementations suitable for both local and preauth (Shibboleth) environments.
Like uw-spring-security, uw-php-security provides a class called `UWUserDetails` for easily accessing common UW user attributes. This model is provided to applications through a `UserDetailsProvider`, and uw-php-security provides implementations suitable for both local and preauth (Shibboleth) environments.
```php
<?php
use edu\wisc\doit\PreauthUserDetailsService; // or LocalUserDetailsService for local development
use edu\wisc\doit\PreauthUserDetailsProvider; // or LocalUserDetailsProvider for local development
...
$userDetailsService = new PreauthUserDetailsService();
$user = $userDetailsService->loadUser();
$userDetailsProvider = new PreauthUserDetailsProvider();
$user = $userDetailsProvider->loadUser();
if ($user == null) {
// handle error
}
......
<?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 (empty(getenv(FederatedPreauthUserDetailsProvider::SHIB_SESSION_ID)) &&
empty(getenv(FederatedPreauthUserDetailsProvider::SHIB_SESSION_ID_HTTP))) {
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;
/**
* Implementation of {@link UserDetailsAttributeMapper} for use in local development.
*/
class LocalUserDetailsAttributeMapper implements UserDetailsAttributeMapper
{
/**
* {@inheritdoc}
*/
public function mapUser()
{
$jsonString = file_get_contents(__DIR__ . "/../../../resources/localuser.json");
if ($jsonString === false) {
return null;
}
// Return user attributes into a standard PHP array (true specifies array)
return json_decode($jsonString, true);
}
}
\ No newline at end of file
<?php
namespace edu\wisc\doit;
/**
* LocalUserDetailsProvider provides a developer with a {@link UWUserDetails} suitable for use in local development.
*/
class LocalUserDetailsProvider implements UserDetailsProvider
{
/** @var string */
private $filePath;
/**
* LocalUserDetailsProvider constructor.
*
* @param $filePath path to JSON file defining a local user.
*/
public function __construct($filePath)
{
$this->filePath = $filePath;
}
/**
* {@inheritdoc}
*/
public function loadUser()
{
$jsonString = file_get_contents($this->filePath);
if ($jsonString === false) {
return null;
}
$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]
);
}
/**
* Get the path of the local user JSON file.
*
* @return string
*/
public function getFilePath()
{
return $this->filePath;
}
/**
* Set the path of the local user JSON file.
* @param $filePath
*/
public function setFilePath($filePath)
{
$this->filePath = $filePath;
}
}
\ No newline at end of file
<?php
namespace edu\wisc\doit;
/**
* Implementation of {@link UserDetailsService} for use in local development.
*/
class LocalUserDetailsService implements UserDetailsService
{
/** @var UserDetailsAttributeMapper */
private $attributeMapper;
/**
* LocalUserDetailsService constructor.
* @param UserDetailsAttributeMapper $mapper
*/
public function __construct(UserDetailsAttributeMapper $mapper = null)
{
if ($mapper == null) {
$this->attributeMapper = new LocalUserDetailsAttributeMapper();
} else {
$this->attributeMapper = $mapper;
}
}
/**
* {@inheritdoc}
*/
public function loadUser()
{
$userAttributes = $this->attributeMapper->mapUser();
// Return null if attribute reading failed
if ($userAttributes == null) {
return null;
}
return new UWUserDetails(
$userAttributes[UserDetailsAttributeMapper::EPPN],
$userAttributes[UserDetailsAttributeMapper::PVI],
$userAttributes[UserDetailsAttributeMapper::FULLNAME],
$userAttributes[UserDetailsAttributeMapper::UDDS],
$userAttributes[UserDetailsAttributeMapper::EMAIL],
$userAttributes[UserDetailsAttributeMapper::SOURCE],
$userAttributes[UserDetailsAttributeMapper::ISIS_EMPLID],
$userAttributes[UserDetailsAttributeMapper::FIRST_NAME],
$userAttributes[UserDetailsAttributeMapper::LAST_NAME]
);
}
}
\ No newline at end of file
<?php
namespace edu\wisc\doit;
/**
* Default implementation of {@UserDetailsAttributeMapper} for use in preauthenticated (Shibboleth) environments.
*/
class PreauthUserDetailsAttributeMapper implements UserDetailsAttributeMapper
{
/**
* {@inheritdoc}
*/
public function mapUser()
{
$userAttributes[UserDetailsAttributeMapper::EPPN] = $_SERVER[UserDetailsAttributeMapper::EPPN];
$userAttributes[UserDetailsAttributeMapper::PVI] = $_SERVER[UserDetailsAttributeMapper::PVI];
$userAttributes[UserDetailsAttributeMapper::FULLNAME] = $_SERVER[UserDetailsAttributeMapper::FULLNAME];
$userAttributes[UserDetailsAttributeMapper::FIRST_NAME] = $_SERVER[UserDetailsAttributeMapper::FIRST_NAME];
$userAttributes[UserDetailsAttributeMapper::LAST_NAME] = $_SERVER[UserDetailsAttributeMapper::LAST_NAME];
$userAttributes[UserDetailsAttributeMapper::EMAIL] = $_SERVER[UserDetailsAttributeMapper::EMAIL];
$userAttributes[UserDetailsAttributeMapper::UDDS] = $_SERVER[UserDetailsAttributeMapper::UDDS];
$userAttributes[UserDetailsAttributeMapper::SOURCE] = $_SERVER[UserDetailsAttributeMapper::SOURCE];
$userAttributes[UserDetailsAttributeMapper::ISIS_EMPLID] = $_SERVER[UserDetailsAttributeMapper::ISIS_EMPLID];
// Require EPPN, PVI and FULLNAME to be set to consider user loading successful
if (empty($userAttributes[UserDetailsAttributeMapper::EPPN]) ||
empty($userAttributes[UserDetailsAttributeMapper::PVI]) ||
empty($userAttributes[UserDetailsAttributeMapper::FULLNAME])) {
return null;
}
return $userAttributes;
}
}
<?php
namespace edu\wisc\doit;
/**
* Default implementation of {@UserDetailsAttributeMapper} for use in preauthenticated (Shibboleth) environments.
*/
class PreauthUserDetailsService implements UserDetailsService
{
/** @var UserDetailsAttributeMapper */
private $attributeMapper;
/**
* PreauthUserDetailsService constructor.
* @param UserDetailsAttributeMapper|null $mapper
*/
public function __construct(UserDetailsAttributeMapper $mapper = null)
{
if ($mapper == null) {
$this->attributeMapper = new PreauthUserDetailsAttributeMapper();
} else {
$this->attributeMapper = $mapper;
}
}
/**
* {@inheritdoc}
*/
public function loadUser()
{
$userAttributes = $this->attributeMapper->mapUser();
// Return null if attribute reading failed
if ($userAttributes == null) {
return null;
}
return new UWUserDetails(
$userAttributes[UserDetailsAttributeMapper::EPPN],
$userAttributes[UserDetailsAttributeMapper::PVI],
$userAttributes[UserDetailsAttributeMapper::FULLNAME],
$userAttributes[UserDetailsAttributeMapper::UDDS],
$userAttributes[UserDetailsAttributeMapper::EMAIL],
$userAttributes[UserDetailsAttributeMapper::SOURCE],
$userAttributes[UserDetailsAttributeMapper::ISIS_EMPLID],
$userAttributes[UserDetailsAttributeMapper::FIRST_NAME],
$userAttributes[UserDetailsAttributeMapper::LAST_NAME]
);
}
}
\ No newline at end of file
......@@ -3,19 +3,22 @@
namespace edu\wisc\doit;
/**
* UserDetailsAttributeMapper defines an interface for mapping common UW user attributes to an associative array. The
* constants defined in this interface represent headers sent by UW Federated login that identify a user.
* TODO: Write documentation
*/
interface UserDetailsAttributeMapper
interface UserDetailsProvider
{
// Constants representing UW Federated login Shibboleth headers which should be mapped by concrete implementations.
const EPPN = "eppn";
const PVI = "eduWisconsinSPVI";
const FULLNAME = "eduWisconsinCommonName";
const FIRST_NAME = "eduWisconsinGivenName";
const LAST_NAME = "eduWisconsinSurname";
const EMAIL = "eduWisconsinEmailAddress";
// 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";
......@@ -23,8 +26,8 @@ interface UserDetailsAttributeMapper
/**
* Map Shibboleth header values to an associative array.
*
* @return array
* @return UserDetails
*/
public function mapUser();
public function loadUser();
}
\ No newline at end of file
<?php
namespace edu\wisc\doit;
/**
* UserDetailsService is the interface that provides an instance of {@link UserDetails}, typically a {@link UWUserDetails}.
* Two concrete implementations are provided, {@link LocalUserDetailsService} and {@link PreauthUserDetailsService}.
*/
interface UserDetailsService
{
/**
* Return a {@link UserDetails} hydrated by a {@link UserDetailsAttributeMapper}, or null if attribute
* mapping failed.
*
* @return UserDetails|null
*/
public function loadUser();
}
\ No newline at end of file
......@@ -3,22 +3,41 @@
namespace edu\wisc\doit;
/**
* Tests for {@link PreauthUserDetailsService}.
* Tests for {@link FederatedPreauthUserDetailsProvider}.
*/
class PreauthUserDetailsServiceTest extends PreauthTestCase
class FederatedPreauthUserDetailsProviderTest extends \PHPUnit_Framework_TestCase
{
/** @var UserDetailsService */
private $userService;
/** @var UserDetailsProvider */
private $userProvider;
/**
* Populate putenv with Shib attributes to simulate a logged in user
*/
protected function setUp()
{
parent::setUp();
$this->userService = new PreauthUserDetailsService();
$jsonString = file_get_contents(__DIR__ . "/../../../resources/testuser.json");
if ($jsonString === false) {
return null;
}
$attributes = json_decode($jsonString, true);
putenv(UserDetailsProvider::FED_EPPN . '=' . $attributes[UserDetailsProvider::FED_EPPN]);
putenv(UserDetailsProvider::FED_SPVI . '=' . $attributes[UserDetailsProvider::FED_SPVI]);
putenv(UserDetailsProvider::FED_FULLNAME . '=' . $attributes[UserDetailsProvider::FED_FULLNAME]);
putenv(UserDetailsProvider::FED_FIRST_NAME . '=' . $attributes[UserDetailsProvider::FED_FIRST_NAME]);
putenv(UserDetailsProvider::FED_LAST_NAME . '=' . $attributes[UserDetailsProvider::FED_LAST_NAME]);
putenv(UserDetailsProvider::UDDS . '=' . implode(",", $attributes[UserDetailsProvider::UDDS]));
putenv(UserDetailsProvider::FED_EMAIL . '=' . $attributes[UserDetailsProvider::FED_EMAIL]);
putenv(UserDetailsProvider::SOURCE . '=' . $attributes[UserDetailsProvider::SOURCE]);
putenv(UserDetailsProvider::ISIS_EMPLID . '=' . $attributes[UserDetailsProvider::ISIS_EMPLID]);
putenv(UserDetailsProvider::SHIB_SESSION_ID . '=' . $attributes[UserDetailsProvider::SHIB_SESSION_ID]);
}
public function testLoadUser() {
$user = $this->userService->loadUser();
$this->userProvider = new FederatedPreauthUserDetailsProvider();
$user = $this->userProvider->loadUser();
$this->assertNotNull($user);
$this->assertEquals("bbadger@wisc.edu", $user->getEppn());
$this->assertEquals("UW123A456", $user->getPvi());
......@@ -31,9 +50,10 @@ class PreauthUserDetailsServiceTest extends PreauthTestCase
}
public function testLoadUserWithNoEPPN() {
// Clear EPPN to simulate no EPPN
$_SERVER[UserDetailsAttributeMapper::EPPN] = null;
$user = $this->userService->loadUser();
$this->userProvider = new FederatedPreauthUserDetailsProvider();
// Clear Shib session ID to simulate no session
putenv(UserDetailsProvider::SHIB_SESSION_ID);
$user = $this->userProvider->loadUser();
$this->assertNull($user);
}
......
<?php
namespace edu\wisc\doit;
/**
* Tests for {@link LocalUserDetailsAttributeMapper}
*/
class LocalUserDetailsAttributeMapperTest extends \PHPUnit_Framework_TestCase
{
/**
* Test attribute mapping for local development.
*/
public function testMapLocalUser() {
$attributeMapper = new LocalUserDetailsAttributeMapper();
$userAttributes = $attributeMapper->mapUser();
$this->assertEquals("bbadger@wisc.edu", $userAttributes[UserDetailsAttributeMapper::EPPN]);
$this->assertEquals("UW123A456", $userAttributes[UserDetailsAttributeMapper::PVI]);
$this->assertEquals("BUCKINGHAM BADGER", $userAttributes[UserDetailsAttributeMapper::FULLNAME]);
$this->assertEquals("bucky.badger@wisc.edu", $userAttributes[UserDetailsAttributeMapper::EMAIL]);
$this->assertEquals("a_source", $userAttributes[UserDetailsAttributeMapper::SOURCE]);
$this->assertEquals("123456789", $userAttributes[UserDetailsAttributeMapper::ISIS_EMPLID]);
$this->assertEquals("BUCKINGHAM", $userAttributes[UserDetailsAttributeMapper::FIRST_NAME]);
$this->assertEquals("BADGER", $userAttributes[UserDetailsAttributeMapper::LAST_NAME]);
$this->assertEquals(["UW123A456", "UW234A567"], $userAttributes[UserDetailsAttributeMapper::UDDS]);
}
}
......@@ -3,14 +3,14 @@
namespace edu\wisc\doit;
/**
* Tests for {@link LocalUserDetailsService}.
* Tests for {@link LocalUserDetailsProvider}.
*/
class LocalUserDetailsServiceTest extends \PHPUnit_Framework_TestCase
class LocalUserDetailsProviderTest extends \PHPUnit_Framework_TestCase
{
public function testLoadUser()
{
$userDetailsService = new LocalUserDetailsService();
$userDetailsService = new LocalUserDetailsProvider(__DIR__ . "/../../../resources/testuser.json");
$user = $userDetailsService->loadUser();
$this->assertEquals("bbadger@wisc.edu", $user->getEppn());
$this->assertEquals("UW123A456", $user->getPvi());
......
<?php
namespace edu\wisc\doit;
/**
* Class to do basic setup needed to simulate a logged in Shibboleth user.
*/
abstract class PreauthTestCase extends \PHPUnit_Framework_TestCase
{
/**
* Populate $_SERVER with Shib attributes to simulate a logged in user
*/
protected function setUp()
{
parent::setUp();
$jsonString = file_get_contents(__DIR__ . "/../../../resources/testuser.json");
if ($jsonString === false) {
return null;
}
$attributes = json_decode($jsonString, true);
$_SERVER[UserDetailsAttributeMapper::EPPN] = $attributes[UserDetailsAttributeMapper::EPPN];
$_SERVER[UserDetailsAttributeMapper::PVI] = $attributes[UserDetailsAttributeMapper::PVI];
$_SERVER[UserDetailsAttributeMapper::FULLNAME] = $attributes[UserDetailsAttributeMapper::FULLNAME];
$_SERVER[UserDetailsAttributeMapper::FIRST_NAME] = $attributes[UserDetailsAttributeMapper::FIRST_NAME];
$_SERVER[UserDetailsAttributeMapper::LAST_NAME] = $attributes[UserDetailsAttributeMapper::LAST_NAME];
$_SERVER[UserDetailsAttributeMapper::UDDS] = $attributes[UserDetailsAttributeMapper::UDDS];
$_SERVER[UserDetailsAttributeMapper::EMAIL] = $attributes[UserDetailsAttributeMapper::EMAIL];
$_SERVER[UserDetailsAttributeMapper::SOURCE] = $attributes[UserDetailsAttributeMapper::SOURCE];
$_SERVER[UserDetailsAttributeMapper::ISIS_EMPLID] = $attributes[UserDetailsAttributeMapper::ISIS_EMPLID];
}
}
\ No newline at end of file
<?php
namespace edu\wisc\doit;
/**
* Tests for {@link PreauthUserDetailsAttributeMapper}.
*/
class PreauthUserDetailsAttributeMapperTest extends PreauthTestCase
{
public function testMapUser() {
$attributeMapper = new PreauthUserDetailsAttributeMapper();
$userAttributes = $attributeMapper->mapUser();
$this->assertEquals("bbadger@wisc.edu", $userAttributes[UserDetailsAttributeMapper::EPPN]);
$this->assertEquals("UW123A456", $userAttributes[UserDetailsAttributeMapper::PVI]);
$this->assertEquals("BUCKINGHAM BADGER", $userAttributes[UserDetailsAttributeMapper::FULLNAME]);
$this->assertEquals("bucky.badger@wisc.edu", $userAttributes[UserDetailsAttributeMapper::EMAIL]);
$this->assertEquals("a_source", $userAttributes[UserDetailsAttributeMapper::SOURCE]);
$this->assertEquals("123456789", $userAttributes[UserDetailsAttributeMapper::ISIS_EMPLID]);
$this->assertEquals("BUCKINGHAM", $userAttributes[UserDetailsAttributeMapper::FIRST_NAME]);
$this->assertEquals("BADGER", $userAttributes[UserDetailsAttributeMapper::LAST_NAME]);
$this->assertEquals(["UW123A456", "UW234A567"], $userAttributes[UserDetailsAttributeMapper::UDDS]);
}
}
......@@ -11,5 +11,6 @@
],
"eduWisconsinEmailAddress": "bucky.badger@wisc.edu",
"source": "a_source",
"isisEmplid": "123456789"
"isisEmplid": "123456789",
"Shib-Session-Id": "1234567890"
}
\ No newline at end of file
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