From 99ea808e33a95ee73efa32b875e6f0280ce8f07f Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Tue, 24 Jan 2012 20:19:51 -0600 Subject: [PATCH] Story #1104: As a user I can authenticate through Identity Services. --- src/HPCloud/Services/IdentityServices.php | 226 ++++++++++++++++++++-- test/Tests/IdentityServicesTest.php | 94 ++++++++- 2 files changed, 302 insertions(+), 18 deletions(-) diff --git a/src/HPCloud/Services/IdentityServices.php b/src/HPCloud/Services/IdentityServices.php index 0ecce66..cf1baf5 100644 --- a/src/HPCloud/Services/IdentityServices.php +++ b/src/HPCloud/Services/IdentityServices.php @@ -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. + * + * Where do I get a tenant ID? + * + * 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 + * 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']; - } } diff --git a/test/Tests/IdentityServicesTest.php b/test/Tests/IdentityServicesTest.php index 24a612e..7e384d2 100644 --- a/test/Tests/IdentityServicesTest.php +++ b/test/Tests/IdentityServicesTest.php @@ -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)); + + } }