Skip to content

Commit c0d9096

Browse files
authored
Merge pull request #1545 from keboola/vb/DMD-266-workspace-credentials
2 parents c37dd7c + 88c2cd8 commit c0d9096

4 files changed

Lines changed: 265 additions & 1 deletion

File tree

apiary.apib

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3047,7 +3047,6 @@ Further information can be found in the [Developers Documentation](https://devel
30473047
}
30483048
}
30493049
3050-
30513050
## Data Preview [/v2/storage/tables/{table_id}/data-preview/]
30523051
### Data preview [GET]
30533052
@@ -5661,6 +5660,121 @@ query, non-SELECT query and failed query.
56615660
"message": "An exception occurred while executing a query: SQL compilation error: error line 1 at position 7 invalid identifier 'BIRTHDATE'"
56625661
}
56635662
5663+
5664+
## Workspace credentials [/v2/storage/workspaces/{workspaceId}/credentials]
5665+
5666+
Manage dedicated workspace credentials to be used in Query Service
5667+
### Create credentials [POST]
5668+
5669+
+ Request (application/json)
5670+
+ Headers
5671+
5672+
X-StorageApi-Token: your_token
5673+
5674+
+ Body
5675+
5676+
{
5677+
}
5678+
5679+
+ Response 200 (application/json)
5680+
5681+
{
5682+
"id": 254,
5683+
"type": "table",
5684+
"name": "WORKSPACE_123",
5685+
"component": null,
5686+
"configurationId": null,
5687+
"created": "2025-08-13T15:03:18+0200",
5688+
"connection": {
5689+
"backend": "snowflake",
5690+
"region": "eu-central-1",
5691+
"host": "kebooladev.snowflakecomputing.com",
5692+
"database": "DB_123",
5693+
"schema": "WORKSPACE_123",
5694+
"warehouse": "DEV",
5695+
"user": "JSWORKSPACE_123_QS",
5696+
"loginType": "snowflake-service-keypair",
5697+
"ssoLoginAvailable": false,
5698+
"account": "kebooladev",
5699+
"role": "JSWORKSPACE_123",
5700+
"privateKey": "-----BEGIN PRIVATE KEY--....",
5701+
"credentials": {
5702+
"id": 356
5703+
}
5704+
},
5705+
"backendSize": "small",
5706+
"statementTimeoutSeconds": 0,
5707+
"creatorToken": {
5708+
"id": 1,
5709+
"description": "token"
5710+
},
5711+
"readOnlyStorageAccess": true,
5712+
"platformUsageType": "user"
5713+
}
5714+
5715+
5716+
5717+
Create new or fetch existing credentials for dedicated workspace.
5718+
5719+
### Credentials detail [GET /v2/storage/workspaces/{workspaceId}/credentials/{id}]
5720+
5721+
+ Request (application/json)
5722+
+ Headers
5723+
5724+
X-StorageApi-Token: your_token
5725+
5726+
+ Response 204 (application/json)
5727+
5728+
{
5729+
"id": 254,
5730+
"type": "table",
5731+
"name": "WORKSPACE_123",
5732+
"component": null,
5733+
"configurationId": null,
5734+
"created": "2025-08-13T15:03:18+0200",
5735+
"connection": {
5736+
"backend": "snowflake",
5737+
"region": "eu-central-1",
5738+
"host": "kebooladev.snowflakecomputing.com",
5739+
"database": "DB_123",
5740+
"schema": "WORKSPACE_123",
5741+
"warehouse": "DEV",
5742+
"user": "JSWORKSPACE_123_QS",
5743+
"loginType": "snowflake-service-keypair",
5744+
"ssoLoginAvailable": false,
5745+
"account": "kebooladev",
5746+
"role": "JSWORKSPACE_123",
5747+
"privateKey": "-----BEGIN PRIVATE KEY--....",
5748+
"credentials": {
5749+
"id": 356
5750+
}
5751+
},
5752+
"backendSize": "small",
5753+
"statementTimeoutSeconds": 0,
5754+
"creatorToken": {
5755+
"id": 1,
5756+
"description": "token"
5757+
},
5758+
"readOnlyStorageAccess": true,
5759+
"platformUsageType": "user"
5760+
}
5761+
5762+
Get details of specified workspace credentials
5763+
5764+
### Delete credentials [DELETE /v2/storage/workspaces/{workspaceId}/credentials/{id}]
5765+
5766+
Delete specified workspace credentials. User will be invalidated
5767+
5768+
5769+
+ Request (application/json)
5770+
+ Headers
5771+
5772+
X-StorageApi-Token: your_token
5773+
5774+
+ Response 204 (application/json)
5775+
5776+
5777+
56645778
## Data Structures
56655779
56665780
### Workspace

phpunit.xml.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
<file>tests/Backend/Workspaces/WorkspacesUnloadTest.php</file>
152152
<file>tests/Backend/Workspaces/WorkspacesRenameLoadTest.php</file>
153153
<file>tests/Backend/Workspaces/WorkspacesTest.php</file>
154+
<file>tests/Backend/Workspaces/WorkspacesCredentialsTest.php</file>
154155
</testsuite>
155156

156157
<testsuite name="backend-mixed">
@@ -263,6 +264,7 @@
263264
<!-- Workspaces-->
264265
<file>tests/Backend/Workspaces/WorkspacesTest.php</file>
265266
<file>tests/Backend/Workspaces/BigqueryWorkspacesUnloadTest.php</file>
267+
<file>tests/Backend/Workspaces/WorkspacesCredentialsTest.php</file>
266268
<!-- External bucket -->
267269
<file>tests/Backend/ExternalBuckets/BigqueryRegisterBucketTest.php</file>
268270
</testsuite>

src/Keboola/StorageApi/Workspaces.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,23 @@ public function resetCredentials(
310310
$loginType->value,
311311
));
312312
}
313+
314+
public function createCredentials(int|string $workspaceId): array
315+
{
316+
$result = $this->client->apiPostJson("workspaces/{$workspaceId}/credentials");
317+
assert(is_array($result));
318+
return $result;
319+
}
320+
321+
public function getCredentials(int|string $workspaceId, int|string $credentialsId): array
322+
{
323+
$result = $this->client->apiGet("workspaces/{$workspaceId}/credentials/{$credentialsId}");
324+
assert(is_array($result));
325+
return $result;
326+
}
327+
328+
public function deleteCredentials(int|string $workspaceId, int|string $credentialsId): void
329+
{
330+
$this->client->apiDelete("workspaces/{$workspaceId}/credentials/{$credentialsId}");
331+
}
313332
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Keboola\Test\Backend\Workspaces;
6+
7+
use Exception;
8+
use Keboola\StorageApi\BranchAwareClient;
9+
use Keboola\StorageApi\Workspaces;
10+
use Keboola\TableBackendUtils\Escaping\Snowflake\SnowflakeQuote;
11+
use Keboola\Test\Backend\WorkspaceConnectionTrait;
12+
use Keboola\Test\Backend\Workspaces\Backend\WorkspaceBackendFactory;
13+
use Keboola\Test\ClientProvider\ClientProvider;
14+
use Keboola\Test\Utils\EventsQueryBuilder;
15+
16+
class WorkspacesCredentialsTest extends ParallelWorkspacesTestCase
17+
{
18+
use WorkspaceConnectionTrait;
19+
20+
public function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$this->allowTestForBackendsOnly([
25+
'snowflake',
26+
'bigquery',
27+
], 'Workspace credentials are implemented only for Snowflake and BigQuery');
28+
}
29+
30+
public function testConnectByCredentials(): void
31+
{
32+
$this->skipTestForBackend(['bigquery'], 'BigQuery is WIP');
33+
34+
// create workspace and source table in workspace
35+
$workspace = $this->initTestWorkspace(forceRecreate: true);
36+
37+
// connect to workspace after creation (we have credentials in response)
38+
$backend = WorkspaceBackendFactory::createWorkspaceBackend($workspace);
39+
40+
// create dummy table to check it later by
41+
$backend->dropTableIfExists('test_Languages');
42+
$backend->createTable('test_Languages', [
43+
'id' => 'integer',
44+
'name' => 'string',
45+
]);
46+
if ($workspace['connection']['backend'] === 'snowflake') {
47+
$backend->executeQuery(sprintf(
48+
'INSERT INTO %s (%s, %s) VALUES (1, \'cz\'), (2, \'en\');',
49+
SnowflakeQuote::createQuotedIdentifierFromParts([
50+
$workspace['connection']['schema'],
51+
'test_Languages',
52+
]),
53+
SnowflakeQuote::quoteSingleIdentifier('id'),
54+
SnowflakeQuote::quoteSingleIdentifier('name'),
55+
));
56+
} else {
57+
// BigQuery
58+
$backend->executeQuery(sprintf(
59+
'INSERT INTO %s.`test_Languages` (`id`, `name`) VALUES (1, \'cz\'), (2, \'en\');',
60+
$workspace['connection']['schema'],
61+
));
62+
}
63+
64+
$workspaces = new Workspaces($this->workspaceSapiClient);
65+
66+
$this->initEvents($this->workspaceSapiClient);
67+
// credentials creation
68+
$assertCallback = function ($events) {
69+
$this->assertCount(1, $events);
70+
};
71+
72+
$query = new EventsQueryBuilder();
73+
$query->setEvent('storage.workspaceCredentialsCreated')->setTokenId($this->tokenId);
74+
75+
$workspaceCredentials = $workspaces->createCredentials($workspace['id']);
76+
$this->assertEventWithRetries($this->_client, $assertCallback, $query);
77+
$workspaceBackend = WorkspaceBackendFactory::createWorkspaceBackend($workspaceCredentials, true);
78+
79+
$dbResult = $workspaceBackend->fetchAll('test_Languages');
80+
81+
$this->assertEquals(
82+
[
83+
0 => [1, 'cz'],
84+
1 => [2, 'en'],
85+
],
86+
$dbResult,
87+
);
88+
89+
// should just return the same credentials
90+
$query = new EventsQueryBuilder();
91+
$query->setEvent('storage.workspaceCredentialsRetrieved')->setTokenId($this->tokenId);
92+
93+
$retrievedCredentials = $workspaces->createCredentials($workspace['id']);
94+
$this->assertEventWithRetries($this->_client, $assertCallback, $query);
95+
$this->assertEquals($workspaceCredentials['connection']['privateKey'], $retrievedCredentials['connection']['privateKey']);
96+
97+
// credential detail is working and returning a correct object
98+
$credentialsId = $workspaceCredentials['connection']['credentials']['id'];
99+
100+
$query = new EventsQueryBuilder();
101+
$query->setEvent('storage.workspaceCredentialsDetail')->setTokenId($this->tokenId);
102+
$workspaceCredentialsRefreshed = $workspaces->getCredentials($workspace['id'], $credentialsId);
103+
$this->assertEventWithRetries($this->_client, $assertCallback, $query);
104+
$this->assertEquals($workspaceCredentials['connection']['privateKey'], $workspaceCredentialsRefreshed['connection']['privateKey']);
105+
$workspaceBackendRefreshed = WOrkspaceBackendFactory::createWorkspaceBackend($workspaceCredentialsRefreshed, true);
106+
107+
$dbResultRefreshed = $workspaceBackendRefreshed->fetchAll('test_Languages');
108+
109+
$this->assertEquals(
110+
[
111+
0 => [1, 'cz'],
112+
1 => [2, 'en'],
113+
],
114+
$dbResultRefreshed,
115+
);
116+
117+
$query = new EventsQueryBuilder();
118+
$query->setEvent('storage.workspaceCredentialsDeleted')->setTokenId($this->tokenId);
119+
$workspaces->deleteCredentials($workspace['id'], $credentialsId);
120+
$this->assertEventWithRetries($this->_client, $assertCallback, $query);
121+
122+
try {
123+
WorkspaceBackendFactory::createWorkspaceBackend($workspaceCredentialsRefreshed, true);
124+
$this->fail('Expected exception to be thrown.');
125+
} catch (Exception $e) {
126+
$this->assertTrue(str_contains($e->getMessage(), 'JWT token is invalid.'));
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)