Skip to content

Commit

Permalink
fix: move to delegate, add tests, revert version requirement
Browse files Browse the repository at this point in the history
  • Loading branch information
Nexushunter committed Sep 1, 2023
1 parent bc4a170 commit 6a32542
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 273 deletions.
Expand Up @@ -183,8 +183,10 @@ class InputSpec {

const InputSpec.emptyJson() : this(defaultYaml: false);

Map<String, dynamic> toJsonMap() =>
{'path': path, 'defaultYaml': defaultYaml};
Map<String, dynamic> toJsonMap() => {
'path': path,
'defaultYaml': defaultYaml,
};

InputSpec.fromMap(Map<String, dynamic> map)
: this(
Expand All @@ -204,196 +206,124 @@ class InputSpec {
/// This contains authentication information for fetching the OAS spec ONLY. This
/// does not apply security to the entry points defined in the OAS spec.
class RemoteSpec extends InputSpec {
final String? authHeaderContent;
final RemoteSpecHeaderDelegate headerDelegate;

const RemoteSpec({
required super.path,
this.authHeaderContent,
});
required String path,
this.headerDelegate = const RemoteSpecHeaderDelegate(),
}) : super(path: path);

const RemoteSpec.empty() : this(path: 'http://localhost:8080/');

Uri get url => Uri.parse(path);

Map<String, String> toHeaderMap() {
return {
if (authHeaderContent != null)
'Authorization': 'Bearer ${authHeaderContent}',
};
}

Map<String, dynamic> toJsonMap() {
return {
if (authHeaderContent != null) 'authHeaderContent': authHeaderContent!,
...super.toJsonMap(),
};
}

RemoteSpec.fromMap(Map<String, dynamic> map)
: authHeaderContent = map['authHeaderContent'],
: headerDelegate = map['headerDelegate'],
super.fromMap(map);
}

/// Default [RemoteSpecHeaderDelegate] used when retrieving a remote OAS spec.
class RemoteSpecHeaderDelegate {
const RemoteSpecHeaderDelegate();

Map<String, String>? header() => null;

RemoteSpecHeaderDelegate.fromMap(Map<String, dynamic> map) : this();
}

/// Indicates whether or not the spec file live within AWS.
///
/// Since AWS handles the authentication header differently, we need to inform
/// the builder to include the alternate auth header.
///
/// This currently only support AWS S3.
/// This currently only supports AWS Object GET.
///
/// This contains authentication information for fetching the OAS spec ONLY. This
/// does not apply security to the entry points defined in the OAS spec.
class AwsRemoteSpec extends RemoteSpec {
/// The accessKeyId use to interact with AWS.
///
/// When this is null authentication will fail.
final String? _accessKeyId;

@visibleForTesting
String? get accessKeyId => _accessKeyId;

/// The secretAccessKey for the user.
///
/// When this is null the requests wil not be authenticated.
final String? _secretAccessKey;

@visibleForTesting
String? get secretAccessKey => _secretAccessKey;
///
/// This delegate makes the assumption that the AWS credentials to be used are
/// provided by the environment and are not empty.
class AWSRemoteSpecHeaderDelegate extends RemoteSpecHeaderDelegate {
/// The [bucket] where the OAS spec is stored within AWS.
final String bucket;
final String? accessKeyId;
final String? secretAccessKey;

/// The region where the AWS resource resides.
final String region;
const AWSRemoteSpecHeaderDelegate({
required this.bucket,
this.secretAccessKey = null,
this.accessKeyId = null,
}) : super();

/// The S3 bucket name.
final String bucket;
AWSRemoteSpecHeaderDelegate.fromMap(Map<String, dynamic> map)
: bucket = map['bucket'],
accessKeyId = map['accessKeyId'],
secretAccessKey = map['secretAccessKey'],
super.fromMap(map);

/// The current timestamp.
/// Generates the [header] map used within the GET request.
///
/// This shouldn't be manually provided.
final DateTime? now;
/// Assumes that the user's auth AWS credentials
@override
Map<String, String>? header({
String? path,
}) {
if (!(path != null && path.isNotEmpty)) {
throw new AssertionError('The path to the OAS spec should be provided');
}

/// Creates a reference to an OAS spec hosted within AWS S3.
///
/// [accessKeyId] User AWS accessKeyId
/// [secretAccessKeyId] User AWS secretAccessKeyId
/// [region] The region to target within AWS.
/// [bucket] The S3 bucket to target.
/// [path] The path of the OAS spec from the root of the [bucket] without the
/// leading /.
const AwsRemoteSpec({
String? accessKeyId,
String? secretAccessKey,
required this.region,
required this.bucket,
required super.path,
this.now,
}) : _secretAccessKey = secretAccessKey,
_accessKeyId = accessKeyId;
// Use the provided credentials to the constructor, if any, otherwise
// fallback to the environment values or throw.
final accessKey = accessKeyId ?? Platform.environment['AWS_ACCESS_KEY_ID'];
final secretKey =
secretAccessKey ?? Platform.environment['AWS_SECRET_ACCESS_KEY'];
if ((accessKey == null || accessKey.isEmpty) ||
(secretKey == null || secretKey.isEmpty)) {
throw new AssertionError(
'AWS_SECRET_KEY_ID & AWS_SECRET_ACCESS_KEY should be defined and not empty or they should be provided in the delegate constructor.');
}

/// The url of the OAS spec within S3.
@override
Uri get url => Uri.https('$bucket.s3.$region.amazonaws.com', '/$path');
final now = DateTime.now();

return {
'Authorization': authHeaderContent(
now: now,
bucket: bucket,
path: path,
accessKeyId: accessKey,
secretAccessKey: secretKey,
),
'x-amz-date': now.toIso8601String(),
};
}

/// The [Authentication] header content.
///
/// This builds the Authorization header content format used by AWS.
///
/// Multiple calls will result in the same content unless [toHeaderMap] has been
/// called in between.
String get authHeaderContent {
if (_secretAccessKey == null || _accessKeyId == null) {
return '';
}
@visibleForTesting
String authHeaderContent({
required DateTime now,
required String bucket,
required String path,
required String accessKeyId,
required String secretAccessKey,
}) {
// https://docs.aws.amazon.com/AmazonS3/latest/userguide/RESTAuthentication.html#RESTAuthenticationExamples
String toSign = [
'GET',
'',
'',
now,
now.toIso8601String(),
'/$bucket/$path',
].join('\n');

final utf8AKey = utf8.encode(_secretAccessKey!);
final utf8AKey = utf8.encode(secretAccessKey);
final utf8ToSign = utf8.encode(toSign);

final signature =
base64Encode(Hmac(sha1, utf8AKey).convert(utf8ToSign).bytes);
return 'AWS $_accessKeyId:$signature';
}

/// Load the user credentials from the environment.
AwsRemoteSpec loadCredentials() => AwsRemoteSpec(
region: region,
bucket: bucket,
path: path,
accessKeyId: Platform.environment['AWS_ACCESS_KEY_ID'],
secretAccessKey: Platform.environment['AWS_SECRET_ACCESS_KEY'],
now: DateTime.now(),
);

/// Builds the headers map for the authenticated request.
///
/// This will return an empty map when when the [_accessKeyId] or [_secretAccessKey]
/// are empty after attempting to fetch them from the environment.
Map<String, String> toHeaderMap() {
AwsRemoteSpec specAuth = this;
if (_accessKeyId == null || _secretAccessKey == null || now == null) {
specAuth = loadCredentials();
if (specAuth.accessKeyId == null ||
specAuth.secretAccessKey == null ||
now == null) {
// TODO: This should probably throw or assert
return {};
}
}

return {
'Authorization': authHeaderContent,
'x-amz-date': now!.toIso8601String(),
};
}

Map<String, String> toJsonMap() {
return {
if (_accessKeyId != null) 'accessKeyId': _accessKeyId!,
if (_secretAccessKey != null) 'secretAccessKey': _secretAccessKey!,
'path': path,
'bucket': bucket,
'region': region,
...super.toJsonMap(),
};
return 'AWS $accessKeyId:$signature';
}

AwsRemoteSpec.fromMap(Map<String, dynamic> map)
: bucket = map['bucket'],
region = map['region'],
_accessKeyId = map['accessKeyId'],
_secretAccessKey = map['secretAccessKey'],
now = DateTime.now(),
super.fromMap(map);
}

/// A localstack remote spec for testing AWS like requests.
@visibleForTesting
class LocalStackRemoteSpec extends AwsRemoteSpec {
final int localStackPort;

const LocalStackRemoteSpec({
super.path,
this.localStackPort = 4566,
}) : super(
region: 'us-east-1',
accessKeyId: 'test',
secretAccessKey: 'test',
bucket: 'bucket',
);

Uri get url => Uri.http(
'$bucket.s3.$region.localhost.localstack.cloud:$localStackPort',
'/$path');

LocalStackRemoteSpec.fromMap(Map<String, dynamic> map)
: localStackPort = map['localStackPort'] ?? 4566,
super.fromMap(map);
}

class AdditionalProperties {
Expand Down
2 changes: 1 addition & 1 deletion openapi-generator-annotations/pubspec.yaml
Expand Up @@ -5,7 +5,7 @@ homepage: https://github.com/gibahjoe/openapi-generator-dart


environment:
sdk: '>=2.17.0 <3.0.0'
sdk: '>=2.12.0 <3.0.0'

dev_dependencies:
test:
Expand Down

0 comments on commit 6a32542

Please sign in to comment.