Skip to content

Commit

Permalink
Authenticated cloud-to-prod interop tests. (#55)
Browse files Browse the repository at this point in the history
Added authentication provider classes, and wired up the auth interop
tests.

Refactored connection logic to throw initial connection errors early.

Fixes #53
  • Loading branch information
jakobr-google authored Feb 5, 2018
1 parent c082e5b commit 7621132
Show file tree
Hide file tree
Showing 11 changed files with 390 additions and 105 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.3.0 - 2018-02-05

* Added authentication metadata providers, optimized for use with Google Cloud.
* Added service URI to metadata provider API, needed for Json Web Token generation.
* Added authenticated cloud-to-prod interoperability tests.
* Refactored connection logic to throw initial connection errors early.

## 0.2.1 - 2018-01-18

* Updated generated code in examples using latest protoc compiler plugin.
Expand Down
57 changes: 0 additions & 57 deletions example/googleapis/bin/logging.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,72 +14,15 @@
// limitations under the License.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:googleapis_auth/auth_io.dart' as auth;
import 'package:grpc/grpc.dart';
import 'package:http/http.dart' as http;

import 'package:googleapis/src/generated/google/api/monitored_resource.pb.dart';
import 'package:googleapis/src/generated/google/logging/type/log_severity.pb.dart';
import 'package:googleapis/src/generated/google/logging/v2/log_entry.pb.dart';
import 'package:googleapis/src/generated/google/logging/v2/logging.pbgrpc.dart';

const _tokenExpirationThreshold = const Duration(seconds: 30);

class ServiceAccountAuthenticator {
auth.ServiceAccountCredentials _serviceAccountCredentials;
final List<String> _scopes;
String _projectId;

auth.AccessToken _accessToken;
Future<CallOptions> _call;

ServiceAccountAuthenticator(String serviceAccountJson, this._scopes) {
final serviceAccount = JSON.decode(serviceAccountJson);
_serviceAccountCredentials =
new auth.ServiceAccountCredentials.fromJson(serviceAccount);
_projectId = serviceAccount['project_id'];
}

String get projectId => _projectId;

Future authenticate(Map<String, String> metadata) async {
if (_accessToken == null || _accessToken.hasExpired) {
await _obtainAccessCredentials();
}

metadata['authorization'] = 'Bearer ${_accessToken.data}';

if (_tokenExpiresSoon) {
// Token is about to expire. Extend it prematurely.
_obtainAccessCredentials().catchError((_) {});
}
}

bool get _tokenExpiresSoon => _accessToken.expiry
.subtract(_tokenExpirationThreshold)
.isBefore(new DateTime.now().toUtc());

Future _obtainAccessCredentials() {
if (_call == null) {
final authClient = new http.Client();
_call = auth
.obtainAccessCredentialsViaServiceAccount(
_serviceAccountCredentials, _scopes, authClient)
.then((credentials) {
_accessToken = credentials.accessToken;
_call = null;
authClient.close();
});
}
return _call;
}

CallOptions get toCallOptions => new CallOptions(providers: [authenticate]);
}

Future<Null> main() async {
final serviceAccountFile = new File('logging-service-account.json');
if (!serviceAccountFile.existsSync()) {
Expand Down
1 change: 0 additions & 1 deletion example/googleapis/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ environment:

dependencies:
async: ^1.13.3
googleapis_auth: ^0.2.3+6
grpc:
path: ../../
protobuf: ^0.7.0
Expand Down
162 changes: 155 additions & 7 deletions interop/lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import 'dart:typed_data';

import 'package:collection/collection.dart';
import 'package:grpc/grpc.dart';

import 'package:interop/src/generated/empty.pb.dart';
import 'package:interop/src/generated/messages.pb.dart';
import 'package:interop/src/generated/test.pbgrpc.dart';
Expand All @@ -40,6 +39,17 @@ class Tester {
String defaultServiceAccount;
String oauthScope;
String serviceAccountKeyFile;
String _serviceAccountJson;

String get serviceAccountJson =>
_serviceAccountJson ??= _readServiceAccountJson();

String _readServiceAccountJson() {
if (serviceAccountKeyFile?.isEmpty ?? true) {
throw 'Service account key file not specified.';
}
return new File(serviceAccountKeyFile).readAsStringSync();
}

void set serverPort(String value) {
if (value == null) {
Expand All @@ -61,6 +71,7 @@ class Tester {
_useTestCA = value == 'true';
}

ClientChannel channel;
TestServiceClient client;
UnimplementedServiceClient unimplementedServiceClient;

Expand Down Expand Up @@ -90,7 +101,7 @@ class Tester {
options = new ChannelOptions.insecure();
}

final channel =
channel =
new ClientChannel(serverHost, port: _serverPort, options: options);
client = new TestServiceClient(channel);
unimplementedServiceClient = new UnimplementedServiceClient(channel);
Expand Down Expand Up @@ -124,6 +135,8 @@ class Tester {
return emptyStream();
case 'compute_engine_creds':
return computeEngineCreds();
case 'service_account_creds':
return serviceAccountCreds();
case 'jwt_token_creds':
return jwtTokenCreds();
case 'oauth2_auth_token':
Expand Down Expand Up @@ -458,7 +471,8 @@ class Tester {
responses.map((response) => response.payload.body.length).toList();

if (!new ListEquality().equals(responseLengths, expectedResponses)) {
throw 'Incorrect response lengths received (${responseLengths.join(', ')} != ${expectedResponses.join(', ')})';
throw 'Incorrect response lengths received (${responseLengths.join(
', ')} != ${expectedResponses.join(', ')})';
}
}

Expand Down Expand Up @@ -571,7 +585,8 @@ class Tester {
requests.add(index);
await for (final response in responses) {
if (index >= expectedResponses.length) {
throw 'Received too many responses. $index > ${expectedResponses.length}.';
throw 'Received too many responses. $index > ${expectedResponses
.length}.';
}
if (response.payload.body.length != expectedResponses[index]) {
throw 'Response mismatch for response $index: '
Expand Down Expand Up @@ -638,6 +653,62 @@ class Tester {
/// * clients are free to assert that the response payload body contents are
/// zero and comparing the entire response message against a golden response
Future<Null> computeEngineCreds() async {
final credentials = new ComputeEngineAuthenticator();
final clientWithCredentials =
new TestServiceClient(channel, options: credentials.toCallOptions);

final response = await _sendSimpleRequestForAuth(clientWithCredentials,
fillUsername: true, fillOauthScope: true);

final user = response.username;
final oauth = response.oauthScope;

if (user?.isEmpty ?? true) {
throw 'Username not received.';
}
if (oauth?.isEmpty ?? true) {
throw 'OAuth scope not received.';
}

if (!serviceAccountJson.contains(user)) {
throw 'Got user name $user, which is not a substring of $serviceAccountJson';
}
if (!oauthScope.contains(oauth)) {
throw 'Got OAuth scope $oauth, which is not a substring of $oauthScope';
}
}

/// This test is only for cloud-to-prod path.
///
/// This test verifies unary calls succeed in sending messages while using
/// service account credentials.
///
/// Test caller should set flag `--service_account_key_file` with the path to
/// json key file downloaded from https://console.developers.google.com.
/// Alternately, if using a usable auth implementation, she may specify the
/// file location in the environment variable GOOGLE_APPLICATION_CREDENTIALS.
///
/// Procedure:
/// 1. Client configures the channel to use ServiceAccountCredentials
/// 2. Client calls UnaryCall with:
/// {
/// response_size: 314159
/// payload: {
/// body: 271828 bytes of zeros
/// }
/// fill_username: true
/// }
///
/// Client asserts:
/// * call was successful
/// * received SimpleResponse.username is not empty and is in the json key
/// file used by the auth library. The client can optionally check the
/// username matches the email address in the key file or equals the value
/// of `--default_service_account` flag.
/// * response payload body is 314159 bytes in size
/// * clients are free to assert that the response payload body contents are
/// zero and comparing the entire response message against a golden response
Future<Null> serviceAccountCreds() async {
throw 'Not implemented';
}

Expand Down Expand Up @@ -672,7 +743,19 @@ class Tester {
/// * clients are free to assert that the response payload body contents are
/// zero and comparing the entire response message against a golden response
Future<Null> jwtTokenCreds() async {
throw 'Not implemented';
final credentials = new JwtServiceAccountAuthenticator(serviceAccountJson);
final clientWithCredentials =
new TestServiceClient(channel, options: credentials.toCallOptions);

final response = await _sendSimpleRequestForAuth(clientWithCredentials,
fillUsername: true);
final username = response.username;
if (username?.isEmpty ?? true) {
throw 'Username not received.';
}
if (!serviceAccountJson.contains(username)) {
throw 'Got user name $username, which is not a substring of $serviceAccountJson';
}
}

/// This test is only for cloud-to-prod path and some implementations may run
Expand Down Expand Up @@ -715,7 +798,30 @@ class Tester {
/// check against the json key file or GCE default service account email.
/// * received SimpleResponse.oauth_scope is in `--oauth_scope`
Future<Null> oauth2AuthToken() async {
throw 'Not implemented';
final credentials =
new ServiceAccountAuthenticator(serviceAccountJson, [oauthScope]);
final clientWithCredentials =
new TestServiceClient(channel, options: credentials.toCallOptions);

final response = await _sendSimpleRequestForAuth(clientWithCredentials,
fillUsername: true, fillOauthScope: true);

final user = response.username;
final oauth = response.oauthScope;

if (user?.isEmpty ?? true) {
throw 'Username not received.';
}
if (oauth?.isEmpty ?? true) {
throw 'OAuth scope not received.';
}

if (!serviceAccountJson.contains(user)) {
throw 'Got user name $user, which is not a substring of $serviceAccountJson';
}
if (!oauthScope.contains(oauth)) {
throw 'Got OAuth scope $oauth, which is not a substring of $oauthScope';
}
}

/// Similar to the other auth tests, this test is only for cloud-to-prod path.
Expand Down Expand Up @@ -747,7 +853,49 @@ class Tester {
/// file used by the auth library. The client can optionally check the
/// username matches the email address in the key file.
Future<Null> perRpcCreds() async {
throw 'Not implemented';
final credentials =
new ServiceAccountAuthenticator(serviceAccountJson, [oauthScope]);

final response = await _sendSimpleRequestForAuth(client,
fillUsername: true,
fillOauthScope: true,
options: credentials.toCallOptions);

final user = response.username;
final oauth = response.oauthScope;

if (user?.isEmpty ?? true) {
throw 'Username not received.';
}
if (oauth?.isEmpty ?? true) {
throw 'OAuth scope not received.';
}

if (!serviceAccountJson.contains(user)) {
throw 'Got user name $user, which is not a substring of $serviceAccountJson';
}
if (!oauthScope.contains(oauth)) {
throw 'Got OAuth scope $oauth, which is not a substring of $oauthScope';
}
}

Future<SimpleResponse> _sendSimpleRequestForAuth(TestServiceClient client,
{bool fillUsername: false,
bool fillOauthScope: false,
CallOptions options}) async {
final payload = new Payload()..body = new Uint8List(271828);
final request = new SimpleRequest()
..responseSize = 314159
..payload = payload
..fillUsername = fillUsername
..fillOauthScope = fillOauthScope;
final response = await client.unaryCall(request, options: options);
final receivedBytes = response.payload.body.length;
if (receivedBytes != 314159) {
throw 'Response payload mismatch. Expected 314159 bytes, '
'got ${receivedBytes}.';
}
return response;
}

/// This test verifies that custom metadata in either binary or ascii format
Expand Down
2 changes: 2 additions & 0 deletions lib/grpc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

export 'src/auth/auth.dart';

export 'src/client/call.dart';
export 'src/client/channel.dart';
export 'src/client/client.dart';
Expand Down
Loading

0 comments on commit 7621132

Please sign in to comment.