Skip to content

Commit d6ca74c

Browse files
authored
Updated uploader checks. (#2642)
* Updated uploader checks. * OperationForbiddenException * AccountPkgOptions.isAdmin * 403 * isPackageAdmin with fixed userId parameter.
1 parent 802c20e commit d6ca74c

File tree

9 files changed

+102
-58
lines changed

9 files changed

+102
-58
lines changed

app/bin/tools/uploader.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Future addUploader(String packageName, String uploaderEmail) async {
7272
await accountBackend.getEmailsOfUserIds(package.uploaders);
7373
print('Current uploaders: $uploaderEmails');
7474
final user = await accountBackend.lookupOrCreateUserByEmail(uploaderEmail);
75-
if (package.hasUploader(user.userId)) {
75+
if (package.containsUploader(user.userId)) {
7676
throw Exception('Uploader $uploaderEmail already exists');
7777
}
7878
package.addUploader(user.userId);
@@ -104,7 +104,7 @@ Future removeUploader(String packageName, String uploaderEmail) async {
104104
await accountBackend.getEmailsOfUserIds(package.uploaders);
105105
print('Current uploaders: $uploaderEmails');
106106
final user = await accountBackend.lookupOrCreateUserByEmail(uploaderEmail);
107-
if (!package.hasUploader(user.userId)) {
107+
if (!package.containsUploader(user.userId)) {
108108
throw Exception('Uploader $uploaderEmail does not exist');
109109
}
110110
if (package.uploaderCount <= 1) {

app/lib/frontend/backend.dart

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import 'package:uuid/uuid.dart';
2323
import '../account/backend.dart';
2424
import '../history/backend.dart';
2525
import '../history/models.dart';
26+
import '../publisher/models.dart';
2627
import '../shared/analyzer_client.dart';
2728
import '../shared/configuration.dart';
2829
import '../shared/dartdoc_client.dart';
@@ -283,9 +284,7 @@ class Backend {
283284
throw NotFoundException('Package $package does not exists.');
284285
}
285286
latestVersion = p.latestVersion;
286-
if (!p.hasUploader(user.userId)) {
287-
throw AuthorizationException.userIsNotAdminForPackage(package);
288-
}
287+
await checkPackageAdmin(p, user.userId);
289288
p.isDiscontinued = options.isDiscontinued ?? p.isDiscontinued;
290289
_logger.info('Updating $package options: '
291290
'isDiscontinued: ${p.isDiscontinued} '
@@ -297,6 +296,40 @@ class Backend {
297296
await analyzerClient.triggerAnalysis(package, latestVersion, <String>{});
298297
});
299298
}
299+
300+
/// Whether [userId] is a package admin (through direct uploaders list or
301+
/// publisher admin).
302+
///
303+
/// Returns false if the user is not an admin.
304+
Future<bool> isPackageAdmin(models.Package p, String userId) async {
305+
if (userId == null) {
306+
return false;
307+
}
308+
if (p.publisherId == null) {
309+
return p.containsUploader(userId);
310+
} else {
311+
final memberKey = db.emptyKey
312+
.append(Publisher, id: p.publisherId)
313+
.append(PublisherMember, id: userId);
314+
final list = await db.lookup<PublisherMember>([memberKey]);
315+
final member = list.single;
316+
return member?.role == PublisherMemberRole.admin;
317+
}
318+
}
319+
320+
/// Whether the [userId] is a package admin (through direct uploaders list or
321+
/// publisher admin).
322+
///
323+
/// Throws AuthenticationException if the user is provided.
324+
/// Throws AuthorizationException if the user is not an admin for the package.
325+
Future checkPackageAdmin(models.Package package, String userId) async {
326+
if (userId == null) {
327+
throw AuthenticationException.authenticationRequired();
328+
}
329+
if (!await isPackageAdmin(package, userId)) {
330+
throw AuthorizationException.userIsNotAdminForPackage(package.name);
331+
}
332+
}
300333
}
301334

302335
/// Invalidate [cache] entries for given [package].
@@ -503,11 +536,7 @@ class GCloudPackageRepository extends PackageRepository {
503536
if (package == null) {
504537
_logger.info('New package uploaded. [new-package-uploaded]');
505538
package = _newPackageFromVersion(db, newVersion);
506-
}
507-
508-
// Check if the uploader of the new version is allowed to upload to
509-
// the package.
510-
if (!package.hasUploader(user.userId)) {
539+
} else if (!await backend.isPackageAdmin(package, user.userId)) {
511540
_logger.info('User ${user.userId} (${user.email}) is not an uploader '
512541
'for package ${package.name}, rolling transaction back.');
513542
await T.rollback();
@@ -649,15 +678,15 @@ class GCloudPackageRepository extends PackageRepository {
649678
final packageKey = db.emptyKey.append(models.Package, id: packageName);
650679
final package = (await db.lookup([packageKey])).first as models.Package;
651680

652-
_validatePackageUploader(packageName, package, user.userId);
681+
await _validatePackageUploader(packageName, package, user.userId);
653682

654683
if (!isValidEmail(uploaderEmail)) {
655684
throw GenericProcessingException(
656685
'Not a valid e-mail: `$uploaderEmail`.');
657686
}
658687

659688
final uploader = await accountBackend.lookupUserByEmail(uploaderEmail);
660-
if (uploader != null && package.hasUploader(uploader.userId)) {
689+
if (uploader != null && package.containsUploader(uploader.userId)) {
661690
// The requested uploaderEmail is already part of the uploaders.
662691
return;
663692
}
@@ -709,13 +738,13 @@ class GCloudPackageRepository extends PackageRepository {
709738
final package = (await tx.lookup([packageKey])).first as models.Package;
710739

711740
try {
712-
_validatePackageUploader(packageName, package, fromUserId);
741+
await _validatePackageUploader(packageName, package, fromUserId);
713742
} catch (_) {
714743
await tx.rollback();
715744
rethrow;
716745
}
717746

718-
if (package.hasUploader(uploader.userId)) {
747+
if (package.containsUploader(uploader.userId)) {
719748
// The requested uploaderEmail is already part of the uploaders.
720749
await tx.rollback();
721750
return;
@@ -742,15 +771,15 @@ class GCloudPackageRepository extends PackageRepository {
742771
});
743772
}
744773

745-
void _validatePackageUploader(
746-
String packageName, models.Package package, String userId) {
774+
Future _validatePackageUploader(
775+
String packageName, models.Package package, String userId) async {
747776
// Fail if package doesn't exist.
748777
if (package == null) {
749778
throw NotFoundException.resource(packageName);
750779
}
751780

752781
// Fail if calling user doesn't have permission to change uploaders.
753-
if (!package.hasUploader(userId)) {
782+
if (!await backend.isPackageAdmin(package, userId)) {
754783
throw AuthorizationException.userCannotChangeUploaders(package.name);
755784
}
756785
}
@@ -763,22 +792,12 @@ class GCloudPackageRepository extends PackageRepository {
763792
final packageKey = db.emptyKey.append(models.Package, id: packageName);
764793
final package = (await T.lookup([packageKey])).first as models.Package;
765794

766-
// Fail if package doesn't exist.
767-
if (package == null) {
768-
await T.rollback();
769-
throw NotFoundException.resource(packageName);
770-
}
771-
772-
// Fail if calling user doesn't have permission to change uploaders.
773-
if (!package.hasUploader(user.userId)) {
774-
await T.rollback();
775-
throw AuthorizationException.userCannotChangeUploaders(package.name);
776-
}
795+
await _validatePackageUploader(packageName, package, user.userId);
777796

778797
final uploader =
779798
await accountBackend.lookupOrCreateUserByEmail(uploaderEmail);
780799
// Fail if the uploader we want to remove does not exist.
781-
if (!package.hasUploader(uploader.userId)) {
800+
if (!package.containsUploader(uploader.userId)) {
782801
await T.rollback();
783802
throw GenericProcessingException(
784803
'The uploader to remove does not exist.');

app/lib/frontend/handlers/account.dart

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ Future<shelf.Response> putAccountConsentHandler(
2525
/// Handles /api/account/options/packages/<package>
2626
Future<shelf.Response> accountPkgOptionsHandler(
2727
shelf.Request request, String package) async {
28-
final p = await backend.lookupPackage(package);
29-
if (p == null) {
30-
return notFoundHandler(request);
31-
}
32-
final options =
33-
AccountPkgOptions(isUploader: p.hasUploader(authenticatedUser.userId));
34-
return jsonResponse(options.toJson());
28+
return await withAuthenticatedUser((user) async {
29+
final p = await backend.lookupPackage(package);
30+
if (p == null) {
31+
return notFoundHandler(request);
32+
}
33+
final options = AccountPkgOptions(
34+
isAdmin: await backend.isPackageAdmin(p, user.userId));
35+
return jsonResponse(options.toJson());
36+
});
3537
}

app/lib/frontend/models.dart

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:meta/meta.dart';
1111
import 'package:pub_semver/pub_semver.dart';
1212

1313
import '../scorecard/models.dart';
14+
import '../shared/exceptions.dart';
1415
import '../shared/model_properties.dart';
1516
import '../shared/search_service.dart' show ApiPageRef;
1617
import '../shared/urls.dart' as urls;
@@ -80,23 +81,31 @@ class Package extends db.ExpandoModel {
8081
return shortDateFormat.format(updated);
8182
}
8283

83-
// Check if a user is an uploader for a package.
84-
bool hasUploader(String uploaderId) {
85-
return uploaderId != null && uploaders.contains(uploaderId);
84+
// Check if a [userId] is in the list of [uploaders].
85+
bool containsUploader(String userId) {
86+
return userId != null && uploaders.contains(userId);
8687
}
8788

8889
int get uploaderCount => uploaders.length;
8990

90-
/// Add the id to the list of uploaders.
91-
void addUploader(String uploaderId) {
92-
if (uploaderId != null && !uploaders.contains(uploaderId)) {
93-
uploaders.add(uploaderId);
91+
/// Add the [userId] to the list of [uploaders].
92+
void addUploader(String userId) {
93+
if (publisherId != null) {
94+
throw OperationForbiddenException.publisherOwnedPackageNoUploader(
95+
name, publisherId);
96+
}
97+
if (userId != null && !uploaders.contains(userId)) {
98+
uploaders.add(userId);
9499
}
95100
}
96101

97-
// Remove the id from the list of uploaders.
98-
void removeUploader(String uploaderId) {
99-
uploaders.removeWhere((s) => s == uploaderId);
102+
// Remove the [userId] from the list of [uploaders].
103+
void removeUploader(String userId) {
104+
if (publisherId != null) {
105+
throw OperationForbiddenException.publisherOwnedPackageNoUploader(
106+
name, publisherId);
107+
}
108+
uploaders.remove(userId);
100109
}
101110

102111
/// Updates latest stable and dev version keys with the new version.

app/lib/shared/exceptions.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,20 @@ class PackageRejectedException extends ResponseException
150150
400, 'PackageRejected', 'Package archive exceeded $limit bytes.');
151151
}
152152

153+
/// Thrown when the operation is rejected because of the internal state of a resource.
154+
class OperationForbiddenException extends ResponseException
155+
implements GenericProcessingException {
156+
/// The operation tried to update the list of uploaders, but it can't be done
157+
/// while the package is owned by a publisher.
158+
OperationForbiddenException.publisherOwnedPackageNoUploader(
159+
String packageName, String publisherId)
160+
: super._(
161+
403,
162+
'OperationForbidden',
163+
'Package "$packageName" is owned by publisher "$publisherId". '
164+
'Updating the uploaders is not permitted.');
165+
}
166+
153167
/// Thrown when authentication failed, credentials is missing or invalid.
154168
class AuthenticationException extends ResponseException
155169
implements UnauthorizedAccessException {

app/test/frontend/backend_test.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -171,21 +171,21 @@ void main() {
171171
testWithServices('not logged in', () async {
172172
final pkg = foobarPackage.name;
173173
final rs = backend.repository.addUploader(pkg, '[email protected]');
174-
expectLater(rs, throwsA(isA<AuthenticationException>()));
174+
await expectLater(rs, throwsA(isA<AuthenticationException>()));
175175
});
176176

177177
testWithServices('not authorized', () async {
178178
final pkg = foobarPackage.name;
179179
registerAuthenticatedUser(
180180
AuthenticatedUser('uuid-foo-at-bar-dot-com', '[email protected]'));
181181
final rs = backend.repository.addUploader(pkg, '[email protected]');
182-
expectLater(rs, throwsA(isA<AuthorizationException>()));
182+
await expectLater(rs, throwsA(isA<AuthorizationException>()));
183183
});
184184

185185
testWithServices('package does not exist', () async {
186186
registerAuthenticatedUser(hansAuthenticated);
187187
final rs = backend.repository.addUploader('no_package', '[email protected]');
188-
expectLater(rs, throwsA(isA<NotFoundException>()));
188+
await expectLater(rs, throwsA(isA<NotFoundException>()));
189189
});
190190

191191
Future testAlreadyExists(
@@ -243,7 +243,7 @@ void main() {
243243
testWithServices('not logged in', () async {
244244
final rs =
245245
backend.repository.removeUploader('hydrogen', hansUser.email);
246-
expectLater(rs, throwsA(isA<AuthenticationException>()));
246+
await expectLater(rs, throwsA(isA<AuthenticationException>()));
247247
});
248248

249249
testWithServices('not authorized', () async {
@@ -257,14 +257,14 @@ void main() {
257257
registerAuthenticatedUser(hansAuthenticated);
258258
final rs =
259259
backend.repository.removeUploader('hydrogen', hansUser.email);
260-
expectLater(rs, throwsA(isA<AuthorizationException>()));
260+
await expectLater(rs, throwsA(isA<AuthorizationException>()));
261261
});
262262

263263
testWithServices('package does not exist', () async {
264264
registerAuthenticatedUser(hansAuthenticated);
265265
final rs =
266266
backend.repository.removeUploader('non_hydrogen', hansUser.email);
267-
expectLater(rs, throwsA(isA<NotFoundException>()));
267+
await expectLater(rs, throwsA(isA<NotFoundException>()));
268268
});
269269

270270
testWithServices('cannot remove last uploader', () async {
@@ -368,7 +368,7 @@ void main() {
368368
testWithServices('no active user', () async {
369369
final rs = backend.repository
370370
.startAsyncUpload(Uri.parse('http://example.com/'));
371-
expectLater(rs, throwsA(isA<AuthenticationException>()));
371+
await expectLater(rs, throwsA(isA<AuthenticationException>()));
372372
});
373373

374374
testWithServices('successful', () async {

pkg/client_data/lib/account_api.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ part 'account_api.g.dart';
1010
/// Account-specific information about a package.
1111
@JsonSerializable()
1212
class AccountPkgOptions {
13-
final bool isUploader;
13+
final bool isAdmin;
1414

1515
AccountPkgOptions({
16-
@required this.isUploader,
16+
@required this.isAdmin,
1717
});
1818

1919
factory AccountPkgOptions.fromJson(Map<String, dynamic> json) =>

pkg/client_data/lib/account_api.g.dart

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/web_app/lib/src/account.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Future _updateOnCredChange() async {
8787
.get('/api/account/options/packages/${pageData.pkgData.package}');
8888
final map = json.decode(rs.body) as Map<String, dynamic>;
8989
final options = AccountPkgOptions.fromJson(map);
90-
_isPkgUploader = options.isUploader ?? false;
90+
_isPkgUploader = options.isAdmin ?? false;
9191
_updateUi();
9292
}
9393
} catch (e) {

0 commit comments

Comments
 (0)