Skip to content

Commit 5735afe

Browse files
committed
feat(storage): insert object acl
1 parent 10693d8 commit 5735afe

File tree

2 files changed

+282
-0
lines changed

2 files changed

+282
-0
lines changed

pkgs/google_cloud_storage/lib/src/client.dart

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'bucket.dart';
2424
import 'bucket_metadata_json.dart';
2525
import 'bucket_metadata_patch_builder.dart'
2626
show BucketMetadataPatchBuilderJsonEncodable;
27+
import 'common_json.dart';
2728
import 'default_http_client_web.dart'
2829
if (dart.library.io) 'default_http_client_vm.dart';
2930
import 'default_project_id_web.dart'
@@ -585,6 +586,66 @@ final class Storage {
585586
} while (nextPageToken != null);
586587
}
587588

589+
/// Creates a new ACL entry on the specified [[Google Cloud Storage object].
590+
///
591+
/// This operation is not idempotent.
592+
///
593+
/// If the bucket has uniform bucket-level access enabled, this operation
594+
/// will fail with [BadRequestException].
595+
///
596+
/// Throws [NotFoundException] if the object does not exist.
597+
///
598+
/// [entity] specifies the entity holding the permission. Supported formats:
599+
/// - `user-emailAddress`
600+
/// - `group-emailAddress`
601+
/// - `domain-domain`
602+
/// - `project-team-projectNumber`
603+
/// - `allUsers`
604+
/// - `allAuthenticatedUsers`
605+
///
606+
/// For example:
607+
/// - The user `liz@example.com` would be `"user-liz@example.com"`.
608+
/// - The group `example@googlegroups.com` would be
609+
/// `"group-example@googlegroups.com"`.
610+
/// - To refer to all members of the domain `example.com`, the entity would
611+
/// be `"domain-example.com"`.
612+
///
613+
/// [role] specifies the access permission for the entity. There are two roles
614+
/// that can be assigned to an object:
615+
/// 1. `READER` can get an object, though the `acl` property will not be
616+
/// revealed.
617+
/// 2. `OWNER` are `READER`s, and they can get the `acl` property, update the
618+
/// object's metadata, and call all [ObjectAccessControl]-related methods
619+
/// on the object. The owner of an object is always an `OWNER`.
620+
///
621+
/// See [API reference docs](https://cloud.google.com/storage/docs/json_api/v1/objectAccessControls/insert).
622+
///
623+
/// [Google Cloud Storage object]: https://docs.cloud.google.com/storage/docs/objects
624+
625+
Future<ObjectAccessControl> insertObjectAcl(
626+
String bucket,
627+
String object,
628+
String entity,
629+
String role, {
630+
RetryRunner retry = defaultRetry,
631+
}) => retry.run(() async {
632+
final serviceClient = await _serviceClient;
633+
final url = _requestUrl([
634+
'storage',
635+
'v1',
636+
'b',
637+
bucket,
638+
'o',
639+
object,
640+
'acl',
641+
], {});
642+
final j = await serviceClient.post(
643+
url,
644+
body: _JsonEncodableWrapper({'entity': entity, 'role': role}),
645+
);
646+
return objectAccessControlFromJson(j as Map<String, Object?>)!;
647+
}, isIdempotent: false);
648+
588649
/// Information about a [Google Cloud Storage object].
589650
///
590651
/// This operation is read-only and always idempotent.
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'package:google_cloud_storage/google_cloud_storage.dart';
16+
import 'package:http/http.dart' as http;
17+
import 'package:http/testing.dart';
18+
import 'package:test/test.dart';
19+
20+
import 'test_utils.dart';
21+
22+
void main() async {
23+
late Storage storage;
24+
25+
group('insert object acl', () {
26+
group('google-cloud', tags: ['google-cloud', 'no-ulba'], () {
27+
setUp(() {
28+
storage = Storage();
29+
});
30+
31+
tearDown(() => storage.close());
32+
33+
test('success', () async {
34+
final bucketName = await createBucketWithTearDown(
35+
storage,
36+
'ins_obj_acl_ok',
37+
metadata: BucketMetadata(
38+
iamConfiguration: BucketIamConfiguration(
39+
uniformBucketLevelAccess: UniformBucketLevelAccess(
40+
enabled: false,
41+
),
42+
),
43+
),
44+
);
45+
46+
await storage.uploadObjectFromString(
47+
bucketName,
48+
'object.txt',
49+
'Hello World!',
50+
ifGenerationMatch: BigInt.zero,
51+
);
52+
53+
final acl = await storage.insertObjectAcl(
54+
bucketName,
55+
'object.txt',
56+
'user-daenerysstone.938939@gmail.com',
57+
'READER',
58+
);
59+
60+
expect(acl.entity, 'user-daenerysstone.938939@gmail.com');
61+
expect(acl.role, 'READER');
62+
63+
final metadata = await storage.objectMetadata(
64+
bucketName,
65+
'object.txt',
66+
projection: 'full',
67+
);
68+
print(metadata.acl);
69+
final testUserRoles = [
70+
for (var i in metadata.acl ?? <ObjectAccessControl>[])
71+
if (i.entity == 'user-daenerysstone.938939@gmail.com')
72+
(i.entity, i.role),
73+
];
74+
expect(testUserRoles, [
75+
('user-daenerysstone.938939@gmail.com', 'READER'),
76+
]);
77+
});
78+
79+
test('reader then owner', () async {
80+
final bucketName = await createBucketWithTearDown(
81+
storage,
82+
'ins_obj_acl_ok',
83+
metadata: BucketMetadata(
84+
iamConfiguration: BucketIamConfiguration(
85+
uniformBucketLevelAccess: UniformBucketLevelAccess(
86+
enabled: false,
87+
),
88+
),
89+
),
90+
);
91+
92+
await storage.uploadObjectFromString(
93+
bucketName,
94+
'object.txt',
95+
'Hello World!',
96+
ifGenerationMatch: BigInt.zero,
97+
);
98+
99+
await storage.insertObjectAcl(
100+
bucketName,
101+
'object.txt',
102+
'user-daenerysstone.938939@gmail.com',
103+
'READER',
104+
);
105+
await storage.insertObjectAcl(
106+
bucketName,
107+
'object.txt',
108+
'user-daenerysstone.938939@gmail.com',
109+
'OWNER',
110+
);
111+
112+
final metadata = await storage.objectMetadata(
113+
bucketName,
114+
'object.txt',
115+
projection: 'full',
116+
);
117+
118+
final testUserRoles = [
119+
for (var i in metadata.acl ?? <ObjectAccessControl>[])
120+
if (i.entity == 'user-daenerysstone.938939@gmail.com')
121+
(i.entity, i.role),
122+
];
123+
expect(testUserRoles, [
124+
('user-daenerysstone.938939@gmail.com', 'OWNER'),
125+
]);
126+
});
127+
128+
test('owner then reader', () async {
129+
final bucketName = await createBucketWithTearDown(
130+
storage,
131+
'ins_obj_acl_ok',
132+
metadata: BucketMetadata(
133+
iamConfiguration: BucketIamConfiguration(
134+
uniformBucketLevelAccess: UniformBucketLevelAccess(
135+
enabled: false,
136+
),
137+
),
138+
),
139+
);
140+
141+
await storage.uploadObjectFromString(
142+
bucketName,
143+
'object.txt',
144+
'Hello World!',
145+
ifGenerationMatch: BigInt.zero,
146+
);
147+
148+
await storage.insertObjectAcl(
149+
bucketName,
150+
'object.txt',
151+
'user-daenerysstone.938939@gmail.com',
152+
'OWNER',
153+
);
154+
await storage.insertObjectAcl(
155+
bucketName,
156+
'object.txt',
157+
'user-daenerysstone.938939@gmail.com',
158+
'READER',
159+
);
160+
161+
final metadata = await storage.objectMetadata(
162+
bucketName,
163+
'object.txt',
164+
projection: 'full',
165+
);
166+
167+
final testUserRoles = [
168+
for (var i in metadata.acl ?? <ObjectAccessControl>[])
169+
if (i.entity == 'user-daenerysstone.938939@gmail.com')
170+
(i.entity, i.role),
171+
];
172+
expect(testUserRoles, [
173+
('user-daenerysstone.938939@gmail.com', 'READER'),
174+
]);
175+
});
176+
177+
test('not found', () async {
178+
final bucketName = await createBucketWithTearDown(
179+
storage,
180+
'ins_obj_acl_not_found',
181+
);
182+
183+
final bucketMetadata = await storage.bucketMetadata(
184+
bucketName,
185+
projection: 'full',
186+
);
187+
final entity = bucketMetadata.acl!.first.entity!;
188+
189+
expect(
190+
() => storage.insertObjectAcl(
191+
bucketName,
192+
'non-existent.txt',
193+
entity,
194+
'READER',
195+
),
196+
throwsA(isA<NotFoundException>()),
197+
);
198+
});
199+
});
200+
201+
test('non-idempotent transport failure', () async {
202+
var count = 0;
203+
final mockClient = MockClient((request) async {
204+
count++;
205+
if (count == 1) {
206+
throw http.ClientException('Some transport failure');
207+
} else {
208+
throw StateError('Unexpected call count: $count');
209+
}
210+
});
211+
212+
final storage = Storage(client: mockClient, projectId: 'fake project');
213+
214+
await expectLater(
215+
storage.insertObjectAcl('bucket', 'object', 'entity', 'role'),
216+
throwsA(isA<http.ClientException>()),
217+
);
218+
expect(count, 1);
219+
});
220+
});
221+
}

0 commit comments

Comments
 (0)