diff --git a/composer.json b/composer.json index 5b3c8f2..8a3fe97 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Access HPCloud and OpenStack services in PHP.", "type": "library", "keywords": ["openstack","hpcloud","cloud","swift","nova"], - "license": "MIT-like", + "license": "MIT", "homepage": "http://hpcloud.com", "authors": [ { diff --git a/doc/documentation.php b/doc/documentation.php index 25a04de..2bdfeb5 100644 --- a/doc/documentation.php +++ b/doc/documentation.php @@ -171,12 +171,16 @@ * * @code * serviceCatalog('object-storage'); - * $objectStorageUrl = storageList[0]['endpoints'][0]['publicURL']; + * // $storageList = $identity->serviceCatalog('object-storage'); + * // $objectStorageUrl = storageList[0]['endpoints'][0]['publicURL']; * * // Create a new ObjectStorage instance: - * $objectStore = new \HPCloud\Storage\ObjectStorage($token, $objectStorageUrl); + * // $objectStore = new \HPCloud\Storage\ObjectStorage($token, $objectStorageUrl); + * + * // Or let ObjectStorage figure out which instance to use: + * $objectStore = \HPCloud\Storage\ObjectStorage::newFromIdentity($identity); * * // List containers: * print_r($objectStore->containers()); diff --git a/doc/oo-tutorial.md b/doc/oo-tutorial.md index 4f1f4d2..bc48453 100644 --- a/doc/oo-tutorial.md +++ b/doc/oo-tutorial.md @@ -269,6 +269,9 @@ Now we can get a new HPCloud::Storage::ObjectStorage instance: $catalog = $idService->serviceCatalog(); $store = ObjectStorage::newFromServiceCatalog($catalog, $token); + +// UPDATE: As of Beta 6, you can use newFromIdentity(): +// $store = ObjectStorage::newFromIdentity($idService); ?> ~~~ diff --git a/src/HPCloud/Bootstrap.php b/src/HPCloud/Bootstrap.php index f493668..a47d771 100644 --- a/src/HPCloud/Bootstrap.php +++ b/src/HPCloud/Bootstrap.php @@ -29,6 +29,9 @@ SOFTWARE. namespace HPCloud; +use HPCloud\Services\IdentityServices; +use HPCloud\Exception; + /** * Bootstrapping services. * @@ -128,6 +131,12 @@ class Bootstrap { 'transport' => '\HPCloud\Transport\CURLTransport', ); + /** + * An identity services object created from the global settings. + * @var object HPCloud::Services::IdentityServices + */ + public static $identity = NULL; + /** * Add the autoloader to PHP's autoloader list. * @@ -313,4 +322,56 @@ class Bootstrap { public static function hasConfig($name) { return isset(self::$config[$name]); } + + /** + * Get a HPCloud::Services::IdentityService object from the bootstrap config. + * + * A factory helper function that uses the bootstrap configuration to create + * a ready to use HPCloud::Services::IdentityService object. + * + * @param bool $force + * Whether to force the generation of a new object even if one is already + * cached. + * @retval HPCloud::Services::IdentityService + * An authenticated ready to use HPCloud::Services::IdentityService object. + * @throws HPCloud::Exception + * When the needed configuration to authenticate is not available. + */ + public static function identity($force = FALSE) { + + // If we already have an identity make sure the token is not expired. + if ($force || is_null(self::$identity) || self::$identity->isExpired()) { + + // Make sure we have an endpoint to use + if (!self::hasConfig('endpoint')) { + throw new Exception('Unable to authenticate. No endpoint supplied.'); + } + + // Neither user nor account can be an empty string, so we need + // to do more checking than self::hasConfig(), which returns TRUE + // if an item exists and is an empty string. + $user = self::config('username', NULL); + $account = self::config('account', NULL); + + // Check if we have a username/password + if (!empty($user) && self::hasConfig('password')) { + $is = new IdentityServices(self::config('endpoint')); + $is->authenticateAsUser($user, self::config('password'), self::config('tenantid', NULL), self::config('tenantname', NULL)); + self::$identity = $is; + } + + // Otherwise we go with access/secret keys + elseif (!empty($account) && self::hasConfig('secret')) { + $is = new IdentityServices(self::config('endpoint')); + $is->authenticateAsAccount($account, self::config('secret'), self::config('tenantid', NULL), self::config('tenantname', NULL)); + self::$identity = $is; + } + + else { + throw new Exception('Unable to authenticate. No account credentials supplied.'); + } + } + + return self::$identity; + } } diff --git a/src/HPCloud/Services/IdentityServices.php b/src/HPCloud/Services/IdentityServices.php index 10e1755..4f56bcf 100644 --- a/src/HPCloud/Services/IdentityServices.php +++ b/src/HPCloud/Services/IdentityServices.php @@ -122,9 +122,14 @@ namespace HPCloud\Services; * - tenants() * - rescope() * + * Serializing + * + * IdentityServices has been intentionally built to serialize well. + * This allows implementors to cache IdentityServices objects rather + * than make repeated requests for identity information. * */ -class IdentityServices { +class IdentityServices /*implements Serializable*/ { /** * The version of the API currently supported. */ @@ -171,6 +176,8 @@ class IdentityServices { */ protected $catalog = array(); + protected $userDetails; + /** * Build a new IdentityServices object. * @@ -266,7 +273,6 @@ class IdentityServices { 'auth' => $ops, ); - $body = json_encode($envelope); $headers = array( @@ -288,17 +294,20 @@ class IdentityServices { } /** - * Authenticate to Identity Services with username, password, and tenant ID. + * Authenticate to Identity Services with username, password, and either + * tenant ID or tenant Name. * * 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. + * given tenant ID. If a tenant Name is provided, this will associate the user + * with the given tenant Name. Only the tenant ID or tenant Name needs to be + * given, not both. * - * If no tenant ID is given, it will likely be necessary to rescope() the - * request (See also tenants()). + * If no tenant ID or tenant Name is given, it will likely be necessary to + * rescope() the request (See also tenants()). * * Other authentication methods: * @@ -312,13 +321,16 @@ class IdentityServices { * @param string $tenantId * The tenant ID for this account. This can be obtained through the * HPCloud console. + * @param string $tenantName + * The tenant Name for this account. This can be obtained through the + * HPCloud 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 = NULL) { + public function authenticateAsUser($username, $password, $tenantId = NULL, $tenantName = NULL) { $ops = array( 'passwordCredentials' => array( 'username' => $username, @@ -330,6 +342,12 @@ class IdentityServices { if (!empty($tenantId)) { $ops['tenantId'] = $tenantId; } + + // If a tenant name is provided, add it to the auth array. + if (!empty($tenantName)) { + $ops['tenantName'] = $tenantName; + } + return $this->authenticate($ops); } /** @@ -342,10 +360,11 @@ class IdentityServices { * The account ID and access key information can be found in the account * section of the 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. + * The third and fourth paramaters allow you to specify a tenant ID or + * tenantName. In order to access services, this object will need a tenant ID + * or tenant name. 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: * @@ -361,6 +380,9 @@ class IdentityServices { * @param string $tenantId * A valid tenant ID. This will be used to associate a tenant's services * with this token. + * @param string $tenantName + * The tenant Name for this account. This can be obtained through the + * HPCloud console. * @retval string * The auth token. * @throws HPCloud::Transport::AuthorizationException @@ -369,7 +391,7 @@ class IdentityServices { * For abnormal network conditions. The message will give an indication as * to the underlying problem. */ - public function authenticateAsAccount($account, $key, $tenantId = NULL) { + public function authenticateAsAccount($account, $key, $tenantId = NULL, $tenantName = NULL) { $ops = array( 'apiAccessKeyCredentials' => array( 'accessKey' => $account, @@ -380,6 +402,10 @@ class IdentityServices { if (!empty($tenantId)) { $ops['tenantId'] = $tenantId; } + if (!empty($tenantName)) { + $ops['tenantName'] = $tenantName; + } + return $this->authenticate($ops); } @@ -416,11 +442,16 @@ class IdentityServices { } /** - * Get the tenant name. + * Get the tenant name associated with this token. + * + * If this token has a tenant name, the name will be returned. Otherwise, this + * will return NULL. + * + * This will not be populated until after an authentication method has been + * run. * * @retval string - * The tenant name. Often this is an email - * address or other alpha-numeric string. + * The tenant name if available, or NULL. */ public function tenantName() { if (!empty($this->tokenDetails['tenant']['name'])) { @@ -460,6 +491,31 @@ class IdentityServices { return $this->tokenDetails; } + /** + * Check whether the current identity has an expired token. + * + * This does not perform a round-trip to the server. Instead, it compares the + * machine's local timestamp with the server's expiration time stamp. A + * mis-configured machine timestamp could give spurious results. + * + * @retval boolean + * This will return FALSE if there is a current token and it has + * not yet expired (according to the date info). In all other cases + * it returns TRUE. + */ + public function isExpired() { + $details = $this->tokenDetails(); + + if (empty($details['expires'])) { + return TRUE; + } + + $currentDateTime = new \DateTime('now'); + $expireDateTime = new \DateTime($details['expires']); + + return $currentDateTime > $expireDateTime; + } + /** * Get the service catalog, optionaly filtering by type. * @@ -638,6 +694,14 @@ class IdentityServices { } /** + * @see HPCloud::Services::IdentityServices::rescopeUsingTenantId() + * @deprecated + */ + public function rescope($tenantId) { + return $this->rescopeUsingTenantId($tenantId); + } + + /** * Rescope the authentication token to a different tenant. * * Note that this will rebuild the service catalog and user information for @@ -667,7 +731,7 @@ class IdentityServices { * For abnormal network conditions. The message will give an indication as * to the underlying problem. */ - public function rescope($tenantId) { + public function rescopeUsingTenantId($tenantId) { $url = $this->url() . '/tokens'; $token = $this->token(); $data = array( @@ -694,6 +758,63 @@ class IdentityServices { return $this->token(); } + /** + * 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 $tenantName + * The tenant name that this present token should be bound to. If this is the + * empty string (`''`), the present token will be "unscoped" and its tenant + * name will be removed. + * + * @retval 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 rescopeUsingTenantName($tenantName) { + $url = $this->url() . '/tokens'; + $token = $this->token(); + $data = array( + 'auth' => array( + 'tenantName' => $tenantName, + '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. * @@ -712,4 +833,24 @@ class IdentityServices { $this->serviceCatalog = $json['access']['serviceCatalog']; } + /* Not necessary. + public function serialize() { + $data = array( + 'tokenDetails' => $this->tokenDetails, + 'userDetails' => $this->userDetails, + 'serviceCatalog' => $this->serviceCatalog, + 'endpoint' => $this->endpoint, + ); + return serialize($data); + } + + public function unserialize($data) { + $vals = unserialize($data); + $this->tokenDetails = $vals['tokenDetails']; + $this->userDetails = $vals['userDetails']; + $this->serviceCatalog = $vals['serviceCatalog']; + $this->endpoint = $vals['endpoint']; + } + */ + } diff --git a/src/HPCloud/Storage/CDN.php b/src/HPCloud/Storage/CDN.php index cf1f17c..ad2bfa0 100644 --- a/src/HPCloud/Storage/CDN.php +++ b/src/HPCloud/Storage/CDN.php @@ -138,6 +138,28 @@ class CDN { */ protected $token; + /** + * Create a new instance from an IdentityServices object. + * + * This builds a new CDN instance form an authenticated + * IdentityServices object. + * + * In the service catalog, this selects the first service entry + * for CDN. At this time, that is sufficient. + * + * @param HPCloud::Services::IdentityServices $identity + * The identity to use. + * @retval object + * A CDN object or FALSE if no CDN services could be found + * in the catalog. + */ + public static function newFromIdentity($identity) { + $tok = $identity->token(); + $cat = $identity->serviceCatalog(); + + return self::newFromServiceCatalog($cat, $tok); + } + /** * Create a new CDN object based on a service catalog. * diff --git a/src/HPCloud/Storage/ObjectStorage.php b/src/HPCloud/Storage/ObjectStorage.php index 9336154..238982a 100644 --- a/src/HPCloud/Storage/ObjectStorage.php +++ b/src/HPCloud/Storage/ObjectStorage.php @@ -163,6 +163,24 @@ class ObjectStorage { return $store; } + /** + * Given an IdentityServices instance, create an ObjectStorage instance. + * + * This constructs a new ObjectStorage from an authenticated instance + * of an HPCloud::Services::IdentityServices object. + * + * @param HPCloud::Services::IdentityServices $identity + * An identity services object that already has a valid token and a + * service catalog. + * @retval object ObjectStorage + * A new ObjectStorage instance. + */ + public static function newFromIdentity($identity) { + $cat = $identity->serviceCatalog(); + $tok = $identity->token(); + return self::newFromServiceCatalog($cat, $tok); + } + /** * Given a service catalog and an token, create an ObjectStorage instance. * diff --git a/src/HPCloud/Storage/ObjectStorage/Container.php b/src/HPCloud/Storage/ObjectStorage/Container.php index ead371b..aabbf5e 100644 --- a/src/HPCloud/Storage/ObjectStorage/Container.php +++ b/src/HPCloud/Storage/ObjectStorage/Container.php @@ -148,7 +148,7 @@ class Container implements \Countable, \IteratorAggregate { */ public static function objectUrl($base, $oname) { if (strpos($oname, '/') === FALSE) { - return $base . '/' . $oname; + return $base . '/' . rawurlencode($oname); } $oParts = explode('/', $oname); @@ -513,7 +513,7 @@ class Container implements \Countable, \IteratorAggregate { if (empty($file)) { // Now build up the rest of the headers: - $headers['ETag'] = $obj->eTag(); + $headers['Etag'] = $obj->eTag(); // If chunked, we set transfer encoding; else // we set the content length. @@ -541,7 +541,7 @@ class Container implements \Countable, \IteratorAggregate { $hash = hash_init('md5'); hash_update_stream($hash, $file); $etag = hash_final($hash); - $headers['ETag'] = $etag; + $headers['Etag'] = $etag; // Not sure if this is necessary: rewind($file); diff --git a/src/HPCloud/Storage/ObjectStorage/Object.php b/src/HPCloud/Storage/ObjectStorage/Object.php index c03237f..90b87a9 100644 --- a/src/HPCloud/Storage/ObjectStorage/Object.php +++ b/src/HPCloud/Storage/ObjectStorage/Object.php @@ -451,6 +451,28 @@ class Object { return $this->additionalHeaders; } + /** + * Remove headers. + * + * This takes an array of header names, and removes + * any matching headers. Typically, only headers set + * by setAdditionalHeaders() are removed from an Object. + * (RemoteObject works differently). + * + * @attention + * Many headers are generated automatically, such as + * Content-Type and Content-Length. Removing these + * will simply result in their being regenerated. + * + * @param array $keys + * The header names to be removed. + */ + public function removeHeaders($keys) { + foreach ($keys as $k) { + unset($this->additionalHeaders[$k]); + } + } + /** * This object should be transmitted in chunks. * diff --git a/src/HPCloud/Storage/ObjectStorage/RemoteObject.php b/src/HPCloud/Storage/ObjectStorage/RemoteObject.php index bda1543..b4494e5 100644 --- a/src/HPCloud/Storage/ObjectStorage/RemoteObject.php +++ b/src/HPCloud/Storage/ObjectStorage/RemoteObject.php @@ -65,7 +65,7 @@ class RemoteObject extends Object { * serve as a good indicator that the object does not have all * attributes set. */ - protected $allHeaders; + protected $allHeaders = array(); protected $cdnUrl; protected $cdnSslUrl; @@ -125,7 +125,8 @@ class RemoteObject extends Object { public static function newFromHeaders($name, $headers, $token, $url, $cdnUrl = NULL, $cdnSslUrl = NULL) { $object = new RemoteObject($name); - $object->allHeaders = $headers; + //$object->allHeaders = $headers; + $object->setHeaders($headers); //throw new \Exception(print_r($headers, TRUE)); @@ -259,6 +260,16 @@ class RemoteObject extends Object { return $this->metadata; } + public function setHeaders($headers) { + $this->allHeaders = array(); + + foreach ($headers as $name => $value) { + if (strpos($name, Container::METADATA_HEADER_PREFIX) !== 0) { + $this->allHeaders[$name] = $value; + } + } + } + /** * Get the HTTP headers sent by the server. * @@ -274,6 +285,66 @@ class RemoteObject extends Object { return $this->allHeaders; } + public function additionalHeaders($mergeAll = FALSE) { + // Any additional headers will be set. Note that $this->headers will contain + // some headers that are NOT additional. But we do not know which headers are + // additional and which are from Swift because Swift does not commit to using + // a specific set of headers. + if ($mergeAll) { + $additionalHeaders = parent::additionalHeaders() + $this->allHeaders; + $this->filterHeaders($additionalHeaders); + } + else { + $additionalHeaders = parent::additionalHeaders(); + } + + return $additionalHeaders; + } + + protected $reservedHeaders = array( + 'etag' => TRUE, 'content-length' => TRUE, + 'x-auth-token' => TRUE, + 'transfer-encoding' => TRUE, + 'x-trans-id' => TRUE, + ); + public function filterHeaders(&$headers) { + $unset = array(); + foreach ($headers as $name => $value) { + $lower = strtolower($name); + if (isset($this->reservedHeaders[$lower])) { + $unset[] = $name; + } + } + foreach ($unset as $u) { + unset($headers[$u]); + } + } + + /** + * Given an array of header names. + * + * This will remove the given headers from the existing headers. + * Both additional headers and the original headers from the + * server are affected here. + * + * Note that you cannot remove metadata through this mechanism, + * as it is managed using the metadata() methods. + * + * @attention + * Many headers are generated automatically, such as + * Content-Type and Content-Length. Removing these + * will simply result in their being regenerated. + * + * @param array $keys + * The header names to be removed. + */ + public function removeHeaders($keys) { + foreach ($keys as $key) { + unset($this->allHeaders[$key]); + unset($this->additionalHeaders[$key]); + } + } + /** * Get the content of this object. * diff --git a/src/HPCloud/Storage/ObjectStorage/StreamWrapper.php b/src/HPCloud/Storage/ObjectStorage/StreamWrapper.php index 608dbad..cd22218 100644 --- a/src/HPCloud/Storage/ObjectStorage/StreamWrapper.php +++ b/src/HPCloud/Storage/ObjectStorage/StreamWrapper.php @@ -108,7 +108,8 @@ use \HPCloud\Storage\ObjectStorage; * array('swift' => array( * 'account' => ACCOUNT_NUMBER, * 'key' => SECRET_KEY, - * 'tenantId' => TENANT_ID + * 'tenantid' => TENANT_ID, + * 'tenantname' => TENANT_NAME, // Optional instead of tenantid. * 'endpoint' => AUTH_ENDPOINT_URL, * ) * ) @@ -218,9 +219,12 @@ use \HPCloud\Storage\ObjectStorage; * -# User login: username, password, tenantid, endpoint * -# Existing (valid) token: token, swift_endpoint * + * @attention + * As of 1.0.0-beta6, you may use `tenantname` instead of `tenantid`. + * * The third method (token) can be used when the application has already - * authenticated. In this case, a token has been generated and assigneet - * to an account and tenant ID. + * authenticated. In this case, a token has been generated and assigned + * to an account and tenant. * * The following parameters may be set either in the stream context * or through HPCloud::Bootstrap::setConfiguration(): @@ -230,10 +234,10 @@ use \HPCloud\Storage\ObjectStorage; * option. * - swift_endpoint: The URL to the swift instance. This is only necessary if * 'token' is set. Otherwise it is ignored. - * - username: A username. MUST be accompanied by 'password' and 'tenantid'. - * - password: A password. MUST be accompanied by 'username' and 'tenantid'. - * - account: An account ID. MUST be accompanied by a 'key' and 'tenantid'. - * - key: A secret key. MUST be accompanied by an 'account' and 'tenantid'. + * - username: A username. MUST be accompanied by 'password' and 'tenantid' (or 'tenantname'). + * - password: A password. MUST be accompanied by 'username' and 'tenantid' (or 'tenantname'). + * - account: An account ID. MUST be accompanied by a 'key' and 'tenantid' (or 'tenantname'). + * - key: A secret key. MUST be accompanied by an 'account' and 'tenantid' (or 'tenantname'). * - endpoint: The URL to the authentication endpoint. Necessary if you are not * using a 'token' and 'swift_endpoint'. * - use_swift_auth: If this is set to TRUE, it will force the app to use @@ -251,6 +255,10 @@ use \HPCloud\Storage\ObjectStorage; * - cdn_require_ssl: If this is set to FALSE, then CDN-based requests * may use plain HTTP instead of HTTPS. This will spead up CDN * fetches at the cost of security. + * - tenantid: The tenant ID for the services you will use. (An account may + * have multiple tenancies associated.) + * - tenantname: The tenant name for the services you will use. You may use + * this in lieu of tenant ID. * * @attention * ADVANCED: You can also pass an HPCloud::Storage::CDN object in use_cdn instead of @@ -533,7 +541,8 @@ class StreamWrapper { * @code * '1234', + * 'tenantname' => 'foo@example.com', + * // 'tenantid' => '1234', // You can use this instead of tenantname * 'account' => '1234', * 'secret' => '4321', * 'endpoint' => 'https://auth.example.com', @@ -947,7 +956,7 @@ class StreamWrapper { * @code * '12345', + * 'tenantname' => 'me@example.com', * 'username' => 'me@example.com', * 'password' => 'secret', * 'endpoint' => 'https://auth.example.com', @@ -1475,10 +1484,10 @@ class StreamWrapper { * option. * - swift_endpoint: The URL to the swift instance. This is only necessary if * 'token' is set. Otherwise it is ignored. - * - username: A username. MUST be accompanied by 'password' and 'tenantid'. - * - password: A password. MUST be accompanied by 'username' and 'tenantid'. - * - account: An account ID. MUST be accompanied by a 'key' and 'tenantid'. - * - key: A secret key. MUST be accompanied by an 'account' and 'tenantid'. + * - username: A username. MUST be accompanied by 'password' and 'tenantname'. + * - password: A password. MUST be accompanied by 'username' and 'tenantname'. + * - account: An account ID. MUST be accompanied by a 'key' and 'tenantname'. + * - key: A secret key. MUST be accompanied by an 'account' and 'tenantname'. * - endpoint: The URL to the authentication endpoint. Necessary if you are not * using a 'token' and 'swift_endpoint'. * - use_swift_auth: If this is set to TRUE, it will force the app to use @@ -1498,6 +1507,7 @@ class StreamWrapper { $key = $this->cxt('key'); $tenantId = $this->cxt('tenantid'); + $tenantName = $this->cxt('tenantname'); $authUrl = $this->cxt('endpoint'); $endpoint = $this->cxt('swift_endpoint'); @@ -1522,8 +1532,11 @@ class StreamWrapper { } // If we get here and tenant ID is not set, we can't get a container. - elseif (empty($tenantId) || empty($authUrl)) { - throw new \HPCloud\Exception('Tenant ID (tenantid) and endpoint are required.'); + elseif (empty($tenantId) && empty($tenantName)) { + throw new \HPCloud\Exception('Either Tenant ID (tenantid) or Tenant Name (tenantname) is required.'); + } + elseif (empty($authUrl)) { + throw new \HPCloud\Exception('An Identity Service Endpoint (endpoint) is required.'); } // Try to authenticate and get a new token. else { @@ -1612,6 +1625,7 @@ class StreamWrapper { $key = $this->cxt('key'); $tenantId = $this->cxt('tenantid'); + $tenantName = $this->cxt('tenantname'); $authUrl = $this->cxt('endpoint'); $ident = new \HPCloud\Services\IdentityServices($authUrl); @@ -1619,10 +1633,10 @@ class StreamWrapper { // Frustrated? Go burninate. http://www.homestarrunner.com/trogdor.html if (!empty($username) && !empty($password)) { - $token = $ident->authenticateAsUser($username, $password, $tenantId); + $token = $ident->authenticateAsUser($username, $password, $tenantId, $tenantName); } elseif (!empty($account) && !empty($key)) { - $token = $ident->authenticateAsAccount($account, $key, $tenantId); + $token = $ident->authenticateAsAccount($account, $key, $tenantId, $tenantName); } else { throw new \HPCloud\Exception('Either username/password or account/key must be provided.'); diff --git a/src/HPCloud/Transport/Response.php b/src/HPCloud/Transport/Response.php index f39aa32..026c0fe 100644 --- a/src/HPCloud/Transport/Response.php +++ b/src/HPCloud/Transport/Response.php @@ -179,12 +179,13 @@ class Response { } } else { - while (!feof($this->handle)) { - $out .= fread($this->handle, 8192); - } + // XXX: This works fine with CURL, but will not + // work with PHP HTTP Stream Wrapper b/c the + // wrapper has a bug that will cause this to + // hang. + $out = stream_get_contents($this->handle); } - // Should we close or rewind? // Cannot rewind PHP HTTP streams. fclose($this->handle); diff --git a/test/AuthTest.php b/test/AuthTest.php index 0f4d8ca..d258d91 100644 --- a/test/AuthTest.php +++ b/test/AuthTest.php @@ -26,7 +26,8 @@ SOFTWARE. * You can run the test with `php test/AuthTest.php username key`. */ -require_once 'src/HPCloud/Bootstrap.php'; +$base = dirname(__DIR__); +require_once $base . '/src/HPCloud/Bootstrap.php'; use \HPCloud\Storage\ObjectStorage; use \HPCloud\Services\IdentityServices; diff --git a/test/TestCase.php b/test/TestCase.php index 86b72b1..c918b28 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -102,8 +102,8 @@ class TestCase extends \PHPUnit_Framework_TestCase { $user = self::$settings['hpcloud.swift.account']; $key = self::$settings['hpcloud.swift.key']; - // $url = self::$settings['hpcloud.swift.url']; - $url = self::$settings['hpcloud.identity.url']; + $url = self::$settings['hpcloud.swift.url']; + //$url = self::$settings['hpcloud.identity.url']; return \HPCloud\Storage\ObjectStorage::newFromSwiftAuth($user, $key, $url); @@ -145,19 +145,7 @@ class TestCase extends \PHPUnit_Framework_TestCase { if ($reset || empty(self::$ostore)) { $ident = $this->identity($reset); - $services = $ident->serviceCatalog(\HPCloud\Storage\ObjectStorage::SERVICE_TYPE); - - if (empty($services)) { - throw new \Exception('No object-store service found.'); - } - - /* - //$serviceURL = $services[0]['endpoints'][0]['adminURL']; - $serviceURL = $services[0]['endpoints'][0]['publicURL']; - - $objStore = new \HPCloud\Storage\ObjectStorage($ident->token(), $serviceURL); - */ - $objStore = \HPCloud\Storage\ObjectStorage::newFromServiceCatalog($services, $ident->token()); + $objStore = \HPCloud\Storage\ObjectStorage::newFromIdentity($ident); self::$ostore = $objStore; diff --git a/test/Tests/CDNTest.php b/test/Tests/CDNTest.php index 9fc1d2d..cf2cad5 100644 --- a/test/Tests/CDNTest.php +++ b/test/Tests/CDNTest.php @@ -80,6 +80,18 @@ class CDNTest extends \HPCloud\Tests\TestCase { return $cdn; } + /** + * @depends testConstructor + */ + public function testNewFromIdentity() { + $ident = $this->identity(); + $cdn = CDN::newFromIdentity($ident); + + $this->assertInstanceOf('\HPCloud\Storage\CDN', $cdn); + + return $cdn; + } + /** * @depends testNewFromServiceCatalog */ diff --git a/test/Tests/IdentityServicesTest.php b/test/Tests/IdentityServicesTest.php index d9e16ea..ec0c783 100644 --- a/test/Tests/IdentityServicesTest.php +++ b/test/Tests/IdentityServicesTest.php @@ -30,6 +30,7 @@ require_once 'src/HPCloud/Bootstrap.php'; require_once 'test/TestCase.php'; use \HPCloud\Services\IdentityServices; +use \HPCloud\Bootstrap; class IdentityServicesTest extends \HPCloud\Tests\TestCase { @@ -169,6 +170,48 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase { $this->assertNotEmpty($service->token()); } + /** + * @depends testAuthenticateAsAccount + */ + public function testIsExpired($service) { + $this->assertFalse($service->isExpired()); + + $service2 = new IdentityServices(self::conf('hpcloud.identity.url')); + $this->assertTrue($service2->isExpired()); + } + + /** + * @depends testAuthenticateAsAccount + */ + public function testTenantName() { + $account = self::conf('hpcloud.identity.account'); + $secret = self::conf('hpcloud.identity.secret'); + $user = self::conf('hpcloud.identity.username'); + $pass = self::conf('hpcloud.identity.password'); + $tenantName = self::conf('hpcloud.identity.tenantName'); + + $service = new IdentityServices(self::conf('hpcloud.identity.url')); + $this->assertNull($service->tenantName()); + + $service->authenticateAsUser($user, $pass); + $this->assertEmpty($service->tenantName()); + + $service = new IdentityServices(self::conf('hpcloud.identity.url')); + $ret = $service->authenticateAsUser($user, $pass, NULL, $tenantName); + $this->assertNotEmpty($service->tenantName()); + + $service = new IdentityServices(self::conf('hpcloud.identity.url')); + $this->assertNull($service->tenantName()); + + $service->authenticateAsAccount($account, $secret); + $this->assertEmpty($service->tenantName()); + + $service = new IdentityServices(self::conf('hpcloud.identity.url')); + $ret = $service->authenticateAsAccount($account, $secret, NULL, $tenantName); + $this->assertNotEmpty($service->tenantName()); + $this->assertEquals($tenantName, $service->tenantName()); + } + /** * @depends testAuthenticateAsAccount */ @@ -271,6 +314,33 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase { $this->assertNotEmpty($user['roles']); } + /** + * @depends testAuthenticateAsAccount + * @group serialize + */ + public function testSerialization($service) { + + $ser = serialize($service); + + $this->assertNotEmpty($ser); + + $again = unserialize($ser); + + $this->assertInstanceOf('\HPCloud\Services\IdentityServices', $again); + + $this->assertEquals($service->tenantId(), $again->tenantId()); + $this->assertEquals($service->serviceCatalog(), $again->serviceCatalog()); + $this->assertEquals($service->tokenDetails(), $again->tokenDetails()); + $this->assertEquals($service->user(), $again->user()); + $this->assertFalse($again->isExpired()); + + $tenantId = $again->tenantId(); + + $newTok = $again->rescopeUsingTenantId($tenantId); + + $this->assertNotEmpty($newTok); + } + /** * @group tenant */ @@ -319,7 +389,7 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase { $catalog = $service->serviceCatalog(); $this->assertEquals(1, count($catalog)); - $service->rescope($tenantId); + $service->rescopeUsingTenantId($tenantId); $details = $service->tokenDetails(); $this->assertEquals($tenantId, $details['tenant']['id']); @@ -327,6 +397,46 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase { $catalog = $service->serviceCatalog(); $this->assertGreaterThan(1, count($catalog)); + // Test unscoping + $service->rescopeUsingTenantId(''); + $details = $service->tokenDetails(); + $this->assertEmpty($details['tenant']); + $catalog = $service->serviceCatalog(); + $this->assertEquals(1, count($catalog)); + + } + + /** + * @group tenant + * @depends testTenants + */ + function testRescopeByTenantName() { + $service = new IdentityServices(self::conf('hpcloud.identity.url')); + $user = self::conf('hpcloud.identity.username'); + $pass = self::conf('hpcloud.identity.password'); + $tenantName = self::conf('hpcloud.identity.tenantName'); + + // 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->rescopeUsingTenantName($tenantName); + + $details = $service->tokenDetails(); + $this->assertEquals($tenantName, $details['tenant']['name']); + + $catalog = $service->serviceCatalog(); + $this->assertGreaterThan(1, count($catalog)); + // Test unscoping $service->rescope(''); $details = $service->tokenDetails(); @@ -335,4 +445,77 @@ class IdentityServicesTest extends \HPCloud\Tests\TestCase { $this->assertEquals(1, count($catalog)); } + + /** + * Test the bootstrap identity factory. + * @depends testAuthenticateAsAccount + * @depends testAuthenticateAsUser + */ + function testBootstrap() { + + // We need to save the config settings and reset the bootstrap to this. + // It does not remove the old settings. The means the identity fall through + // for different settings may not happen because of ordering. So, we cache + // and reset back to the default for each test. + $reset = Bootstrap::$config; + + // Test authenticating as a user. + $settings = array( + 'username' => self::conf('hpcloud.identity.username'), + 'password' => self::conf('hpcloud.identity.password'), + 'endpoint' => self::conf('hpcloud.identity.url'), + 'tenantid' => self::conf('hpcloud.identity.tenantId'), + ); + Bootstrap::setConfiguration($settings); + + $is = Bootstrap::identity(TRUE); + $this->assertInstanceOf('\HPCloud\Services\IdentityServices', $is); + + Bootstrap::$config = $reset; + + // Test authenticating as an account. + $settings = array( + 'account' => self::conf('hpcloud.identity.account'), + 'secret' => self::conf('hpcloud.identity.secret'), + 'endpoint' => self::conf('hpcloud.identity.url'), + 'tenantid' => self::conf('hpcloud.identity.tenantId'), + ); + Bootstrap::setConfiguration($settings); + + $is = Bootstrap::identity(TRUE); + $this->assertInstanceOf('\HPCloud\Services\IdentityServices', $is); + + // Test getting a second instance from the cache. + $is2 = Bootstrap::identity(); + $this->assertEquals($is, $is2); + + // Test that forcing a refresh does so. + $is2 = Bootstrap::identity(TRUE); + $this->assertNotEquals($is, $is2); + + Bootstrap::$config = $reset; + + // Test with tenant name + $settings = array( + 'account' => self::conf('hpcloud.identity.account'), + 'secret' => self::conf('hpcloud.identity.secret'), + 'endpoint' => self::conf('hpcloud.identity.url'), + 'tenantname' => self::conf('hpcloud.identity.tenantName'), + ); + Bootstrap::setConfiguration($settings); + + $is = Bootstrap::identity(TRUE); + $this->assertInstanceOf('\HPCloud\Services\IdentityServices', $is); + + $settings = array( + 'username' => self::conf('hpcloud.identity.username'), + 'password' => self::conf('hpcloud.identity.password'), + 'endpoint' => self::conf('hpcloud.identity.url'), + 'tenantname' => self::conf('hpcloud.identity.tenantName'), + ); + Bootstrap::setConfiguration($settings); + + $is = Bootstrap::identity(TRUE); + $this->assertInstanceOf('\HPCloud\Services\IdentityServices', $is); + } } diff --git a/test/Tests/ObjectStorageTest.php b/test/Tests/ObjectStorageTest.php index 540cffb..73657f5 100644 --- a/test/Tests/ObjectStorageTest.php +++ b/test/Tests/ObjectStorageTest.php @@ -77,7 +77,17 @@ class ObjectStorageTest extends \HPCloud\Tests\TestCase { } public function testNewFromServiceCatalog() { - $ostore = $this->objectStore(); + $ident = $this->identity(); + $tok = $ident->token(); + $cat = $ident->serviceCatalog(); + $ostore = \HPCloud\Storage\ObjectStorage::newFromServiceCatalog($cat, $tok); + $this->assertInstanceOf('\HPCloud\Storage\ObjectStorage', $ostore); + $this->assertTrue(strlen($ostore->token()) > 0); + } + + public function testNewFromIdnetity() { + $ident = $this->identity(); + $ostore = \HPCloud\Storage\ObjectStorage::newFromIdentity($ident); $this->assertInstanceOf('\HPCloud\Storage\ObjectStorage', $ostore); $this->assertTrue(strlen($ostore->token()) > 0); } diff --git a/test/Tests/ObjectTest.php b/test/Tests/ObjectTest.php index c7a1d3c..2e18716 100644 --- a/test/Tests/ObjectTest.php +++ b/test/Tests/ObjectTest.php @@ -131,4 +131,24 @@ class ObjectTest extends \HPCloud\Tests\TestCase { $this->assertEquals('Leibniz', $got['Gottfried']); } + + public function testAdditionalHeaders() { + $o = $this->basicObjectFixture(); + + $extra = array( + 'a' => 'b', + 'aaa' => 'bbb', + 'ccc' => 'bbb', + ); + $o->setAdditionalHeaders($extra); + + $got = $o->additionalHeaders(); + $this->assertEquals(3, count($got)); + + $o->removeHeaders(array('ccc')); + + + $got = $o->additionalHeaders(); + $this->assertEquals(2, count($got)); + } } diff --git a/test/Tests/RemoteObjectTest.php b/test/Tests/RemoteObjectTest.php index 3d6768b..2a57b58 100644 --- a/test/Tests/RemoteObjectTest.php +++ b/test/Tests/RemoteObjectTest.php @@ -53,6 +53,10 @@ class RemoteObjectTest extends \HPCloud\Tests\TestCase { $object->setMetadata(array(self::FMETA_NAME => self::FMETA_VALUE)); $object->setDisposition(self::FDISPOSITION); $object->setEncoding(self::FENCODING); + $object->setAdditionalHeaders(array( + 'Access-Control-Allow-Origin' => 'http://example.com', + 'Access-control-allow-origin' => 'http://example.com', + )); // Need some headers that Swift actually stores and returns. This // one does not seem to be returned ever. @@ -146,6 +150,15 @@ class RemoteObjectTest extends \HPCloud\Tests\TestCase { $headers = $obj->headers(); $this->assertTrue(count($headers) > 1); + //fwrite(STDOUT, print_r($headers, TRUE)); + + $this->assertNotEmpty($headers['Date']); + + $obj->removeHeaders(array('Date')); + + $headers = $obj->headers(); + $this->assertFalse(isset($headers['Date'])); + // Swift doesn't return CORS headers even though it is supposed to. //$this->assertEquals(self::FCORS_VALUE, $headers[self::FCORS_NAME]); } diff --git a/test/Tests/StreamWrapperFSTest.php b/test/Tests/StreamWrapperFSTest.php index 3639e14..dc31352 100644 --- a/test/Tests/StreamWrapperFSTest.php +++ b/test/Tests/StreamWrapperFSTest.php @@ -45,6 +45,46 @@ class StreamWrapperFSTest extends \HPCloud\Tests\TestCase { /*public static function setUpBeforeClass() { }*/ + /** + * Cleaning up the test container so we can reuse it for other tests. + */ + public static function tearDownAfterClass() { + + // First we get an identity + $user = self::conf('hpcloud.identity.username'); + $pass = self::conf('hpcloud.identity.password'); + $tenantId = self::conf('hpcloud.identity.tenantId'); + $url = self::conf('hpcloud.identity.url'); + + $ident = new \HPCloud\Services\IdentityServices($url); + + $token = $ident->authenticateAsUser($user, $pass, $tenantId); + + // Then we need to get an instance of storage + $store = \HPCloud\Storage\ObjectStorage::newFromIdentity($ident); + + + // Delete the container and all the contents. + $cname = self::$settings['hpcloud.swift.container']; + + try { + $container = $store->container($cname); + } + // The container was never created. + catch (\HPCloud\Transport\FileNotFoundException $e) { + return; + } + + foreach ($container as $object) { + try { + $container->delete($object->name()); + } + catch (\Exception $e) {} + } + + $store->deleteContainer($cname); + } + protected function newUrl($objectName) { $scheme = StreamWrapperFS::DEFAULT_SCHEME; $cname = self::$settings['hpcloud.swift.container']; @@ -129,7 +169,7 @@ class StreamWrapperFSTest extends \HPCloud\Tests\TestCase { 'account' => self::$settings['hpcloud.identity.account'], 'key' => self::$settings['hpcloud.identity.secret'], 'endpoint' => self::$settings['hpcloud.identity.url'], - 'tenantit' => self::$settings['hpcloud.identity.tenantId'], + 'tenantid' => self::$settings['hpcloud.identity.tenantId'], 'token' => $this->objectStore()->token(), 'swift_endpoint' => $this->objectStore()->url(), ); diff --git a/test/Tests/StreamWrapperTest.php b/test/Tests/StreamWrapperTest.php index 70e768d..ff4ef59 100644 --- a/test/Tests/StreamWrapperTest.php +++ b/test/Tests/StreamWrapperTest.php @@ -42,6 +42,46 @@ class StreamWrapperTest extends \HPCloud\Tests\TestCase { const FNAME = 'streamTest.txt'; const FTYPE = 'application/x-tuna-fish; charset=iso-8859-13'; + /** + * Cleaning up the test container so we can reuse it for other tests. + */ + public static function tearDownAfterClass() { + + // First we get an identity + $user = self::conf('hpcloud.identity.username'); + $pass = self::conf('hpcloud.identity.password'); + $tenantId = self::conf('hpcloud.identity.tenantId'); + $url = self::conf('hpcloud.identity.url'); + + $ident = new \HPCloud\Services\IdentityServices($url); + + $token = $ident->authenticateAsUser($user, $pass, $tenantId); + + // Then we need to get an instance of storage + $store = \HPCloud\Storage\ObjectStorage::newFromIdentity($ident); + + + // Delete the container and all the contents. + $cname = self::$settings['hpcloud.swift.container']; + + try { + $container = $store->container($cname); + } + // The container was never created. + catch (\HPCloud\Transport\FileNotFoundException $e) { + return; + } + + foreach ($container as $object) { + try { + $container->delete($object->name()); + } + catch (\Exception $e) {} + } + + $store->deleteContainer($cname); + } + protected function newUrl($objectName) { $scheme = StreamWrapper::DEFAULT_SCHEME; $cname = self::$settings['hpcloud.swift.container']; @@ -126,7 +166,7 @@ class StreamWrapperTest extends \HPCloud\Tests\TestCase { 'account' => self::$settings['hpcloud.identity.account'], 'key' => self::$settings['hpcloud.identity.secret'], 'endpoint' => self::$settings['hpcloud.identity.url'], - 'tenantit' => self::$settings['hpcloud.identity.tenantId'], + 'tenantid' => self::$settings['hpcloud.identity.tenantId'], 'token' => $this->objectStore()->token(), 'swift_endpoint' => $this->objectStore()->url(), ); diff --git a/test/example.settings.ini b/test/example.settings.ini index 75a02b3..4e03212 100644 --- a/test/example.settings.ini +++ b/test/example.settings.ini @@ -4,9 +4,13 @@ ;;;;;;;;;;;;;;;;;; ; Settings to work with swift: +; Account is the tenandId:console username. hpcloud.swift.account = 12345678:87654321 +; Key is the console account password. hpcloud.swift.key = abcdef123456 -hpcloud.swift.url = https://region-a.geo-1.objects.hpcloudsvc.com/auth/v1.0/ +; URL is the same as used for identity services calls (including port) except +; with /auth/v1.0/ appended to the end. +hpcloud.swift.url = https://region-a.geo-1.identity.hpcloudsvc.com:35357/auth/v1.0/ ; Container used for testing. hpcloud.swift.container = "I♡HPCloud"