Story #1104: As a user I can authenticate through Identity Services.

This commit is contained in:
Matt Butcher
2012-01-24 20:19:51 -06:00
parent 40440b3bb9
commit 99ea808e33
2 changed files with 302 additions and 18 deletions

View File

@@ -13,7 +13,8 @@ namespace HPCloud\Services;
*
* - Authenticate
* - Obtain tokens valid accross services
* - Obtain a list of the account's current services (i.e. the Service Catalog)
* - Obtain a list of the services currently available with a token
* - Associate with tenants using tenant IDs.
*
* AUTHENTICATION
*
@@ -28,20 +29,89 @@ namespace HPCloud\Services;
* - Account ID and Secret Key
*
* Other mechanisms may be supported in the future.
*
* TENANTS
*
* Services are associated with tenants. A token is returned when
* authentication succeeds. It *may* be associated with a tenant. If it is not,
* it is called "unscoped", and it will not have access to any services.
*
* A token that is associated with a tenant is considered "scoped". This token
* can be used to access any of the services attached to that tenant.
*
* There are two different ways to attach a tenant to a token:
*
* - During authentication, provide a tenant ID. This will attach a tenant at
* the outset.
* - After authentication, "rescope" the token to attach it to a tenant. This
* is done with the rescope() method.
*
* <b>Where do I get a tenant ID?</b>
*
* There are two notable places to get this information:
*
* A list of tenants associated with this account can be obtain programatically
* using the tenants() method on this object.
*
* HPCloud customers can find their tenant ID in the management console along
* with their account ID and secret key.
*
* EXAMPLE
*
* The following example illustrates typical use of this class.
*
* @code
* <?php
* // You may need to use \HPCloud\Bootstrap to set things up first.
*
* use \HPCloud\Services\IdentityServices;
*
* // Create a new object with the endpoint URL (no version number)
* $ident = new IdentityServices('https://example.com:35357');
*
* // Authenticate and set the tenant ID simultaneously.
* $ident->authenticateAsUser('butcher@hp.com', 'password', '1234567');
*
* // The token to use when connecting to other services:
* $token = $ident->token();
*
* // The tenant ID.
* $tenant = $ident->tenantId();
*
* // Details about what services this token can access.
* $services = $ident->serviceCatalog();
*
* // List all available tenants.
* $tenants = $ident->tenants();
*
* // Switch to a different tenant.
* $ident->rescope($tenants[0]['id']);
*
* ?>
* @endcode
*
* PERFORMANCE CONSIDERATIONS
*
* The following methods require network requests:
*
* - authenticate()
* - authenticateAsUser()
* - authenticateAsAccount()
* - tenants()
* - rescope()
*
*
*/
class IdentityServices {
/**
* The version of the API currently supported.
*
* This must match the IdentityServices::ACCEPT_TYPE.
*/
const API_VERSION = '2.0';
/**
* The full OpenStack accept type.
*
* This must match the IdentityServices::API_VERSION.
*/
const ACCEPT_TYPE = 'application/json';
// This is no longer supported.
//const ACCEPT_TYPE = 'application/vnd.openstack.identity+json;version=2.0';
/**
@@ -151,6 +221,11 @@ class IdentityServices {
* The token. This is returned for simplicity. The full response is used
* to populate this object's service catalog, etc. The token is also
* retrievable with token().
* @throws \HPCloud\Transport\AuthorizationException
* If authentication failed.
* @throws \HPCloud\Exception
* For abnormal network conditions. The message will give an indication as
* to the underlying problem.
*/
public function authenticate(array $ops) {
$url = $this->url() . '/tokens';
@@ -182,9 +257,20 @@ class IdentityServices {
/**
* Authenticate to Identity Services with username, password, and tenant ID.
*
* Given an HPCloud username and password, and also the account's tenant ID,
* authenticate to Identity Services. Identity Services will then issue a token
* that can be used to access other HPCloud services.
* Given an HPCloud username and password, authenticate to Identity Services.
* Identity Services will then issue a token that can be used to access other
* HPCloud services.
*
* If a tenant ID is provided, this will also associate the user with the
* given tenant ID.
*
* If no tenant ID is given, it will likely be necessary to rescope() the
* request (See also tenants()).
*
* Other authentication methods:
*
* - authenticateAsAccount()
* - authenticate()
*
* @param string $username
* A valid username.
@@ -193,15 +279,24 @@ class IdentityServices {
* @param string $tenantId
* The tenant ID for this account. This can be obtained through the
* HPCloud management console.
* @throws \HPCloud\Transport\AuthorizationException
* If authentication failed.
* @throws \HPCloud\Exception
* For abnormal network conditions. The message will give an indication as
* to the underlying problem.
*/
public function authenticateAsUser($username, $password, $tenantId) {
public function authenticateAsUser($username, $password, $tenantId = NULL) {
$ops = array(
'passwordCredentials' => array(
'username' => $username,
'password' => $password,
),
'tenantId' => $tenantId,
);
// If a tenant ID is provided, added it to the auth array.
if (!empty($tenantId)) {
$ops['tenantId'] = $tenantId;
}
return $this->authenticate($ops);
}
/**
@@ -214,22 +309,44 @@ class IdentityServices {
* The account ID and access key information can be found in the account
* section of the management console.
*
* The third paramater allows you to specify a tenant ID. In order to access
* services, this object will need a tenant ID. If none is specified, it can
* be set later using rescope(). The tenants() method can be used to get a
* list of all available tenant IDs for this token.
*
* Other authentication methods:
*
* - authenticateAsUser()
* - authenticate()
*
* @param string $account
* The account ID. It should look something like this:
* 1234567890:abcdef123456.
* @param string $key
* The access key (i.e. secret key), which should be a series of
* ASCII letters and digits.
* @param string $tenantId
* A valid tenant ID. This will be used to associate a tenant's services
* with this token.
* @return string
* The auth token.
* @throws \HPCloud\Transport\AuthorizationException
* If authentication failed.
* @throws \HPCloud\Exception
* For abnormal network conditions. The message will give an indication as
* to the underlying problem.
*/
public function authenticateAsAccount($account, $key) {
public function authenticateAsAccount($account, $key, $tenantId = NULL) {
$ops = array(
'apiAccessKeyCredentials' => array(
'accessKey' => $account,
'secretKey' => $key,
),
);
if (!empty($tenantId)) {
$ops['tenantId'] = $tenantId;
}
return $this->authenticate($ops);
}
@@ -247,6 +364,24 @@ class IdentityServices {
return $this->tokenDetails['id'];
}
/**
* Get the tenant ID associated with this token.
*
* If this token has a tenant ID, the ID will be returned. Otherwise, this
* will return NULL.
*
* This will not be populated until after an authentication method has been
* run.
*
* @return string
* The tenant ID if available, or NULL.
*/
public function tenantId() {
if (!empty($this->tokenDetails['tenant']['id'])) {
return $this->tokenDetails['tenant']['id'];
}
}
/**
* Get the token details.
*
@@ -268,6 +403,8 @@ class IdentityServices {
* );
* @endcode
*
* This will not be populated until after authentication has been done.
*
* @returns array
* An associative array of details.
*/
@@ -322,6 +459,8 @@ class IdentityServices {
* ?>
* @endcode
*
* This will not be populated until after authentication has been done.
*
* @todo Paging on the service catalog is not yet implemented.
*
* @return array
@@ -356,6 +495,8 @@ class IdentityServices {
* ?>
* @endcode
*
* This will not have data until after authentication has been done.
*
* @return array
* An associative array, as described above.
*/
@@ -392,6 +533,11 @@ class IdentityServices {
* @return array
* An indexed array of tenant info. Each entry will be an associative
* array containing tenant details.
* @throws \HPCloud\Transport\AuthorizationException
* If authentication failed.
* @throws \HPCloud\Exception
* For abnormal network conditions. The message will give an indication as
* to the underlying problem.
*/
public function tenants($token = NULL) {
$url = $this->url() . '/tenants';
@@ -416,6 +562,63 @@ class IdentityServices {
}
/**
* Rescope the authentication token to a different tenant.
*
* Note that this will rebuild the service catalog and user information for
* the current object, since this information is sensitive to tenant info.
*
* An authentication token can be in one of two states:
*
* - unscoped: It has no associated tenant ID.
* - scoped: It has a tenant ID, and can thus access that tenant's services.
*
* This method allows you to do any of the following:
*
* - Begin with an unscoped token, and assign it a tenant ID.
* - Change a token from one tenant ID to another (re-scoping).
* - Remove the tenant ID from a scoped token (unscoping).
*
* @param string $tenantId
* The tenant ID that this present token should be bound to. If this is the
* empty string (`''`), the present token will be "unscoped" and its tenant
* ID will be removed.
*
* @return string
* The authentication token.
* @throws \HPCloud\Transport\AuthorizationException
* If authentication failed.
* @throws \HPCloud\Exception
* For abnormal network conditions. The message will give an indication as
* to the underlying problem.
*/
public function rescope($tenantId) {
$url = $this->url() . '/tokens';
$token = $this->token();
$data = array(
'auth' => array(
'tenantId' => $tenantId,
'token' => array(
'id' => $token,
),
),
);
$body = json_encode($data);
$headers = array(
'Accept' => self::ACCEPT_TYPE,
'Content-Type' => 'application/json',
'Content-Length' => strlen($body),
//'X-Auth-Token' => $token,
);
$client = \HPCloud\Transport::instance();
$response = $client->doRequest($url, 'POST', $headers, $body);
$this->handleResponse($response);
return $this->token();
}
/**
* Given a response object, populate this object.
*
@@ -432,7 +635,6 @@ class IdentityServices {
$this->tokenDetails = $json['access']['token'];
$this->userDetails = $json['access']['user'];
$this->serviceCatalog = $json['access']['serviceCatalog'];
}
}

View File

@@ -70,6 +70,17 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase {
$tok2 = $service->authenticate($auth);
$this->assertEquals($tok, $tok2);
// Again with no tenant ID.
$auth = array(
'passwordCredentials' => array(
'username' => self::conf('hpcloud.identity.username'),
'password' => self::conf('hpcloud.identity.password'),
),
//'tenantId' => self::conf('hpcloud.identity.tenantId'),
);
$tok = $service->authenticate($auth);
$this->assertNotEmpty($tok);
// Test account ID/secret key auth.
$auth = array(
@@ -98,6 +109,13 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase {
$tok = $service->authenticateAsUser($user, $pass, $tenantId);
$this->assertNotEmpty($tok);
// Try again, this time with no tenant ID.
$tok2 = $service->authenticateAsUser($user, $pass);
$this->assertNotEmpty($tok2);
$details = $service->tokenDetails();
$this->assertEmpty($details['tenant']);
}
/**
@@ -108,9 +126,18 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase {
$account = self::conf('hpcloud.identity.account');
$secret = self::conf('hpcloud.identity.secret');
$tenantId = self::conf('hpcloud.identity.tenantId');
// No tenant ID.
$tok = $service->authenticateAsAccount($account, $secret);
$this->assertNotEmpty($tok);
$this->assertEmpty($service->tenantId());
// No tenant ID.
$service = new IdentityServices(self::conf('hpcloud.identity.url'));
$tok = $service->authenticateAsAccount($account, $secret, $tenantId);
$this->assertNotEmpty($tok);
$this->assertEquals($tenantId, $service->tenantId());
return $service;
}
@@ -125,8 +152,33 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase {
/**
* @depends testAuthenticateAsAccount
*/
public function testTokenDetails($service) {
public function testTenantId() {
$user = self::conf('hpcloud.identity.username');
$pass = self::conf('hpcloud.identity.password');
$tenantId = self::conf('hpcloud.identity.tenantId');
$service = new IdentityServices(self::conf('hpcloud.identity.url'));
$this->assertNull($service->tenantId());
$service->authenticateAsUser($user, $pass);
$this->assertEmpty($service->tenantId());
$service = new IdentityServices(self::conf('hpcloud.identity.url'));
$service->authenticateAsUser($user, $pass, $tenantId);
$this->assertNotEmpty($service->tenantId());
}
/**
* @depends testAuthenticateAsAccount
*/
public function testTokenDetails() {
$now = time();
$user = self::conf('hpcloud.identity.username');
$pass = self::conf('hpcloud.identity.password');
$tenantId = self::conf('hpcloud.identity.tenantId');
$service = new IdentityServices(self::conf('hpcloud.identity.url'));
$service->authenticateAsUser($user, $pass);
// Details for account auth.
$details = $service->tokenDetails();
@@ -139,9 +191,6 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase {
// Test details for username auth.
$service = new IdentityServices(self::conf('hpcloud.identity.url'));
$user = self::conf('hpcloud.identity.username');
$pass = self::conf('hpcloud.identity.password');
$tenantId = self::conf('hpcloud.identity.tenantId');
$service->authenticateAsUser($user, $pass, $tenantId);
$details = $service->tokenDetails();
@@ -152,6 +201,8 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase {
$this->assertNotEmpty($details['id']);
$this->assertNotEmpty($details['tenant']['id']);
$this->assertEquals($tenantId, $details['tenant']['id']);
$ts = strtotime($details['expires']);
$this->assertGreaterThan($now, $ts);
}
@@ -217,7 +268,38 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase {
* @depends testTenants
*/
function testRescope() {
$this->markTestIncomplete();
}
$service = new IdentityServices(self::conf('hpcloud.identity.url'));
$user = self::conf('hpcloud.identity.username');
$pass = self::conf('hpcloud.identity.password');
$tenantId = self::conf('hpcloud.identity.tenantId');
// Authenticate without a tenant ID.
$token = $service->authenticateAsUser($user, $pass);
$this->assertNotEmpty($token);
$details = $service->tokenDetails();
$this->assertEmpty($details['tenant']);
// With no tenant ID, there should be only
// one entry in the catalog.
$catalog = $service->serviceCatalog();
$this->assertEquals(1, count($catalog));
$service->rescope($tenantId);
$details = $service->tokenDetails();
$this->assertEquals($tenantId, $details['tenant']['id']);
$catalog = $service->serviceCatalog();
$this->assertGreaterThan(1, count($catalog));
// Test unscoping
$service->rescope('');
$details = $service->tokenDetails();
$this->assertEmpty($details['tenant']);
$catalog = $service->serviceCatalog();
$this->assertEquals(1, count($catalog));
}
}