Skip to content

Commit 4a6ecf7

Browse files
committed
Merge branch 'secret-management'
2 parents 76bd22f + e7eeb6f commit 4a6ecf7

23 files changed

+706
-5
lines changed

README.md

+31-1
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,36 @@ Or you can specify the number and country manually
379379
$client->numbers()->purchase('14155550100', 'US');
380380
```
381381

382+
### Managing Secrets
383+
384+
An API is provided to allow you to rotate your API secrets. You can create a new secret (up to a maximum of two secrets) and delete the existing one once all applications have been updated.
385+
386+
Get a list of the secrets:
387+
388+
```php
389+
$secretCollection = $client->account()->listSecrets(API_KEY);
390+
391+
foreach($secretCollection['secrets'] as $secret) {
392+
echo "ID: " . $secret['id'] . " (created " . $secret['created_at'] .")\n";
393+
}
394+
```
395+
396+
Create a new secret (the created dates will help you know which is which):
397+
398+
```php
399+
$client->account()->createSecret(API_KEY, 'awes0meNewSekret!!;');
400+
```
401+
402+
Delete the old secret (any application still using these credentials will stop working):
403+
404+
```php
405+
try {
406+
$response = $client->account()->deleteSecret(API_KEY, 'd0f40c7e-91f2-4fe0-8bc6-8942587b622c');
407+
} catch(\Nexmo\Client\Exception\Request $e) {
408+
echo $e->getMessage();
409+
}
410+
```
411+
382412
## Troubleshooting
383413

384414
Some users have issues making requests due to the following error:
@@ -400,7 +430,6 @@ curl.cainfo = "/etc/pki/tls/cacert.pem"
400430
curl.cainfo = "C:\php\extras\ssl\cacert.pem"
401431
```
402432

403-
404433
API Coverage
405434
------------
406435

@@ -409,6 +438,7 @@ API Coverage
409438
* [X] Pricing
410439
* [ ] Settings
411440
* [ ] Top Up
441+
* [X] Secret Management
412442
* [X] Numbers
413443
* [X] Search
414444
* [X] Buy

src/Account/Client.php

+74-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Nexmo\Account;
44

5+
use Nexmo\ApiErrorHandler;
56
use Nexmo\Client\ClientAwareInterface;
67
use Nexmo\Client\ClientAwareTrait;
78
use Nexmo\Network;
@@ -95,6 +96,78 @@ public function topUp($trx)
9596
}
9697
}
9798

99+
public function listSecrets($apiKey)
100+
{
101+
$body = $this->get( \Nexmo\Client::BASE_API . '/accounts/'.$apiKey.'/secrets');
102+
return SecretCollection::fromApi($body);
103+
}
104+
105+
public function getSecret($apiKey, $secretId)
106+
{
107+
$body = $this->get( \Nexmo\Client::BASE_API . '/accounts/'.$apiKey.'/secrets/'. $secretId);
108+
return Secret::fromApi($body);
109+
}
110+
111+
public function createSecret($apiKey, $newSecret)
112+
{
113+
$body = [
114+
'secret' => $newSecret
115+
];
116+
117+
$request = new Request(
118+
\Nexmo\Client::BASE_API . '/accounts/'.$apiKey.'/secrets'
119+
,'POST'
120+
, 'php://temp'
121+
, ['content-type' => 'application/json']
122+
);
123+
124+
$request->getBody()->write(json_encode($body));
125+
$response = $this->client->send($request);
126+
127+
$rawBody = $response->getBody()->getContents();
128+
$responseBody = json_decode($rawBody, true);
129+
ApiErrorHandler::check($responseBody, $response->getStatusCode());
130+
131+
return Secret::fromApi($responseBody);
132+
}
133+
134+
public function deleteSecret($apiKey, $secretId)
135+
{
136+
$request = new Request(
137+
\Nexmo\Client::BASE_API . '/accounts/'.$apiKey.'/secrets/'. $secretId
138+
,'DELETE'
139+
, 'php://temp'
140+
, ['content-type' => 'application/json']
141+
);
142+
143+
$response = $this->client->send($request);
144+
$rawBody = $response->getBody()->getContents();
145+
$body = json_decode($rawBody, true);
146+
147+
// This will throw an exception on any error
148+
ApiErrorHandler::check($body, $response->getStatusCode());
149+
150+
// This returns a 204, so no response body
151+
}
152+
153+
protected function get($url) {
154+
$request = new Request(
155+
$url
156+
,'GET'
157+
, 'php://temp'
158+
, ['content-type' => 'application/json']
159+
);
160+
161+
$response = $this->client->send($request);
162+
$rawBody = $response->getBody()->getContents();
163+
$body = json_decode($rawBody, true);
164+
165+
// This will throw an exception on any error
166+
ApiErrorHandler::check($body, $response->getStatusCode());
167+
168+
return $body;
169+
}
170+
98171
protected function getException(ResponseInterface $response, $application = null)
99172
{
100173
$body = json_decode($response->getBody()->getContents(), true);
@@ -111,4 +184,4 @@ protected function getException(ResponseInterface $response, $application = null
111184
return $e;
112185
}
113186

114-
}
187+
}

src/Account/Secret.php

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
namespace Nexmo\Account;
4+
5+
use Nexmo\InvalidResponseException;
6+
7+
class Secret implements \ArrayAccess {
8+
protected $data;
9+
10+
public function __construct($data) {
11+
$this->data = $data;
12+
}
13+
14+
public function getId() {
15+
return $this['id'];
16+
}
17+
18+
public function getCreatedAt() {
19+
return $this['created_at'];
20+
}
21+
22+
public function getLinks() {
23+
return $this['_links'];
24+
}
25+
26+
public static function fromApi($data) {
27+
if (!isset($data['id'])) {
28+
throw new InvalidResponseException("Missing key: 'id");
29+
}
30+
if (!isset($data['created_at'])) {
31+
throw new InvalidResponseException("Missing key: 'created_at");
32+
}
33+
return new self($data);
34+
}
35+
36+
public function offsetExists($offset)
37+
{
38+
return isset($this->data[$offset]);
39+
}
40+
41+
public function offsetGet($offset)
42+
{
43+
return $this->data[$offset];
44+
}
45+
46+
public function offsetSet($offset, $value)
47+
{
48+
throw new \Exception('Secret::offsetSet is not implemented');
49+
}
50+
51+
public function offsetUnset($offset)
52+
{
53+
throw new \Exception('Secret::offsetUnset is not implemented');
54+
}
55+
56+
}

src/Account/SecretCollection.php

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Nexmo\Account;
4+
5+
class SecretCollection implements \ArrayAccess {
6+
protected $data;
7+
8+
public function __construct($secrets, $links) {
9+
$this->data = [
10+
'secrets' => $secrets,
11+
'_links' => $links
12+
];
13+
}
14+
15+
public function getSecrets() {
16+
return $this['secrets'];
17+
}
18+
19+
public function getLinks() {
20+
return $this['_links'];
21+
}
22+
23+
public static function fromApi($data) {
24+
$secrets = [];
25+
foreach ($data['_embedded']['secrets'] as $s) {
26+
$secrets[] = Secret::fromApi($s);
27+
}
28+
return new self($secrets, $data['_links']);
29+
}
30+
31+
public function offsetExists($offset)
32+
{
33+
return isset($this->data[$offset]);
34+
}
35+
36+
public function offsetGet($offset)
37+
{
38+
return $this->data[$offset];
39+
}
40+
41+
public function offsetSet($offset, $value)
42+
{
43+
throw new \Exception('SecretCollection::offsetSet is not implemented');
44+
}
45+
46+
public function offsetUnset($offset)
47+
{
48+
throw new \Exception('SecretCollection::offsetUnset is not implemented');
49+
}
50+
51+
}

src/ApiErrorHandler.php

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Nexmo;
4+
use Nexmo\Client\Exception;
5+
6+
class ApiErrorHandler {
7+
public static function check($body, $statusCode) {
8+
$statusCodeType = (int) ($statusCode / 100);
9+
10+
// If it's ok, we can continue
11+
if ($statusCodeType == 2) {
12+
return;
13+
}
14+
15+
// Build up our error message
16+
$errorMessage = $body['title'];
17+
if (isset($body['detail']) && $body['detail']) {
18+
$errorMessage .= ': '.$body['detail'].'.';
19+
} else {
20+
$errorMessage .= '.';
21+
}
22+
23+
$errorMessage .= ' See '.$body['type'].' for more information';
24+
25+
// If it's a 5xx error, throw an exception
26+
if ($statusCodeType == 5) {
27+
throw new Exception\Server($errorMessage, $statusCode);
28+
}
29+
30+
// Otherwise it's a 4xx, so we may have more context for the user
31+
// If it's a validation error, share that information
32+
if (isset($body['invalid_parameters'])) {
33+
throw new Exception\Validation($errorMessage, $statusCode, null, $body['invalid_parameters']);
34+
}
35+
36+
// Otherwise throw a normal error
37+
throw new Exception\Request($errorMessage, $statusCode);
38+
}
39+
}

src/Client.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,12 @@ public static function signRequest(RequestInterface $request, SignatureSecret $c
187187

188188
public static function authRequest(RequestInterface $request, Basic $credentials)
189189
{
190-
switch($request->getHeaderLine('content-type')){
191-
case 'application/json':
192-
if (static::requiresAuthInUrlNotBody($request)) {
190+
switch($request->getHeaderLine('content-type')) {
191+
case 'application/json':
192+
if (static::requiresBasicAuth($request)) {
193+
$c = $credentials->asArray();
194+
$request = $request->withHeader('Authorization', 'Basic ' . base64_encode($c['api_key'] . ':' . $c['api_secret']));
195+
} else if (static::requiresAuthInUrlNotBody($request)) {
193196
$query = [];
194197
parse_str($request->getUri()->getQuery(), $query);
195198
$query = array_merge($query, $credentials->asArray());
@@ -455,6 +458,14 @@ public function __get($name)
455458
return $this->factory->getApi($name);
456459
}
457460

461+
protected static function requiresBasicAuth(\Psr\Http\Message\RequestInterface $request)
462+
{
463+
$path = $request->getUri()->getPath();
464+
$isSecretManagementEndpoint = strpos($path, '/accounts') === 0 && strpos($path, '/secrets') !== false;
465+
466+
return $isSecretManagementEndpoint;
467+
}
468+
458469
protected static function requiresAuthInUrlNotBody(\Psr\Http\Message\RequestInterface $request)
459470
{
460471
$path = $request->getUri()->getPath();

src/Client/Exception/Validation.php

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/**
3+
* Nexmo Client Library for PHP
4+
*
5+
* @copyright Copyright (c) 2016 Nexmo, Inc. (http://nexmo.com)
6+
* @license https://github.com/Nexmo/nexmo-php/blob/master/LICENSE.txt MIT License
7+
*/
8+
9+
namespace Nexmo\Client\Exception;
10+
11+
use Throwable;
12+
13+
class Validation extends Request
14+
{
15+
public function __construct($message = "", $code = 0, Throwable $previous = null, $errors)
16+
{
17+
$this->errors = $errors;
18+
parent::__construct($message, $code, $previous);
19+
}
20+
21+
public function getValidationErrors() {
22+
return $this->errors;
23+
}
24+
}

src/InvalidResponseException.php

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Nexmo;
4+
5+
class InvalidResponseException extends \Exception {
6+
7+
}

0 commit comments

Comments
 (0)