Skip to content

Commit

Permalink
v1.0.29
Browse files Browse the repository at this point in the history
- Improved `Json.toJson`.
- Added field `APIAuthentication.data`.
- `APISecurity`: added `getAuthenticationData`.
  • Loading branch information
gmpassos committed Oct 19, 2021
1 parent ab75329 commit c9f3683
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 65 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.0.29

- Improved `Json.toJson`.
- Added field `APIAuthentication.data`.
- `APISecurity`: added `getAuthenticationData`.

## 1.0.28

- Added `API-INFO` path: describes the API routes.
Expand Down
13 changes: 10 additions & 3 deletions lib/src/bones_api_authentication.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import 'package:bones_api/src/bones_api_security.dart';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart' as crypto;

import 'bones_api_utils.dart';

/// Represents a authentication credential.
class APICredential {
/// The username/email of this credential.
Expand Down Expand Up @@ -161,8 +163,10 @@ class APIAuthentication {

final bool resumed;

final dynamic data;

APIAuthentication(this.token,
{List<APIPermission>? permissions, this.resumed = false})
{List<APIPermission>? permissions, this.resumed = false, this.data})
: permissions =
List<APIPermission>.unmodifiable(permissions ?? <APIPermission>[]);

Expand Down Expand Up @@ -205,9 +209,12 @@ class APIAuthentication {
enabledPermissionsOfType(type).firstOrNull;

Map<String, dynamic> toJson() => {
'token': token,
'permissions': permissions,
'token': token.toJson(),
if (permissions.isNotEmpty)
'permissions': permissions.map((e) => e.toJson()).toList(),
if (resumed) 'resumed': resumed,
if (data != null)
'data': Json.toJson(data, maskField: Json.standardJsonMaskField),
};
}

Expand Down
2 changes: 1 addition & 1 deletion lib/src/bones_api_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import 'bones_api_utils.dart';
/// Root class of an API.
abstract class APIRoot {
// ignore: constant_identifier_names
static const String VERSION = '1.0.28';
static const String VERSION = '1.0.29';

static final Map<String, APIRoot> _instances = <String, APIRoot>{};

Expand Down
10 changes: 2 additions & 8 deletions lib/src/bones_api_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -407,12 +407,6 @@ class APIConfig {
return 'APIConfig$src$json';
}

bool _maskField(f) {
var k = f.toLowerCase();
return k.contains('pass') ||
k.contains('passphrase') ||
k.contains('pin') ||
k.contains('secret') ||
k.contains('token');
}
bool _maskField(String key) =>
Json.standardJsonMaskField(key, extraKeys: const <String>{'token'});
}
25 changes: 18 additions & 7 deletions lib/src/bones_api_security.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,24 @@ abstract class APISecurity {

return checkCredential(credential).then((ok) {
if (!ok) return null;

return getCredentialPermissions(credential)
.then((permissions) => createAuthentication(credential, permissions));
return _resolveAuthentication(credential, false);
});
}

FutureOr<APIAuthentication?> _resolveAuthentication(
APICredential credential, bool resumed) {
var permissionRet = getCredentialPermissions(credential);
var dataRet = getAuthenticationData(credential);

return permissionRet.resolveOther(
dataRet,
(permissions, data) => createAuthentication(credential, permissions,
data: data, resumed: resumed));
}

APIAuthentication createAuthentication(
APICredential credential, List<APIPermission> permissions,
[bool resumed = false]) {
{Object? data, bool resumed = false}) {
APIToken? token;
if (credential.token != null) {
token =
Expand All @@ -67,7 +76,8 @@ abstract class APISecurity {

token ??= getValidToken(credential.username)!;

return APIAuthentication(token, permissions: permissions, resumed: resumed);
return APIAuthentication(token,
permissions: permissions, data: data, resumed: resumed);
}

FutureOr<APIAuthentication?> resumeAuthentication(APIToken? token) {
Expand All @@ -78,8 +88,7 @@ abstract class APISecurity {
return checkCredential(credential).then((ok) {
if (!ok) return null;

return getCredentialPermissions(credential).then(
(permissions) => createAuthentication(credential, permissions, true));
return _resolveAuthentication(credential, true);
});
}

Expand Down Expand Up @@ -186,6 +195,8 @@ abstract class APISecurity {
FutureOr<List<APIPermission>> getCredentialPermissions(
APICredential credential);

FutureOr<Object?> getAuthenticationData(APICredential credential) => null;

FutureOr<APIAuthentication?> authenticateByRequest(APIRequest request) {
var credential = resolveRequestCredential(request);
credential ??= resolveSessionCredential(request);
Expand Down
156 changes: 111 additions & 45 deletions lib/src/bones_api_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,51 +5,103 @@ import 'package:bones_api/bones_api.dart';
import 'package:collection/collection.dart';
import 'package:reflection_factory/reflection_factory.dart';

typedef ToEncodableJson = Object? Function(Object? object);

typedef JsonFieldMatcher = bool Function(String key);

/// JSON utility class.
class Json {
/// A standard implementation of mask filed.
///
/// - [extraKeys] is the extra keys to mask.
static bool standardJsonMaskField(String key, {Iterable<String>? extraKeys}) {
key = key.trim().toLowerCase();
return key == 'password' ||
key == 'pass' ||
key == 'passwordhash' ||
key == 'passhash' ||
key == 'passphrase' ||
key == 'ping' ||
key == 'secret' ||
key == 'privatekey' ||
key == 'pkey' ||
(extraKeys != null && extraKeys.contains(key));
}

/// Converts [o] to a JSON collection/data.
/// - [maskField] when preset indicates if a field value should be masked with [maskText].
static T? toJson<T>(Object? o,
{bool Function(String key)? maskField,
{JsonFieldMatcher? maskField,
String maskText = '***',
Object? Function(dynamic object)? toEncodable}) {
JsonFieldMatcher? removeField,
ToEncodableJson? toEncodable}) {
return _valueToJson(o, maskField, maskText, removeField, toEncodable) as T;
}

static Object? _valueToJson(o, JsonFieldMatcher? maskField, String maskText,
JsonFieldMatcher? removeField, ToEncodableJson? toEncodable) {
if (o == null) {
return null;
} else if (o is String || o is num || o is bool) {
return o;
} else if (o is DateTime) {
return _dateTimeToJson(o);
} else if (o is Map) {
return _mapToJson(o, maskField, maskText, removeField, toEncodable);
} else if (o is Set) {
return _iterableToJson(o, maskField, maskText, removeField, toEncodable)
.toSet();
} else if (o is Iterable) {
return _iterableToJson(o, maskField, maskText, removeField, toEncodable)
.toList();
} else {
return _entityToJson(o, toEncodable);
}
}

if (o is String || o is num || o is bool) {
return o as T;
}
static Iterable<Object?> _iterableToJson(
Iterable<dynamic> o,
JsonFieldMatcher? maskField,
String maskText,
JsonFieldMatcher? removeField,
ToEncodableJson? toEncodable) {
return o.map(
(e) => _valueToJson(e, maskField, maskText, removeField, toEncodable));
}

if (o is DateTime) {
return _dateTimeToJson(o) as T;
}
static Map<String, dynamic> _mapToJson(
Map<dynamic, dynamic> o,
JsonFieldMatcher? maskField,
String maskText,
JsonFieldMatcher? removeField,
ToEncodableJson? toEncodable) {
var oEntries = o.entries;

if (maskField != null) {
if (o is Map) {
o = _mapToJson(o, maskField, maskText);
} else if (o is Iterable) {
o = o.map((e) {
return e is Map ? _mapToJson(o as Map, maskField, maskText) : e;
}).toList();
}
if (removeField != null) {
oEntries = oEntries.where((e) => !removeField(e.key));
}

return o as T;
var entries = oEntries.map((e) {
var key = e.key;
var value = _mapKeyValueToJson(
key, e.value, maskField, maskText, removeField, toEncodable);
return MapEntry<String, dynamic>(key, value);
});

return Map<String, dynamic>.fromEntries(entries);
}

static String _dateTimeToJson(DateTime o) {
return o.toUtc().toString();
}

static Map<dynamic, dynamic> _mapToJson(
Map o, bool Function(String key)? maskField, String maskText) {
return o.map((key, value) =>
MapEntry(key, _mapKeyToJson(key, value, maskField, maskText)));
}

static dynamic _mapKeyToJson(String k, dynamic o,
bool Function(String key)? maskField, String maskText) {
static Object? _mapKeyValueToJson(
String k,
dynamic o,
JsonFieldMatcher? maskField,
String maskText,
JsonFieldMatcher? removeField,
ToEncodableJson? toEncodable) {
if (o == null) {
return null;
}
Expand All @@ -61,27 +113,41 @@ class Json {
}
}

if (o is String || o is num || o is bool) {
return o;
}
return _valueToJson(o, maskField, maskText, removeField, toEncodable);
}

if (o is DateTime) {
return _dateTimeToJson(o);
static Object? _entityToJson(dynamic o, ToEncodableJson? toEncodable) {
if (toEncodable != null) {
try {
return toEncodable(o);
} catch (_) {
return _entityToJsonImpl(o);
}
} else {
return _entityToJsonImpl(o);
}
}

if (o is Map) {
return o.map((key, value) =>
MapEntry(key, _mapKeyToJson(key, value, maskField, maskText)));
} else if (o is Set) {
return o.map((e) => _mapKeyToJson(k, e, maskField, maskText)).toSet();
} else if (o is Iterable) {
return o.map((e) => _mapKeyToJson(k, e, maskField, maskText)).toList();
} else {
static Object? _entityToJsonImpl(dynamic o) {
var classReflection =
ReflectionFactory().getRegisterClassReflection(o.runtimeType);

if (classReflection != null) {
try {
return o.toJson();
return classReflection.toJson(o);
} catch (_) {
return '$o';
return _entityToJsonDefault(o);
}
} else {
return _entityToJsonDefault(o);
}
}

static _entityToJsonDefault(dynamic o) {
try {
return o.toJson();
} catch (_) {
return '$o';
}
}

Expand All @@ -91,15 +157,15 @@ class Json {
/// - [toEncodable] converts a not encodable [Object] to a encodable JSON collection/data. See [dart_convert.JsonEncoder].
static String encode(Object? o,
{bool pretty = false,
bool Function(String key)? maskField,
JsonFieldMatcher? maskField,
String maskText = '***',
Object? Function(dynamic object)? toEncodable}) {
var json = toJson(o,
maskField: maskField, maskText: maskText, toEncodable: toEncodable);
if (pretty) {
return dart_convert.JsonEncoder.withIndent(' ').convert(toJson(o,
maskField: maskField, maskText: maskText, toEncodable: toEncodable));
return dart_convert.JsonEncoder.withIndent(' ').convert(json);
} else {
return dart_convert.json.encode(toJson(o,
maskField: maskField, maskText: maskText, toEncodable: toEncodable));
return dart_convert.json.encode(json);
}
}

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: bones_api
description: Bones_API - A Powerful API backend framework for Dart. Comes with a built-in HTTP Server, routes handler, entity handler, SQL translator, and DB adapters.
version: 1.0.28
version: 1.0.29
homepage: https://github.com/Colossus-Services/bones_api

environment:
Expand Down
36 changes: 36 additions & 0 deletions test/bones_api_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,48 @@ import 'package:test/test.dart';

final _log = logging.Logger('bones_api_test');

class Foo {
int id;

String name;

Foo(this.id, this.name);

@override
String toString() {
return '#$id[$name]';
}
}

void main() {
_log.handler.logToConsole();

group('Utils', () {
setUp(() {});

test('Json.toJson', () async {
expect(Json.toJson(123), equals(123));
expect(Json.toJson(DateTime.utc(2021, 1, 2, 3, 4, 5)),
equals('2021-01-02 03:04:05.000Z'));

expect(
Json.toJson({'a': 1, 'b': 2, 'p': 123}, removeField: (k) => k == 'p'),
equals({'a': 1, 'b': 2}));

expect(
Json.toJson({'a': 1, 'b': 2, 'p': 123}, maskField: (k) => k == 'p'),
equals({'a': 1, 'b': 2, 'p': '***'}));

expect(Json.toJson({'a': 1, 'b': 2, 'foo': Foo(51, 'x')}),
equals({'a': 1, 'b': 2, 'foo': '#51[x]'}));

expect(
Json.toJson({'a': 1, 'b': 2, 'foo': Foo(51, 'x')}, toEncodable: (o) {
return o is Foo ? '${o.id}:${o.name}' : o;
}),
equals({'a': 1, 'b': 2, 'foo': '51:x'}));
});

test('Json.encode', () async {
expect(Json.encode({'a': 1, 'b': 2}), equals('{"a":1,"b":2}'));

Expand Down

0 comments on commit c9f3683

Please sign in to comment.