6 Commits
0.8 ... main

20 changed files with 1449 additions and 1089 deletions

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ cache
.cache
.config
.local
*.phar
*.phar
.composer

1
.phpunit.result.cache Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,17 @@
<?php
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
namespace CloudObjects\SDK;
interface CustomCacheAndLogInterface {
public function logInfoWithTime($message, $ts);
public function getFromCacheCustom($id);
public function putIntoCacheCustom($id, $data, $ttl);
}

View File

@@ -20,7 +20,7 @@ use CloudObjects\SDK\AccountGateway\AccountContext;
/**
* The ObjectRetriever provides access to objects on CloudObjects.
*/
class ObjectRetriever {
class ObjectRetriever implements CustomCacheAndLogInterface {
use LoggerAwareTrait;
@@ -107,19 +107,31 @@ class ObjectRetriever {
$this->client = new Client($options);
}
private function logInfoWithTime($message, $ts) {
public function logInfoWithTime($message, $ts) {
if (isset($this->logger))
$this->logger->info($message, [ 'elapsed_ms' => round((microtime(true) - $ts) * 1000) ]);
}
private function getCacheKey($id) {
return $this->options['cache_prefix'].$this->options['auth_ns'].'/'.$id;
}
private function getFromCache($id) {
return (isset($this->cache) && $this->cache->contains($this->options['cache_prefix'].$id))
? $this->cache->fetch($this->options['cache_prefix'].$id) : null;
return (isset($this->cache) && $this->cache->contains($this->getCacheKey($id)))
? $this->cache->fetch($this->getCacheKey($id)) : null;
}
private function putIntoCache($id, $data, $ttl) {
if (isset($this->cache))
$this->cache->save($this->options['cache_prefix'].$id, $data, $ttl);
$this->cache->save($this->getCacheKey($id), $data, $ttl);
}
public function getFromCacheCustom($id) {
return $this->getFromCache('custom/'.$id);
}
public function putIntoCacheCustom($id, $data, $ttl) {
$this->putIntoCache('custom/'.$id, $data, $ttl);
}
/**

View File

@@ -154,16 +154,19 @@ class APIClientFactory {
'timeout' => self::DEFAULT_TIMEOUT
];
if ($this->reader->hasProperty($api, 'wa:hasAuthorizationServer')) {
if ($this->reader->hasProperty($api, 'oauth2:hasAuthorizationServer')) {
// We have an authorization server for this endpoint/API
$authServerCoid = $this->reader->getFirstValueIRI($api, 'wa:hasAuthorizationServer');
$authServerCoid = $this->reader->getFirstValueIRI($api, 'oauth2:hasAuthorizationServer');
$authServerObject = $this->objectRetriever->getObject($authServerCoid);
if (!isset($authServer))
if (!isset($authServerObject))
throw new InvalidObjectConfigurationException("Authorization server object <"
. (string)$authServerCoid . "> not available.");
try {
$authServer = new OAuth2AuthServer($authServerObject);
$authServer = new OAuth2AuthServer($authServerObject, $this->objectRetriever);
} catch (InvalidObjectConfigurationException $e) {
throw new InvalidObjectConfigurationException("Authorization server object <"
. (string)$authServerCoid . "> could not be loaded; error: " . $e->getMessage());
} catch (Exception $e) {
throw new InvalidObjectConfigurationException("Authorization server object <"
. (string)$authServerCoid . "> could not be loaded. Its definition may be invalid.");

View File

@@ -9,63 +9,77 @@ namespace CloudObjects\SDK\WebAPI;
use Exception;
use ML\JsonLD\Node;
use GuzzleHttp\Client;
use Webmozart\Assert\Assert;
use CloudObjects\SDK\NodeReader;
use Webmozart\Assert\Assert,
Webmozart\Assert\InvalidArgumentException;
use CloudObjects\SDK\NodeReader,
CloudObjects\SDK\CustomCacheAndLogInterface;
use CloudObjects\SDK\Exceptions\InvalidObjectConfigurationException;
class OAuth2AuthServer {
private $reader;
private $authServer;
private $consumer;
private $cacheAndLog;
private $grantType;
private $clientId;
private $clientSecret;
public function __construct(Node $authServer) {
public function __construct(Node $authServer, CustomCacheAndLogInterface $cacheAndLog) {
$this->reader = new NodeReader([
'prefixes' => [
'oauth2' => 'coid://oauth2.co-n.net/'
]
]);
Assert::true($this->reader->hasProperty($authServer, 'oauth2:hasTokenEndpoint'),
"Authorization Server must have a token endpoint.");
Assert::startsWith($this->reader->getFirstValueString($authServer, 'oauth2:hasTokenEndpoint'),
"https://",
"Token endpoint must be an https:// URL.");
Assert::true($this->reader->hasProperty($authServer, 'oauth2:supportsGrantType'),
"Authorization Server must support at least one grant type.");
Assert::true($this->reader->hasProperty($this->authServer, 'oauth2:usesClientIDFrom'),
"Authorization Server must define client ID property.");
Assert::true($this->reader->hasProperty($this->authServer, 'oauth2:usesClientSecretFrom'),
"Authorization Server must define client secret property.");
try {
Assert::true($this->reader->hasProperty($authServer, 'oauth2:hasTokenEndpoint'),
"Authorization Server must have a token endpoint.");
Assert::startsWith($this->reader->getFirstValueString($authServer, 'oauth2:hasTokenEndpoint'),
"https://",
"Token endpoint must be an https:// URL.");
Assert::true($this->reader->hasProperty($authServer, 'oauth2:supportsGrantType'),
"Authorization Server must support at least one grant type.");
Assert::true($this->reader->hasProperty($authServer, 'oauth2:usesClientIDFrom'),
"Authorization Server must define client ID property.");
Assert::true($this->reader->hasProperty($authServer, 'oauth2:usesClientSecretFrom'),
"Authorization Server must define client secret property.");
} catch (InvalidArgumentException $e) {
throw new InvalidObjectConfigurationException($e->getMessage());
}
$this->authServer = $authServer;
}
private function assertClientCredentialPropertiesExist() : void {
$this->cacheAndLog = $cacheAndLog;
}
public function configureConsumer(Node $consumer) : void {
$this->assertClientCredentialPropertiesExist();
$clientIDProperty = $this->reader->getFirstValueString($this->authServer,
'oauth2:usesClientIDFrom');
$clientSecretProperty = $this->reader->getFirstValueString($this->authServer,
'oauth2:usesClientSecretFrom');
try {
Assert::notNull($this->authServer, "Object wasn't initialized correctly.");
Assert::notNull($this->cacheAndLog, "Object wasn't initialized correctly.");
$clientIDProperty = $this->reader->getFirstValueString($this->authServer,
'oauth2:usesClientIDFrom');
$clientSecretProperty = $this->reader->getFirstValueString($this->authServer,
'oauth2:usesClientSecretFrom');
Assert::true($this->reader->hasProperty($consumer, $clientIDProperty),
"Namespace must have Client ID");
Assert::true($this->reader->hasProperty($consumer, $clientSecretProperty),
"Namespace must have Client Secret");
} catch (InvalidArgumentException $e) {
throw new InvalidObjectConfigurationException($e->getMessage());
}
Assert::true($this->reader->hasProperty($consumer, $clientIDProperty),
"Namespace must have Client ID");
Assert::true($this->reader->hasProperty($consumer, $clientSecretProperty),
"Namespace must have Client Secret");
if ($this->reader->hasPropertyValue($this->authServer,
'oauth2:supportsGrantType', 'oauth2:ClientCredentials')) {
'oauth2:supportsGrantType', 'oauth2:ClientCredentials'))
{
// No additional conditions for "client_credentials" flow
$this->grantType = 'client_credentials';
} else {
throw new Exception("No flow/grant_type found.");
throw new InvalidObjectConfigurationException("No flow/grant_type found.");
}
$this->consumer = $consumer;
@@ -74,10 +88,18 @@ class OAuth2AuthServer {
}
public function getAccessToken() {
Assert::notNull($this->consumer, "Missing consumer.");
Assert::notNull($this->grantType, "Missing grant_type.");
Assert::notNull($this->clientId, "Missing client_id.");
Assert::notNull($this->clientSecret, "Missing client_secret.");
try {
Assert::notNull($this->authServer, "Object wasn't initialized correctly.");
Assert::notNull($this->cacheAndLog, "Object wasn't initialized correctly.");
Assert::notNull($this->consumer, "Missing consumer.");
Assert::notNull($this->grantType, "Missing grant_type.");
Assert::notNull($this->clientId, "Missing client_id.");
Assert::notNull($this->clientSecret, "Missing client_secret.");
} catch (InvalidArgumentException $e) {
throw new InvalidObjectConfigurationException($e->getMessage());
}
$client = new Client;
$tokenEndpointUrl = $this->reader->getFirstValueString($this->authServer, 'oauth2:hasTokenEndpoint');
@@ -90,15 +112,31 @@ class OAuth2AuthServer {
switch ($this->grantType) {
case "client_credentials":
// no additional params needed
break;
default:
throw new Exception("No flow/grant_type found.");
}
$tokenResponse = json_decode($client->post($tokenEndpointUrl, [
'form_params' => $params
])->getBody(true));
$grantCacheKey = sha1(json_encode($params));
Assert::keyExists($tokenResponse, 'access_token');
$ts = microtime(true);
$tokenResponse = json_decode($this->cacheAndLog->getFromCacheCustom($grantCacheKey), true);
if (isset($tokenResponse)) {
$this->cacheAndLog->logInfoWithTime("Reused access token for <".$this->authServer->getId()."> from cache.", $ts);
} else {
// Nothing cached, fetch from server
$tokenResponse = json_decode($client->post($tokenEndpointUrl, [
'form_params' => $params
])->getBody(true), true);
Assert::keyExists($tokenResponse, 'access_token');
$expiry = isset($tokenResponse['expires_in']) ? $tokenResponse['expires_in'] : 84600;
$this->cacheAndLog->logInfoWithTime("Retrieved access token for <".$this->authServer->getId()."> from token endpoint and will cache for ".$expiry." seconds.", $ts);
$this->cacheAndLog->putIntoCacheCustom($grantCacheKey, json_encode($tokenResponse), $expiry);
}
return $tokenResponse['access_token'];
}

View File

@@ -2,7 +2,7 @@
"name": "cloudobjects/sdk",
"description": "CloudObjects SDK for PHP for working with COIDs and object descriptions from CloudObjects.",
"keywords": ["cloudobjects", "sdk"],
"homepage": "https://github.com/CloudObjects/CloudObjects-PHP-SDK",
"homepage": "https://codeberg.org/CloudObjects/CloudObjects-PHP-SDK",
"license": "MPL-2.0",
"require" : {
"ml/json-ld": ">=1.0.7",
@@ -10,7 +10,7 @@
"doctrine/cache" : "1.*",
"guzzlehttp/guzzle" : ">=6.0",
"psr/log": ">=1.1",
"kevinrob/guzzle-cache-middleware": "^3.2",
"kevinrob/guzzle-cache-middleware": "^7.0.0",
"webmozart/assert": "^1.6"
},
"authors": [
@@ -24,16 +24,16 @@
}
},
"require-dev" : {
"phpunit/phpunit": ">=4.8.0,<5.0",
"phpunit/phpunit": "^10",
"symfony/http-foundation" : ">=4.0",
"symfony/psr-http-message-bridge" : ">=1.1.0",
"nyholm/psr7" : "~1.5.1",
"defuse/php-encryption" : "^2.2"
},
"suggest" : {
"symfony/http-foundation" : "Required to use parseSymfonyRequest() in AccountContext.",
"symfony/psr-http-message-bridge" : "Required to use parseSymfonyRequest() in AccountContext.",
"nyholm/psr7" : "Required to use parseSymfonyRequest() in AccountContext.",
"symfony/http-foundation" : "Required to use fromSymfonyRequest() in AccountContext.",
"symfony/psr-http-message-bridge" : "Required to use fromSymfonyRequest() in AccountContext.",
"nyholm/psr7" : "Required to use fromSymfonyRequest() in AccountContext.",
"defuse/php-encryption": "Required to use CryptoHelper"
}
}

2288
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="./tests/bootstrap.php">
<testsuites>
<testsuite name="OfflineTests">

5
run-docker.sh Normal file
View File

@@ -0,0 +1,5 @@
docker run -d -v .:/root --name cloudobjects-sdk-test cloudobjects/php-build-base:8.3
docker exec cloudobjects-sdk-test bash -c "cd /root && composer install"
docker exec cloudobjects-sdk-test bash -c "cd /root && vendor/bin/phpunit"
docker stop cloudobjects-sdk-test
docker rm cloudobjects-sdk-test

View File

@@ -8,7 +8,7 @@ namespace CloudObjects\SDK\AccountGateway;
use ML\IRI\IRI;
class AAUIDParserTest extends \PHPUnit_Framework_TestCase {
class AAUIDParserTest extends \PHPUnit\Framework\TestCase {
public function testValidAccountAAUID() {
$aauid = new IRI('aauid:abcd1234abcd1234');

View File

@@ -9,7 +9,7 @@ namespace CloudObjects\SDK\AccountGateway;
use GuzzleHttp\Psr7\Request as GuzzlePsrRequest;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class AccountContextParseTest extends \PHPUnit_Framework_TestCase {
class AccountContextParseTest extends \PHPUnit\Framework\TestCase {
public function testParsePsrRequest() {
$request = new GuzzlePsrRequest('GET', '/', [

View File

@@ -8,11 +8,11 @@ namespace CloudObjects\SDK\AccountGateway;
use ML\IRI\IRI;
class AccountContextTest extends \PHPUnit_Framework_TestCase {
class AccountContextTest extends \PHPUnit\Framework\TestCase {
private $context;
protected function setUp() {
protected function setUp(): void {
$this->context = new AccountContext(new IRI('aauid:aaaabbbbccccdddd'), 'DUMMY');
}

View File

@@ -8,7 +8,7 @@ namespace CloudObjects\SDK;
use ML\IRI\IRI;
class COIDParserTest extends \PHPUnit_Framework_TestCase {
class COIDParserTest extends \PHPUnit\Framework\TestCase {
public function testRootCOID() {
$coid = new IRI('coid://example.com');

View File

@@ -11,7 +11,7 @@ use GuzzleHttp\Client, GuzzleHttp\Handler\MockHandler,
GuzzleHttp\HandlerStack, GuzzleHttp\Psr7\Response;
use CloudObjects\SDK\ObjectRetriever;
class CryptoHelperTest extends \PHPUnit_Framework_TestCase {
class CryptoHelperTest extends \PHPUnit\Framework\TestCase {
private $retriever;
private $graph;
@@ -22,7 +22,7 @@ class CryptoHelperTest extends \PHPUnit_Framework_TestCase {
$this->retriever->setClient(new Client(['handler' => $handler]));
}
public function setUp() {
protected function setUp(): void {
$this->retriever = new ObjectRetriever([
'auth_ns' => 'test.cloudobjects.io',
'auth_secret' => 'TEST'

View File

@@ -10,12 +10,12 @@ use InvalidArgumentException;
use ML\JsonLD\JsonLD;
use CloudObjects\SDK\ObjectRetriever;
class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
class SchemaValidatorTest extends \PHPUnit\Framework\TestCase {
private $schemaValidator;
private $graph;
public function setUp() {
protected function setUp(): void {
$this->schemaValidator = new SchemaValidator(new ObjectRetriever);
$this->graph = JsonLD::getDocument('{}')->getGraph();
}
@@ -24,10 +24,11 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/String'));
$this->schemaValidator->validateAgainstNode("Test", $node);
$this->addToAssertionCount(1);
}
public function testNotString() {
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/String'));
@@ -38,10 +39,11 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Number'));
$this->schemaValidator->validateAgainstNode(3.5, $node);
$this->addToAssertionCount(1);
}
public function testNotNumber() {
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Number'));
@@ -52,10 +54,11 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Integer'));
$this->schemaValidator->validateAgainstNode(12, $node);
$this->addToAssertionCount(1);
}
public function testNotInteger() {
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Integer'));
@@ -66,10 +69,11 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Array'));
$this->schemaValidator->validateAgainstNode([ 1, 2, "foo" ], $node);
$this->addToAssertionCount(1);
}
public function testNotArray() {
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Array'));
@@ -83,10 +87,11 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
'a' => 'A',
'b' => 'B'
], $node);
$this->addToAssertionCount(1);
}
public function testNotObject() {
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Object'));
@@ -105,10 +110,11 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
'a' => 'A',
'b' => 'B'
], $node);
$this->addToAssertionCount(1);
}
public function testObjectWithPropertyTypeError() {
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$stringNode = $this->graph->createNode();
$stringNode->setProperty('coid://json.co-n.net/hasKey', 'a');
@@ -136,10 +142,11 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
'a' => 'A',
'b' => 'B'
], $node);
$this->addToAssertionCount(1);
}
public function testObjectWithRequiredPropertyTypeError() {
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$stringNode = $this->graph->createNode();
$stringNode->setProperty('coid://json.co-n.net/hasKey', 'a');
@@ -156,7 +163,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
}
public function testObjectWithRequiredPropertyMissing() {
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$stringNode = $this->graph->createNode();
$stringNode->setProperty('coid://json.co-n.net/hasKey', 'a');

View File

@@ -10,7 +10,7 @@ use ML\IRI\IRI;
use GuzzleHttp\Client, GuzzleHttp\Handler\MockHandler,
GuzzleHttp\HandlerStack, GuzzleHttp\Psr7\Response;
class NodeReaderMockTest extends \PHPUnit_Framework_TestCase {
class NodeReaderMockTest extends \PHPUnit\Framework\TestCase {
private $retriever;
private $reader;
@@ -27,7 +27,7 @@ class NodeReaderMockTest extends \PHPUnit_Framework_TestCase {
'{"@context":{"co":"coid:\/\/cloudobjects.io\/","rdf":"http:\/\/www.w3.org\/1999\/02\/22-rdf-syntax-ns#","agws":"coid:\/\/aauid.net\/","rdfs":"http:\/\/www.w3.org\/2000\/01\/rdf-schema#"},"@id":"coid:\/\/cloudobjects.io","@type":["agws:Service","co:Namespace"],"co:isAtRevision":"6-fbea0c90b2c5e5300e4039ed99be9b2d","co:isVisibleTo":{"@id":"co:Public"},"co:recommendsPrefix":"co","co:wasUpdatedAt":{"@type":"http:\/\/www.w3.org\/2001\/XMLSchema#dateTime","@value":"2017-01-16T17:29:22+00:00"},"rdfs:comment":"The CloudObjects namespace defines the essential objects.","rdfs:label":"CloudObjects"}'));
}
protected function setUp() {
protected function setUp(): void {
$this->retriever = new ObjectRetriever;
$this->reader = new NodeReader([
'prefixes' => [

View File

@@ -10,7 +10,7 @@ use ML\IRI\IRI;
use GuzzleHttp\Client, GuzzleHttp\Handler\MockHandler,
GuzzleHttp\HandlerStack, GuzzleHttp\Psr7\Response;
class ObjectRetrieverMockTest extends \PHPUnit_Framework_TestCase {
class ObjectRetrieverMockTest extends \PHPUnit\Framework\TestCase {
private $retriever;
@@ -20,7 +20,7 @@ class ObjectRetrieverMockTest extends \PHPUnit_Framework_TestCase {
$this->retriever->setClient(new Client(['handler' => $handler]));
}
protected function setUp() {
protected function setUp(): void {
$this->retriever = new ObjectRetriever;
}

View File

@@ -10,11 +10,11 @@ use InvalidArgumentException;
use ML\IRI\IRI;
use CloudObjects\SDK\ObjectRetriever;
class SchemaValidatorPublicTest extends \PHPUnit_Framework_TestCase {
class SchemaValidatorPublicTest extends \PHPUnit\Framework\TestCase {
private $schemaValidator;
public function setUp() {
protected function setUp(): void {
$this->schemaValidator = new SchemaValidator(new ObjectRetriever);
}
@@ -24,10 +24,11 @@ class SchemaValidatorPublicTest extends \PHPUnit_Framework_TestCase {
'region' => 'Hessen',
'country-name' => 'Germany'
], new IRI('coid://json.co-n.net/Address'));
$this->addToAssertionCount(1);
}
public function testNotAddress() {
$this->setExpectedException(InvalidArgumentException::class);
$this->expectException(InvalidArgumentException::class);
$this->schemaValidator->validateAgainstCOID([
'region' => 'Hessen',

View File

@@ -8,11 +8,11 @@ namespace CloudObjects\SDK;
use ML\IRI\IRI;
class ObjectRetrieverTest extends \PHPUnit_Framework_TestCase {
class ObjectRetrieverPublicTest extends \PHPUnit\Framework\TestCase {
private $retriever;
protected function setUp() {
protected function setUp(): void {
$this->retriever = new ObjectRetriever;
}