Skip to content

Commit 274fe51

Browse files
authored
Code refactor: tagPattern verification and ref checks. (#8697)
1 parent b6233bd commit 274fe51

File tree

2 files changed

+117
-33
lines changed

2 files changed

+117
-33
lines changed

Diff for: app/lib/package/backend.dart

+54-33
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ class PackageBackend {
527527
final repository = githubConfig.repository?.trim() ?? '';
528528
githubConfig.repository = repository.isEmpty ? null : repository;
529529
final tagPattern = githubConfig.tagPattern?.trim() ?? '';
530+
verifyTagPattern(tagPattern: tagPattern);
530531
githubConfig.tagPattern = tagPattern.isEmpty ? null : tagPattern;
531532
final environment = githubConfig.environment?.trim() ?? '';
532533
githubConfig.environment = environment.isEmpty ? null : environment;
@@ -544,15 +545,6 @@ class PackageBackend {
544545
'The `repository` field has invalid characters.');
545546
}
546547

547-
final tagPatternParts = tagPattern.split('{{version}}');
548-
InvalidInputException.check(tagPatternParts.length == 2,
549-
'The `tagPattern` field must contain a single `{{version}}` part.');
550-
InvalidInputException.check(
551-
tagPatternParts
552-
.where((e) => e.isNotEmpty)
553-
.every(_validGitHubVersionPattern.hasMatch),
554-
'The `tagPattern` field has invalid characters.');
555-
556548
InvalidInputException.check(
557549
!githubConfig.requireEnvironment || environment.isNotEmpty,
558550
'The `environment` field must not be empty when enabled.');
@@ -1339,11 +1331,6 @@ class PackageBackend {
13391331
if (repository == null || repository.isEmpty) {
13401332
throw AssertionError('Missing or empty repository.');
13411333
}
1342-
final tagPattern = githubConfig.tagPattern ?? '';
1343-
if (!tagPattern.contains('{{version}}')) {
1344-
throw AssertionError(
1345-
'Configured tag pattern does not include `{{version}}`');
1346-
}
13471334
final requireEnvironment = githubConfig.requireEnvironment;
13481335
final environment = githubConfig.environment;
13491336
if (requireEnvironment && (environment == null || environment.isEmpty)) {
@@ -1375,25 +1362,11 @@ class PackageBackend {
13751362
throw AuthorizationException.githubActionIssue(
13761363
'publishing is only allowed from "tag" refType, this token has "${agent.payload.refType}" refType');
13771364
}
1378-
final expectedRefStart = 'refs/tags/';
1379-
if (!agent.payload.ref.startsWith(expectedRefStart)) {
1380-
throw AuthorizationException.githubActionIssue(
1381-
'publishing is only allowed from "refs/tags/*" ref, this token has "${agent.payload.ref}" ref');
1382-
}
1383-
final expectedTagValue = tagPattern.replaceFirst('{{version}}', newVersion);
1384-
if (agent.payload.ref != 'refs/tags/$expectedTagValue') {
1385-
// At this point we have concluded that the agent has push rights to the repository,
1386-
// however, the tag pattern they have used is not the one we expect.
1387-
//
1388-
// By revealing the expected tag pattern, we are serving the users with better
1389-
// error message, while not exposing much information to an assumed attacker.
1390-
// With the current access level, an attacker would have access to past tags, and
1391-
// figuring out the tag pattern from those should be straightforward anyway.
1392-
throw AuthorizationException.githubActionIssue(
1393-
'publishing is configured to only be allowed from actions with specific ref pattern, '
1394-
'this token has "${agent.payload.ref}" ref for which publishing is not allowed. '
1395-
'Expected tag "$expectedTagValue". Check that the version in the tag matches the version in "pubspec.yaml"');
1396-
}
1365+
verifyTagPatternWithRef(
1366+
tagPattern: githubConfig.tagPattern ?? '',
1367+
ref: agent.payload.ref,
1368+
newVersion: newVersion,
1369+
);
13971370

13981371
// When environment is configured, it must match the action's environment.
13991372
if (requireEnvironment && environment != agent.payload.environment) {
@@ -1759,6 +1732,54 @@ Future<void> purgePackageCache(String package) async {
17591732
]);
17601733
}
17611734

1735+
/// Verifies the [tagPattern] before storing it on the automated publishing
1736+
/// settings object.
1737+
@visibleForTesting
1738+
void verifyTagPattern({required String tagPattern}) {
1739+
final tagPatternParts = tagPattern.split('{{version}}');
1740+
InvalidInputException.check(tagPatternParts.length == 2,
1741+
'The `tagPattern` field must contain a single `{{version}}` part.');
1742+
InvalidInputException.check(
1743+
tagPatternParts
1744+
.where((e) => e.isNotEmpty)
1745+
.every(_validGitHubVersionPattern.hasMatch),
1746+
'The `tagPattern` field has invalid characters.');
1747+
}
1748+
1749+
/// Verifies the user-settings [tagPattern] with the authentication-provided
1750+
/// [ref] value, and throws if the ref is not allowed or not recognized as
1751+
/// valid pattern.
1752+
@visibleForTesting
1753+
void verifyTagPatternWithRef({
1754+
required String tagPattern,
1755+
required String ref,
1756+
required String newVersion,
1757+
}) {
1758+
if (!tagPattern.contains('{{version}}')) {
1759+
throw AssertionError(
1760+
'Configured tag pattern does not include `{{version}}`');
1761+
}
1762+
final expectedRefStart = 'refs/tags/';
1763+
if (!ref.startsWith(expectedRefStart)) {
1764+
throw AuthorizationException.githubActionIssue(
1765+
'publishing is only allowed from "refs/tags/*" ref, this token has "$ref" ref');
1766+
}
1767+
final expectedTagValue = tagPattern.replaceFirst('{{version}}', newVersion);
1768+
if (ref != 'refs/tags/$expectedTagValue') {
1769+
// At this point we have concluded that the agent has push rights to the repository,
1770+
// however, the tag pattern they have used is not the one we expect.
1771+
//
1772+
// By revealing the expected tag pattern, we are serving the users with better
1773+
// error message, while not exposing much information to an assumed attacker.
1774+
// With the current access level, an attacker would have access to past tags, and
1775+
// figuring out the tag pattern from those should be straightforward anyway.
1776+
throw AuthorizationException.githubActionIssue(
1777+
'publishing is configured to only be allowed from actions with specific ref pattern, '
1778+
'this token has "$ref" ref for which publishing is not allowed. '
1779+
'Expected tag "$expectedTagValue". Check that the version in the tag matches the version in "pubspec.yaml"');
1780+
}
1781+
}
1782+
17621783
/// The status of an invite after being created or updated.
17631784
class InviteStatus {
17641785
final String? urlNonce;

Diff for: app/test/package/backend_test.dart

+63
Original file line numberDiff line numberDiff line change
@@ -470,4 +470,67 @@ void main() {
470470
});
471471
});
472472
});
473+
474+
group('GitHub tagPattern', () {
475+
test('verifyTagPattern: valid inputs', () {
476+
final values = [
477+
'{{version}}',
478+
'v{{version}}',
479+
'{{version}}v',
480+
'package-{{version}}',
481+
'package-v{{version}}',
482+
'package-v{{version}}-postfix',
483+
];
484+
for (final value in values) {
485+
verifyTagPattern(tagPattern: value);
486+
}
487+
});
488+
489+
test('verifyTagPattern: invalid inputs', () {
490+
final values = [
491+
'', // empty pattern is not allowed
492+
'{{version}}{{version}}', // two {{version}} is not allowed
493+
'%-{{version}}', // % is not allowed
494+
'abc/def-{{version}}', // / is not allowed
495+
'{{version}}-abc/def', // / is not allowed
496+
];
497+
for (final value in values) {
498+
expect(
499+
() => verifyTagPattern(tagPattern: value),
500+
throwsA(isA<InvalidInputException>()),
501+
);
502+
}
503+
});
504+
505+
test('verifyTagPatternWithRef: valid inputs', () {
506+
final values = [
507+
('{{version}}', 'refs/tags/1.0.0'),
508+
('pkg-v{{version}}', 'refs/tags/pkg-v1.0.0'),
509+
];
510+
for (final value in values) {
511+
verifyTagPatternWithRef(
512+
tagPattern: value.$1,
513+
ref: value.$2,
514+
newVersion: '1.0.0',
515+
);
516+
}
517+
});
518+
519+
test('verifyTagPatternWithRef: invalid inputs', () {
520+
final values = [
521+
('v{{version}}', 'refs/tags/1.0.0'), // does not match `v` prefix
522+
('v{{version}}', 'refs/x/v1.0.0'), // missing refs/tags
523+
];
524+
for (final value in values) {
525+
expect(
526+
() => verifyTagPatternWithRef(
527+
tagPattern: value.$1,
528+
ref: value.$2,
529+
newVersion: '1.0.0',
530+
),
531+
throwsA(isA<AuthorizationException>()),
532+
);
533+
}
534+
});
535+
});
473536
}

0 commit comments

Comments
 (0)