Skip to content

Commit

Permalink
Add RegisterAccess (#39)
Browse files Browse the repository at this point in the history
Add new bindings to register access for the S3-compatible gateway and to
construct a linksharing URL.

The RegisterAccess happy flow test isn't currently run in CI because there is no
auth service in storj-sim.

Solves #37
  • Loading branch information
Erikvv authored Oct 4, 2022
1 parent 63f984b commit a8e9225
Show file tree
Hide file tree
Showing 12 changed files with 371 additions and 1 deletion.
8 changes: 8 additions & 0 deletions src/Access.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,12 @@ public function overrideEncryptionKey(string $bucket, string $prefix, Encryption
$scope = Scope::exit(fn() => $this->ffi->uplink_free_error($pError));
Util::throwIfError($pError);
}

/**
* @internal
*/
public function getNativeAccess(): FFI\CData
{
return $this->cAccess;
}
}
65 changes: 65 additions & 0 deletions src/Edge/Config.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace Storj\Uplink\Edge;

use FFI;
use Storj\Uplink\Internal\Scope;
use Storj\Uplink\Internal\Util;

/**
* Parameters when connecting to edge services
*/
class Config
{
/**
* DRPC server e.g. auth.storjshare.io:7777.
* Currently mandatory to set this manually.
*/
private string $authServiceAddress = "";

/**
* Root certificate(s) or chain(s) against which Uplink checks the auth service.
* In PEM format.
* Intended to test against a self-hosted auth service or to improve security.
*/
private string $certificatePem = "";

public function withAuthServiceAddress(string $authServiceAddress): self
{
$self = clone $this;
$self->authServiceAddress = $authServiceAddress;
return $self;
}

public function withCertificatePem(string $certificatePem): self
{
$self = clone $this;
$self->certificatePem = $certificatePem;
return $self;
}

public function getAuthServiceAddress(): string
{
return $this->authServiceAddress;
}

public function getCertificatePem(): string
{
return $this->certificatePem;
}

/**
* @internal
*/
public function toCStruct(FFI $ffi, Scope $scope): FFI\CData
{
$cAuthServiceAddress = Util::createCString($this->authServiceAddress, $scope);
$cCertificatePem = Util::createCString($this->certificatePem, $scope);

$cConfig = $ffi->new('EdgeConfig');
$cConfig->auth_service_address = $cAuthServiceAddress;
$cConfig->certificate_pem = $cCertificatePem;

return $cConfig;
}
}
46 changes: 46 additions & 0 deletions src/Edge/Credentials.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Storj\Uplink\Edge;

/**
* Gateway credentials in S3 format
*/
class Credentials
{
/**
* Is also used in the linksharing url path
*/
private string $accessKeyId;

private string $secretKey;

/**
* Base HTTP(S) URL to the gateway
*/
private string $endpoint;

public function __construct(
string $accessKeyId,
string $secretKey,
string $endpoint
) {
$this->accessKeyId = $accessKeyId;
$this->secretKey = $secretKey;
$this->endpoint = $endpoint;
}

public function getAccessKeyId(): string
{
return $this->accessKeyId;
}

public function getSecretKey(): string
{
return $this->secretKey;
}

public function getEndpoint(): string
{
return $this->endpoint;
}
}
112 changes: 112 additions & 0 deletions src/Edge/Edge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace Storj\Uplink\Edge;

use FFI;
use Storj\Uplink\Access;
use Storj\Uplink\Exception\Edge\DialFailed;
use Storj\Uplink\Exception\Edge\RegisterAccessFailed;
use Storj\Uplink\Exception\UplinkException;
use Storj\Uplink\Internal\Scope;
use Storj\Uplink\Internal\Util;
use Storj\Uplink\Permission;

class Edge
{
/**
* With libuplink.so and header files loaded
*/
private FFI $ffi;

public function __construct(FFI $ffi)
{
$this->ffi = $ffi;
}

/**
* Get credentials for the Storj-hosted Gateway and linkshare service.
*
* All files accessible under the Access are then also accessible via those services.
*
* If you call this function a lot, and the use case allows it,
* please limit the lifetime of the credentials
* by setting @see Permission::$notAfter when creating the Access.
*
* @throws DialFailed in case of network errors
* @throws RegisterAccessFailed in case of server errors
*/
public function registerAccess(
Config $config,
Access $access,
bool $isPublic = false
): Credentials
{
$scope = new Scope();

$cOptions = $this->ffi->new('EdgeRegisterAccessOptions');
$cOptions->is_public = $isPublic;

$cCredentialsResult = $this->ffi->edge_register_access(
$config->toCStruct($this->ffi, $scope),
$access->getNativeAccess(),
FFI::addr($cOptions),
);

$scope->onExit(
fn() => $this->ffi->edge_free_credentials_result($cCredentialsResult)
);

Util::throwIfErrorResult($cCredentialsResult);

$cCredentials = $cCredentialsResult->credentials;

return new Credentials(
FFI::string($cCredentials->access_key_id),
FFI::string($cCredentials->secret_key),
FFI::string($cCredentials->endpoint),
);
}

/**
* JoinShareURL creates a linksharing URL from parts.
* The existence or accessibility of the target is not checked, it might not exist or be inaccessible.
*
* @param string $baseUrl Linksharing service, e.g. https://link.storjshare.io
* @param string $accessKeyId Can be obtained by calling RegisterAccess. It must be associated with public visibility.
* @param string $bucket Optional, leave it blank to share the entire project.
* @param string $key Optional, if empty shares the entire bucket. It can also be a prefix, in which case it must end with a "/".
* @param bool $raw Whether to get a direct link to the data instead of a landing page
*
* @return string example https://link.storjshare.io/s/l5pucy3dmvzxgs3fpfewix27l5pq/mybucket/myprefix/myobject
*
* @throws UplinkException
*/
public function joinShareUrl(
string $baseUrl,
string $accessKeyId,
string $bucket = "",
string $key = "",
bool $raw = false
): string
{
$cOptions = $this->ffi->new('EdgeShareURLOptions');
$cOptions->raw = $raw;

$scope = new Scope();
$cStringResult = $this->ffi->edge_join_share_url(
$baseUrl,
$accessKeyId,
$bucket,
$key,
FFI::addr($cOptions)
);

$scope->onExit(
fn() => $this->ffi->uplink_free_string_result($cStringResult)
);

Util::throwIfErrorResult($cStringResult);

return FFI::string($cStringResult->string);
}
}
8 changes: 8 additions & 0 deletions src/Exception/Edge/DialFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Storj\Uplink\Exception\Edge;


class DialFailed extends EdgeException
{
}
9 changes: 9 additions & 0 deletions src/Exception/Edge/EdgeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Storj\Uplink\Exception\Edge;

use Storj\Uplink\Exception\UplinkException;

abstract class EdgeException extends UplinkException
{
}
9 changes: 9 additions & 0 deletions src/Exception/Edge/RegisterAccessFailed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Storj\Uplink\Exception\Edge;

use Storj\Uplink\Exception\UplinkException;

class RegisterAccessFailed extends EdgeException
{
}
3 changes: 3 additions & 0 deletions src/Exception/UplinkException.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ abstract class UplinkException extends Exception
0x20 => Object\InvalidObjectKey::class,
0x21 => Object\ObjectNotFound::class,
0x22 => Object\UploadDone::class,

0x30 => Edge\DialFailed::class,
0x31 => Edge\RegisterAccessFailed::class,
];

/**
Expand Down
5 changes: 4 additions & 1 deletion src/SharePrefix.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ public function getPrefix(): string
* @param FFI $ffi
* @param SharePrefix[] $sharePrefixes
*/
public static function toCStructArray(FFI $ffi, array $sharePrefixes, Scope $scope): FFI\CData
public static function toCStructArray(FFI $ffi, array $sharePrefixes, Scope $scope): ?FFI\CData
{
$count = count($sharePrefixes);
if ($count === 0) {
return null;
}

$cSharePrefixesType = FFI::arrayType($ffi->type('UplinkSharePrefix'), [$count]);
$cSharePrefixes = $ffi->new($cSharePrefixesType);
Expand Down
6 changes: 6 additions & 0 deletions src/Uplink.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Storj\Uplink;

use FFI;
use Storj\Uplink\Edge\Edge;
use Storj\Uplink\Exception\UplinkException;
use Storj\Uplink\Internal\Scope;
use Storj\Uplink\Internal\Util;
Expand Down Expand Up @@ -170,4 +171,9 @@ public function deriveEncryptionKey(string $passphrase, string $salt): Encryptio
$scope
);
}

public function edgeServices(): Edge
{
return new Edge($this->ffi);
}
}
50 changes: 50 additions & 0 deletions test/Edge/RegisterAccessTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Storj\Uplink\Test\Edge;

use DateInterval;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use Storj\Uplink\Edge\Config;
use Storj\Uplink\Exception\Edge\DialFailed;
use Storj\Uplink\Permission;
use Storj\Uplink\Test\Util;

class RegisterAccessTest extends TestCase
{
public function testRegisterAccessHappyFlow(): void
{
$authService = getenv('AUTH_SERVICE_ADDRESS');
if (!$authService) {
$this->markTestSkipped('No auth service address set');
}

$edgeConfig = (new Config())->withAuthServiceAddress($authService);
$edge = Util::uplink()->edgeServices();

// set expiry so we don't pollute the Auth service prod datebase when running tests against prod
$tomorrow = (new DateTimeImmutable())->add(new DateInterval('P1D'));
$access = Util::access()->share(
Permission::readOnlyPermission()
->notAfter($tomorrow)
);

$credentials = $edge->registerAccess($edgeConfig, $access);

// just to check it isn't empty or garbage
self::assertMatchesRegularExpression('~\w{10,200}~', $credentials->getAccessKeyId());
self::assertMatchesRegularExpression('~\w{10,200}~', $credentials->getSecretKey());
self::assertMatchesRegularExpression('~https://[\w.]{10,200}~', $credentials->getEndpoint());
}

public function testRegisterAccessInvalidAddress(): void
{
// No DRPC auth service is running at this address.
$edgeConfig = (new Config())->withAuthServiceAddress('storj.io:33463');
$uplink = Util::uplink();
$edge = $uplink->edgeServices();

$this->expectException(DialFailed::class);
$edge->registerAccess($edgeConfig, Util::access());
}
}
Loading

0 comments on commit a8e9225

Please sign in to comment.