Skip to content

Commit 3eacd20

Browse files
feat(Spanner): Add FGAC Samples (GoogleCloudPlatform#1766)
1 parent f7b0dd1 commit 3eacd20

File tree

5 files changed

+299
-11
lines changed

5 files changed

+299
-11
lines changed

spanner/src/add_drop_database_role.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,22 +52,20 @@ function add_drop_database_role(string $instanceId, string $databaseId): void
5252
sprintf('GRANT ROLE %s TO ROLE %s', $roleParent, $roleChild)
5353
]);
5454

55-
printf('Waiting for create role and grant operation to complete... %s', PHP_EOL);
55+
printf('Waiting for create role and grant operation to complete...%s', PHP_EOL);
5656
$operation->pollUntilComplete();
5757

58-
printf('Created roles %s and %s and granted privileges %s', $roleParent, $roleChild, PHP_EOL);
58+
printf('Created roles %s and %s and granted privileges%s', $roleParent, $roleChild, PHP_EOL);
5959

6060
$operation = $database->updateDdlBatch([
6161
sprintf('REVOKE ROLE %s FROM ROLE %s', $roleParent, $roleChild),
62-
sprintf('DROP ROLE %s', $roleChild),
63-
sprintf('REVOKE SELECT ON TABLE Singers FROM ROLE %s', $roleParent),
64-
sprintf('DROP ROLE %s', $roleParent)
62+
sprintf('DROP ROLE %s', $roleChild)
6563
]);
6664

67-
printf('Waiting for revoke role and drop role operation to complete... %s', PHP_EOL);
65+
printf('Waiting for revoke role and drop role operation to complete...%s', PHP_EOL);
6866
$operation->pollUntilComplete();
6967

70-
printf('Revoked privileges and dropped roles %s and %s %s', $roleChild, $roleParent, PHP_EOL);
68+
printf('Revoked privileges and dropped role %s%s', $roleChild, PHP_EOL);
7169
}
7270
// [END spanner_add_and_drop_database_role]
7371

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
/**
3+
* Copyright 2023 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* For instructions on how to run the full sample:
20+
*
21+
* @see https://github.com/GoogleCloudPlatform/php-docs-samples/tree/main/spanner/README.md
22+
*/
23+
24+
namespace Google\Cloud\Samples\Spanner;
25+
26+
// [START spanner_enable_fine_grained_access]
27+
use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient;
28+
use \Google\Cloud\Iam\V1\Binding;
29+
use \Google\Type\Expr;
30+
31+
/**
32+
* Enable Fine Grained Access.
33+
* Example:
34+
* ```
35+
* enable_fine_grained_access($projectId, $instanceId, $databaseId, $iamMember, $databaseRole, $title);
36+
* ```
37+
*
38+
* @param string $projectId The Google cloud project ID
39+
* @param string $instanceId The Spanner instance ID.
40+
* @param string $databaseId The Spanner database ID.
41+
* @param string $iamMember The IAM member. Eg: `user:{emailid}`,
42+
* `serviceAccount:{emailid}`, `group:{emailid}`, `domain:{domain}`
43+
* @param string $databaseRole The database role bound to
44+
* the IAM member.
45+
* @param string $title Condition title.
46+
*/
47+
function enable_fine_grained_access(
48+
string $projectId,
49+
string $instanceId,
50+
string $databaseId,
51+
string $iamMember,
52+
string $databaseRole,
53+
string $title
54+
): void {
55+
$adminClient = new DatabaseAdminClient();
56+
$resource = sprintf('projects/%s/instances/%s/databases/%s', $projectId, $instanceId, $databaseId);
57+
$policy = $adminClient->getIamPolicy($resource);
58+
59+
// IAM conditions need at least version 3
60+
if ($policy->getVersion() != 3) {
61+
$policy->setVersion(3);
62+
}
63+
64+
$binding = new Binding([
65+
'role' => 'roles/spanner.fineGrainedAccessUser',
66+
'members' => [$iamMember],
67+
'condition' => new Expr([
68+
'title' => $title,
69+
'expression' => sprintf("resource.name.endsWith('/databaseRoles/%s')", $databaseRole)
70+
])
71+
]);
72+
$policy->setBindings([$binding]);
73+
$adminClient->setIamPolicy($resource, $policy);
74+
75+
printf('Enabled fine-grained access in IAM' . PHP_EOL);
76+
}
77+
// [END spanner_enable_fine_grained_access]
78+
79+
// The following 2 lines are only needed to run the samples
80+
require_once __DIR__ . '/../../testing/sample_helpers.php';
81+
\Google\Cloud\Samples\execute_sample(__FILE__, __NAMESPACE__, $argv);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
/**
3+
* Copyright 2023 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* For instructions on how to run the full sample:
20+
*
21+
* @see https://github.com/GoogleCloudPlatform/php-docs-samples/tree/main/spanner/README.md
22+
*/
23+
24+
namespace Google\Cloud\Samples\Spanner;
25+
26+
// [START spanner_list_database_roles]
27+
use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient;
28+
29+
/**
30+
* List Database roles in the given database.
31+
* Example:
32+
* ```
33+
* list_database_roles($projectId, $instanceId, $databaseId);
34+
* ```
35+
*
36+
* @param string $projectId The Google cloud project ID
37+
* @param string $instanceId The Spanner instance ID.
38+
* @param string $databaseId The Spanner database ID.
39+
*/
40+
function list_database_roles(
41+
string $projectId,
42+
string $instanceId,
43+
string $databaseId
44+
): void {
45+
$adminClient = new DatabaseAdminClient();
46+
$resource = sprintf('projects/%s/instances/%s/databases/%s', $projectId, $instanceId, $databaseId);
47+
48+
$roles = $adminClient->listDatabaseRoles($resource);
49+
printf('List of Database roles:' . PHP_EOL);
50+
foreach ($roles as $role) {
51+
printf($role->getName() . PHP_EOL);
52+
}
53+
}
54+
// [END spanner_list_database_roles]
55+
56+
// The following 2 lines are only needed to run the samples
57+
require_once __DIR__ . '/../../testing/sample_helpers.php';
58+
\Google\Cloud\Samples\execute_sample(__FILE__, __NAMESPACE__, $argv);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
/**
3+
* Copyright 2023 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* For instructions on how to run the full sample:
20+
*
21+
* @see https://github.com/GoogleCloudPlatform/php-docs-samples/tree/main/spanner/README.md
22+
*/
23+
24+
namespace Google\Cloud\Samples\Spanner;
25+
26+
// [START spanner_read_data_with_database_role]
27+
use Google\Cloud\Spanner\SpannerClient;
28+
29+
/**
30+
* Read database with a database role.
31+
* Example:
32+
* ```
33+
* read_data_with_database_role($instanceId, $databaseId);
34+
* ```
35+
*
36+
* @param string $instanceId The Spanner instance ID.
37+
* @param string $databaseId The Spanner database ID.
38+
*/
39+
function read_data_with_database_role(string $instanceId, string $databaseId): void
40+
{
41+
$spanner = new SpannerClient();
42+
$databaseRole = 'new_parent';
43+
$instance = $spanner->instance($instanceId);
44+
$database = $instance->database($databaseId, ['databaseRole' => $databaseRole]);
45+
$results = $database->execute('SELECT * FROM Singers');
46+
47+
foreach ($results as $row) {
48+
printf('SingerId: %s, Firstname: %s, LastName: %s' . PHP_EOL, $row['SingerId'], $row['FirstName'], $row['LastName']);
49+
}
50+
}
51+
// [END spanner_read_data_with_database_role]
52+
53+
// The following 2 lines are only needed to run the samples
54+
require_once __DIR__ . '/../../testing/sample_helpers.php';
55+
\Google\Cloud\Samples\execute_sample(__FILE__, __NAMESPACE__, $argv);

spanner/test/spannerTest.php

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
use Google\Cloud\TestUtils\TestTrait;
2525
use PHPUnitRetry\RetryTrait;
2626
use PHPUnit\Framework\TestCase;
27+
use Google\Auth\ApplicationDefaultCredentials;
28+
use GuzzleHttp\Client;
29+
use GuzzleHttp\HandlerStack;
2730

2831
/**
2932
* @retryAttempts 3
@@ -95,6 +98,12 @@ class spannerTest extends TestCase
9598
/** @var InstanceConfiguration $customInstanceConfig */
9699
protected static $customInstanceConfig;
97100

101+
/** @var string $databaseRole */
102+
protected static $databaseRole;
103+
104+
/** @var string serviceAccountEmail */
105+
protected static $serviceAccountEmail = null;
106+
98107
public static function setUpBeforeClass(): void
99108
{
100109
self::checkProjectEnvVars();
@@ -126,6 +135,7 @@ public static function setUpBeforeClass(): void
126135
self::$baseConfigId = 'nam7';
127136
self::$customInstanceConfigId = 'custom-' . time() . rand();
128137
self::$customInstanceConfig = $spanner->instanceConfiguration(self::$customInstanceConfigId);
138+
self::$databaseRole = 'new_parent';
129139
}
130140

131141
public function testCreateInstance()
@@ -932,10 +942,50 @@ public function testDmlReturningDelete()
932942
public function testAddDropDatabaseRole()
933943
{
934944
$output = $this->runFunctionSnippet('add_drop_database_role');
935-
$this->assertStringContainsString('Waiting for create role and grant operation to complete... ' . PHP_EOL, $output);
936-
$this->assertStringContainsString('Created roles new_parent and new_child and granted privileges ' . PHP_EOL, $output);
937-
$this->assertStringContainsString('Waiting for revoke role and drop role operation to complete... ' . PHP_EOL, $output);
938-
$this->assertStringContainsString('Revoked privileges and dropped roles new_child and new_parent ' . PHP_EOL, $output);
945+
$this->assertStringContainsString('Waiting for create role and grant operation to complete...' . PHP_EOL, $output);
946+
$this->assertStringContainsString('Created roles new_parent and new_child and granted privileges' . PHP_EOL, $output);
947+
$this->assertStringContainsString('Waiting for revoke role and drop role operation to complete...' . PHP_EOL, $output);
948+
$this->assertStringContainsString('Revoked privileges and dropped role new_child' . PHP_EOL, $output);
949+
}
950+
951+
/**
952+
* @depends testAddDropDatabaseRole
953+
*/
954+
public function testListDatabaseRoles()
955+
{
956+
$output = $this->runFunctionSnippet('list_database_roles', [
957+
self::$projectId,
958+
self::$instanceId,
959+
self::$databaseId
960+
]);
961+
$this->assertStringContainsString(sprintf('databaseRoles/%s', self::$databaseRole), $output);
962+
}
963+
964+
/**
965+
* @depends testAddDropDatabaseRole
966+
* @depends testInsertDataWithDml
967+
*/
968+
public function testReadDataWithDatabaseRole()
969+
{
970+
$output = $this->runFunctionSnippet('read_data_with_database_role');
971+
$this->assertStringContainsString('SingerId: 10, Firstname: Virginia, LastName: Watson', $output);
972+
}
973+
974+
/**
975+
* depends testAddDropDatabaseRole
976+
*/
977+
public function testEnableFineGrainedAccess()
978+
{
979+
self::$serviceAccountEmail = $this->createServiceAccount(str_shuffle('testSvcAcnt'));
980+
$output = $this->runFunctionSnippet('enable_fine_grained_access', [
981+
self::$projectId,
982+
self::$instanceId,
983+
self::$databaseId,
984+
sprintf('serviceAccount:%s', self::$serviceAccountEmail),
985+
self::$databaseRole,
986+
'DatabaseRoleBindingTitle'
987+
]);
988+
$this->assertStringContainsString('Enabled fine-grained access in IAM', $output);
939989
}
940990

941991
/**
@@ -1029,6 +1079,49 @@ private function runFunctionSnippet($sampleName, $params = [])
10291079
);
10301080
}
10311081

1082+
private function createServiceAccount($serviceAccountId)
1083+
{
1084+
$client = self::getIamHttpClient();
1085+
// make the request
1086+
$response = $client->post('/v1/projects/' . self::$projectId . '/serviceAccounts', [
1087+
'json' => [
1088+
'accountId' => $serviceAccountId,
1089+
'serviceAccount' => [
1090+
'displayName' => 'Test Service Account',
1091+
'description' => 'This account should be deleted automatically after the unit tests complete.'
1092+
]
1093+
]
1094+
]);
1095+
1096+
return json_decode($response->getBody())->email;
1097+
}
1098+
1099+
public static function deleteServiceAccount($serviceAccountEmail)
1100+
{
1101+
$client = self::getIamHttpClient();
1102+
// make the request
1103+
$client->delete('/v1/projects/' . self::$projectId . '/serviceAccounts/' . $serviceAccountEmail);
1104+
}
1105+
1106+
private static function getIamHttpClient()
1107+
{
1108+
// TODO: When this method is exposed in googleapis/google-cloud-php, remove the use of the following
1109+
$scopes = ['https://www.googleapis.com/auth/cloud-platform'];
1110+
1111+
// create middleware
1112+
$middleware = ApplicationDefaultCredentials::getMiddleware($scopes);
1113+
$stack = HandlerStack::create();
1114+
$stack->push($middleware);
1115+
1116+
// create the HTTP client
1117+
$client = new Client([
1118+
'handler' => $stack,
1119+
'base_uri' => 'https://iam.googleapis.com',
1120+
'auth' => 'google_auth' // authorize all requests
1121+
]);
1122+
return $client;
1123+
}
1124+
10321125
public static function tearDownAfterClass(): void
10331126
{
10341127
if (self::$instance->exists()) {// Clean up database
@@ -1042,5 +1135,8 @@ public static function tearDownAfterClass(): void
10421135
if (self::$customInstanceConfig->exists()) {
10431136
self::$customInstanceConfig->delete();
10441137
}
1138+
if (!is_null(self::$serviceAccountEmail)) {
1139+
self::deleteServiceAccount(self::$serviceAccountEmail);
1140+
}
10451141
}
10461142
}

0 commit comments

Comments
 (0)