Environment credentials support (#3115)
* Environment credentials support
diff --git a/lib/src/authentication/credential.dart b/lib/src/authentication/credential.dart
index d840f6b..a44cc91 100644
--- a/lib/src/authentication/credential.dart
+++ b/lib/src/authentication/credential.dart
@@ -4,6 +4,8 @@
// @dart=2.11
+import 'dart:io';
+
import 'package:meta/meta.dart';
import '../exceptions.dart';
@@ -26,13 +28,21 @@
/// Internal constructor that's only used by [fromJson].
Credential._internal({
@required this.url,
- @required this.token,
@required this.unknownFields,
+ @required this.token,
+ @required this.env,
});
- /// Create a new [Credential].
+ /// Create credential that stores clear text token.
Credential.token(this.url, this.token)
- : unknownFields = const <String, dynamic>{};
+ : env = null,
+ unknownFields = const <String, dynamic>{};
+
+ /// Create credential that stores environment variable name that stores token
+ /// value.
+ Credential.env(this.url, this.env)
+ : token = null,
+ unknownFields = const <String, dynamic>{};
/// Deserialize [json] into [Credential] type.
///
@@ -44,14 +54,29 @@
final hostedUrl = validateAndNormalizeHostedUrl(json['url'] as String);
- const knownKeys = {'url', 'token'};
+ const knownKeys = {'url', 'token', 'env'};
final unknownFields = Map.fromEntries(
json.entries.where((kv) => !knownKeys.contains(kv.key)));
+ /// Returns [String] value from [json] at [key] index or `null` if [json]
+ /// doesn't contains [key].
+ ///
+ /// Throws [FormatException] if value type is not [String].
+ String _string(String key) {
+ if (json.containsKey(key)) {
+ if (json[key] is! String) {
+ throw FormatException('Provided $key value should be string');
+ }
+ return json[key] as String;
+ }
+ return null;
+ }
+
return Credential._internal(
url: hostedUrl,
- token: json['token'] is String ? json['token'] as String : null,
unknownFields: unknownFields,
+ token: _string('token'),
+ env: _string('env'),
);
}
@@ -61,6 +86,9 @@
/// Authentication token value
final String token;
+ /// Environment variable name that stores token value
+ final String env;
+
/// Unknown fields found in pub-tokens.json. The fields might be created by the
/// future version of pub tool. We don't want to override them when using the
/// old SDK.
@@ -71,6 +99,7 @@
return <String, dynamic>{
'url': url.toString(),
if (token != null) 'token': token,
+ if (env != null) 'env': env,
...unknownFields,
};
}
@@ -90,6 +119,17 @@
);
}
+ if (env != null) {
+ final value = Platform.environment[env];
+ if (value == null) {
+ throw DataException(
+ 'Saved credential for "$url" pub repository requires environment '
+ 'variable named "$env" but not defined.',
+ );
+ }
+ return Future.value('Bearer $value');
+ }
+
return Future.value('Bearer $token');
}
@@ -103,7 +143,8 @@
///
/// This method might return `false` when a `pub-tokens.json` file created by
/// future SDK used by pub tool from old SDK.
- bool isValid() => token != null;
+ // Either [token] or [env] should be defined to be valid.
+ bool isValid() => (token == null) ^ (env == null);
static String _normalizeUrl(String url) {
return (url.endsWith('/') ? url : '$url/').toLowerCase();
diff --git a/lib/src/command/token_add.dart b/lib/src/command/token_add.dart
index b00ffaf..f7cd6b9 100644
--- a/lib/src/command/token_add.dart
+++ b/lib/src/command/token_add.dart
@@ -5,6 +5,7 @@
// @dart=2.11
import 'dart:async';
+import 'dart:io';
import '../authentication/credential.dart';
import '../command.dart';
@@ -25,6 +26,14 @@
@override
String get argumentsDescription => '[hosted-url]';
+ String get envVar => argResults['env-var'];
+
+ TokenAddCommand() {
+ argParser.addOption('env-var',
+ help: 'Read the secret token from this environment variable when '
+ 'making requests.');
+ }
+
@override
Future<void> runProtected() async {
if (argResults.rest.isEmpty) {
@@ -40,17 +49,11 @@
throw DataException('Insecure package repository could not be added.');
}
- final token = await stdinPrompt('Enter secret token:', echoMode: false)
- .timeout(const Duration(minutes: 15));
- if (token.isEmpty) {
- usageException('Token is not provided.');
+ if (envVar == null) {
+ await _addTokenFromStdin(hostedUrl);
+ } else {
+ await _addEnvVarToken(hostedUrl);
}
-
- tokenStore.addCredential(Credential.token(hostedUrl, token));
- log.message(
- 'Requests to $hostedUrl will now be authenticated using the secret '
- 'token.',
- );
} on FormatException catch (e) {
usageException('Invalid [hosted-url]: "${argResults.rest.first}"\n'
'${e.message}');
@@ -61,4 +64,37 @@
throw ApplicationException('Token is not provided within 15 minutes.');
}
}
+
+ Future<void> _addTokenFromStdin(Uri hostedUrl) async {
+ final token = await stdinPrompt('Enter secret token:', echoMode: false)
+ .timeout(const Duration(minutes: 15));
+ if (token.isEmpty) {
+ usageException('Token is not provided.');
+ }
+
+ tokenStore.addCredential(Credential.token(hostedUrl, token));
+ log.message(
+ 'Requests to "$hostedUrl" will now be authenticated using the secret '
+ 'token.',
+ );
+ }
+
+ Future<void> _addEnvVarToken(Uri hostedUrl) async {
+ if (envVar.isEmpty) {
+ throw DataException('Cannot use the empty string as --env-var');
+ }
+
+ tokenStore.addCredential(Credential.env(hostedUrl, envVar));
+ log.message(
+ 'Requests to "$hostedUrl" will now be authenticated using the secret '
+ 'token stored in the environment variable `$envVar`.',
+ );
+
+ if (!Platform.environment.containsKey(envVar)) {
+ // If environment variable doesn't exist when
+ // pub token add <hosted-url> --env-var <ENV_VAR> is called, we should
+ // print a warning.
+ log.warning('Environment variable `$envVar` is not defined.');
+ }
+ }
}
diff --git a/test/token/add_token_test.dart b/test/token/add_token_test.dart
index 536b295..8105ce5 100644
--- a/test/token/add_token_test.dart
+++ b/test/token/add_token_test.dart
@@ -34,6 +34,52 @@
}).validate();
});
+ group('with environment variable creates tokens.json that contains env var',
+ () {
+ test('without environment variable provided', () async {
+ await d.tokensFile({
+ 'version': 1,
+ 'hosted': [
+ {'url': 'https://example.com', 'token': 'abc'},
+ ]
+ }).create();
+
+ await runPub(
+ args: ['token', 'add', 'https://example.com/', '--env-var', 'TOKEN'],
+ error: 'Environment variable `TOKEN` is not defined.',
+ );
+
+ await d.tokensFile({
+ 'version': 1,
+ 'hosted': [
+ {'url': 'https://example.com', 'env': 'TOKEN'},
+ ]
+ }).validate();
+ });
+
+ test('with environment variable provided', () async {
+ await d.tokensFile({
+ 'version': 1,
+ 'hosted': [
+ {'url': 'https://example.com', 'token': 'abc'},
+ ]
+ }).create();
+
+ await runPub(
+ args: ['token', 'add', 'https://example.com/', '--env-var', 'TOKEN'],
+ environment: {'TOKEN': 'secret'},
+ error: isNot(contains('Environment variable TOKEN is not defined.')),
+ );
+
+ await d.tokensFile({
+ 'version': 1,
+ 'hosted': [
+ {'url': 'https://example.com', 'env': 'TOKEN'},
+ ]
+ }).validate();
+ });
+ });
+
test('persists unknown fields on unmodified entries', () async {
await d.tokensFile({
'version': 1,