0.0.5
This commit is contained in:
parent
7b36c0592f
commit
a4fa416dfe
15 changed files with 707 additions and 14 deletions
50
README.md
50
README.md
|
@ -1,5 +1,5 @@
|
|||
# security
|
||||
[![version 0.0.0-alpha+4](https://img.shields.io/badge/pub-v0.0.0--alpha+4-red.svg)](https://pub.dartlang.org/packages/angel_security)
|
||||
[![version 0.0.5](https://img.shields.io/badge/pub-v0.0.5-red.svg)](https://pub.dartlang.org/packages/angel_security)
|
||||
[![build status](https://travis-ci.org/angel-dart/security.svg)](https://travis-ci.org/angel-dart/security)
|
||||
|
||||
Angel middleware designed to enhance application security by patching common Web security
|
||||
|
@ -80,11 +80,53 @@ import 'package:angel_security/helmet.dart';
|
|||
```
|
||||
|
||||
# Service Hooks
|
||||
Also included are a set of service hooks, [ported from FeathersJS](https://github.com/feathersjs/feathers-legacy-authentication-hooks).
|
||||
Also included are a set of service hooks, some [ported from FeathersJS](https://github.com/feathersjs/feathers-legacy-authentication-hooks).
|
||||
Others are created just for Angel.
|
||||
|
||||
```dart
|
||||
import 'package:angel_security/hooks.dart';
|
||||
import 'package:angel_security/hooks.dart' as hooks;
|
||||
```
|
||||
|
||||
Included:
|
||||
* `addUserToParams`
|
||||
* `associateCurrentUser`,
|
||||
* `hashPassword`
|
||||
* `queryWithCurrentUser`
|
||||
* `restrictToAuthenticated`
|
||||
* `restrictToOwner`
|
||||
* `variantPermission`
|
||||
|
||||
Also exported is the helper function `isServerSide`. Use this to determine
|
||||
whether a service method is being called by the server, or by a client.
|
||||
|
||||
# Permissions
|
||||
See the tests.
|
||||
Permissions are a great way to restrict access to resources.
|
||||
|
||||
They take the form of:
|
||||
* `service:foo`
|
||||
* `service:create:*`
|
||||
* `some:arbitrary:permission:*:with:*:a:wild:*card`
|
||||
|
||||
The specifics are up to you.
|
||||
|
||||
```dart
|
||||
var permission = new Permission('admin | users:find');
|
||||
|
||||
// Or:
|
||||
// PermissionBuilders support + and | operators. Operands can be Strings, Permissions or PermissionBuilders.
|
||||
var permission = (new PermissionBuilder('admin') | (new PermissionBuilder('users') + 'find')).toPermission();
|
||||
|
||||
// Transform into middleware
|
||||
app.chain(permission.toMiddleware()).get('/protected', ...);
|
||||
|
||||
// Or as a service hook
|
||||
app.service('protected').beforeModify(permission.toHook());
|
||||
|
||||
// Dynamically create a permission hook.
|
||||
// This helps in situations where the resources you need to protect are dynamic.
|
||||
//
|
||||
// `variantPermission` is included in the `package:angel_security/hooks.dart` library.
|
||||
app.service('posts').beforeModify(variantPermission((e) {
|
||||
return new PermissionBuilder('posts:modify:${e.id}');
|
||||
}));
|
||||
```
|
|
@ -1,2 +1,11 @@
|
|||
/// Coming soon!
|
||||
/// Service hooks to lock down user data.
|
||||
library angel_security.hooks;
|
||||
|
||||
export 'src/hooks/add_user_to_params.dart';
|
||||
export 'src/hooks/associate_current_user.dart';
|
||||
export 'src/hooks/hash_password.dart';
|
||||
export 'src/hooks/is_server_side.dart';
|
||||
export 'src/hooks/query_with_current_user.dart';
|
||||
export 'src/hooks/resrict_to_authenticated.dart';
|
||||
export 'src/hooks/restrict_to_owner.dart';
|
||||
export 'src/hooks/variant_permission.dart';
|
||||
|
|
10
lib/src/hooks/add_user_to_params.dart
Normal file
10
lib/src/hooks/add_user_to_params.dart
Normal file
|
@ -0,0 +1,10 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
|
||||
/// Adds the authed user to `e.params`, only if present in `req.injections`.
|
||||
HookedServiceEventListener addUserToParams({String as, String userKey}) {
|
||||
return (HookedServiceEvent e) {
|
||||
var user = e.request?.grab(userKey ?? 'user');
|
||||
|
||||
if (user != null) e.params[as ?? 'user'] = user;
|
||||
};
|
||||
}
|
47
lib/src/hooks/associate_current_user.dart
Normal file
47
lib/src/hooks/associate_current_user.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'errors.dart';
|
||||
import 'is_server_side.dart';
|
||||
|
||||
/// Adds the authed user's id to `data`.
|
||||
///
|
||||
/// Default [as] is `'userId'`.
|
||||
/// Default [userKey] is `'user'`.
|
||||
HookedServiceEventListener associateCurrentUser(
|
||||
{String as,
|
||||
String userKey,
|
||||
String errorMessage,
|
||||
bool allowNullUserId: false,
|
||||
getId(user),
|
||||
setId(id, user)}) {
|
||||
return (HookedServiceEvent e) async {
|
||||
var fieldName = as?.isNotEmpty == true ? as : 'userId';
|
||||
var user = e.request?.grab(userKey ?? 'user');
|
||||
|
||||
if (user == null) {
|
||||
if (!isServerSide(e))
|
||||
throw new AngelHttpException.forbidden(
|
||||
message: errorMessage ?? Errors.NOT_LOGGED_IN);
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
_getId(user) => getId == null ? user?.id : getId(user);
|
||||
|
||||
var id = await _getId(user);
|
||||
|
||||
if (id == null && allowNullUserId != true)
|
||||
throw new AngelHttpException.notProcessable(
|
||||
message: 'Current user is missing a $fieldName field.');
|
||||
|
||||
_setId(id, user) {
|
||||
if (setId != null)
|
||||
return setId(id, user);
|
||||
else if (user is Map)
|
||||
user[fieldName] = id;
|
||||
else
|
||||
user.userId = id;
|
||||
}
|
||||
|
||||
await _setId(id, e.data);
|
||||
};
|
||||
}
|
6
lib/src/hooks/errors.dart
Normal file
6
lib/src/hooks/errors.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
abstract class Errors {
|
||||
static const String NOT_LOGGED_IN =
|
||||
'You must be logged in to perform this action.';
|
||||
static const String INSUFFICIENT_PERMISSIONS =
|
||||
'You have insufficient permissions to access this resource.';
|
||||
}
|
67
lib/src/hooks/hash_password.dart
Normal file
67
lib/src/hooks/hash_password.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
import 'dart:async';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
|
||||
/// Hashes a user's password using a [Hash] algorithm (Default: [sha256]).
|
||||
///
|
||||
/// You may provide your own functions to obtain or set a user's password,
|
||||
/// or just provide a [passwordField] if you are only ever going to deal with Maps.
|
||||
HookedServiceEventListener hashPassword(
|
||||
{Hash hasher,
|
||||
String passwordField,
|
||||
getPassword(user),
|
||||
setPassword(password, user)}) {
|
||||
Hash h = hasher ?? sha256;
|
||||
|
||||
return (HookedServiceEvent e) async {
|
||||
_getPassword(user) {
|
||||
if (getPassword != null)
|
||||
return getPassword(user);
|
||||
else if (user is Map)
|
||||
return user[passwordField ?? 'password'];
|
||||
else
|
||||
return user?.password;
|
||||
}
|
||||
|
||||
_setPassword(password, user) {
|
||||
if (setPassword != null)
|
||||
return setPassword(password, user);
|
||||
else if (user is Map)
|
||||
user[passwordField ?? 'password'] = password;
|
||||
else
|
||||
user?.password = password;
|
||||
}
|
||||
|
||||
if (e.data != null) {
|
||||
var password;
|
||||
|
||||
if (e.data is Iterable) {
|
||||
for (var data in e.data) {
|
||||
var p = await _getPassword(data);
|
||||
|
||||
if (p != null) {
|
||||
password = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else
|
||||
password = await _getPassword(e.data);
|
||||
|
||||
if (password != null) {
|
||||
applyHash(user) async {
|
||||
var password = (await _getPassword(user))?.toString();
|
||||
var digest = h.convert(password.codeUnits);
|
||||
return _setPassword(new String.fromCharCodes(digest.bytes), user);
|
||||
}
|
||||
|
||||
if (e.data is Iterable) {
|
||||
var data = await Future.wait(e.data.map(applyHash));
|
||||
e.data = e.data is List ? data.toList() : data;
|
||||
} else
|
||||
e.data = await applyHash(e.data);
|
||||
|
||||
// TODO (thosakwe): Add salting capability
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
4
lib/src/hooks/is_server_side.dart
Normal file
4
lib/src/hooks/is_server_side.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
|
||||
/// Returns `true` if the event was triggered server-side.
|
||||
bool isServerSide(HookedServiceEvent e) => !e.params.containsKey('provider');
|
41
lib/src/hooks/query_with_current_user.dart
Normal file
41
lib/src/hooks/query_with_current_user.dart
Normal file
|
@ -0,0 +1,41 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'errors.dart';
|
||||
import 'is_server_side.dart';
|
||||
|
||||
/// Adds the authed user's id to `params['query']`.
|
||||
///
|
||||
/// Default [as] is `'userId'`.
|
||||
/// Default [userKey] is `'user'`.
|
||||
HookedServiceEventListener queryWithCurrentUser(
|
||||
{String as,
|
||||
String userKey,
|
||||
String errorMessage,
|
||||
bool allowNullUserId: false,
|
||||
getId(user)}) {
|
||||
return (HookedServiceEvent e) async {
|
||||
var fieldName = as?.isNotEmpty == true ? as : 'userId';
|
||||
var user = e.request?.grab(userKey ?? 'user');
|
||||
|
||||
if (user == null) {
|
||||
if (!isServerSide(e))
|
||||
throw new AngelHttpException.forbidden(
|
||||
message: errorMessage ?? Errors.NOT_LOGGED_IN);
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
_getId(user) => getId == null ? user?.id : getId(user);
|
||||
|
||||
var id = await _getId(user);
|
||||
|
||||
if (id == null && allowNullUserId != true)
|
||||
throw new AngelHttpException.notProcessable(
|
||||
message: 'Current user is missing a $fieldName field.');
|
||||
|
||||
var data = {fieldName: id};
|
||||
|
||||
e.params['query'] = e.params.containsKey('query')
|
||||
? (e.params['query']..addAll(data))
|
||||
: data;
|
||||
};
|
||||
}
|
19
lib/src/hooks/resrict_to_authenticated.dart
Normal file
19
lib/src/hooks/resrict_to_authenticated.dart
Normal file
|
@ -0,0 +1,19 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'errors.dart';
|
||||
import 'is_server_side.dart';
|
||||
|
||||
/// Restricts the service method to authed users only.
|
||||
HookedServiceEventListener restrictToAuthenticated(
|
||||
{String userKey, String errorMessage}) {
|
||||
return (HookedServiceEvent e) async {
|
||||
var user = e.request?.grab(userKey ?? 'user');
|
||||
|
||||
if (user == null) {
|
||||
if (!isServerSide(e))
|
||||
throw new AngelHttpException.forbidden(
|
||||
message: errorMessage ?? Errors.NOT_LOGGED_IN);
|
||||
else
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
54
lib/src/hooks/restrict_to_owner.dart
Normal file
54
lib/src/hooks/restrict_to_owner.dart
Normal file
|
@ -0,0 +1,54 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'errors.dart';
|
||||
import 'is_server_side.dart';
|
||||
|
||||
/// Restricts users to accessing only their own resources.
|
||||
HookedServiceEventListener restrictToOwner(
|
||||
{String userKey, String errorMessage, getId(user), getOwner(obj)}) {
|
||||
return (HookedServiceEvent e) async {
|
||||
if (!isServerSide(e)) {
|
||||
var user = e.request?.grab(userKey ?? 'user');
|
||||
|
||||
if (user == null)
|
||||
throw new AngelHttpException.notAuthenticated(
|
||||
message:
|
||||
'The current user is missing. You must not be authenticated.');
|
||||
|
||||
_getId(user) {
|
||||
if (getId != null)
|
||||
return getId(user);
|
||||
else if (user is Map)
|
||||
return user['id'];
|
||||
else
|
||||
return user.id;
|
||||
}
|
||||
|
||||
var id = await _getId(user);
|
||||
|
||||
if (id == null) throw new Exception('The current user has no ID.');
|
||||
|
||||
var resource = await e.service.read(
|
||||
e.id,
|
||||
{}
|
||||
..addAll(e.params ?? {})
|
||||
..remove('provider'));
|
||||
|
||||
if (resource != null) {
|
||||
_getOwner(obj) {
|
||||
if (getOwner != null)
|
||||
return getOwner(obj);
|
||||
else if (obj is Map)
|
||||
return obj['userId'];
|
||||
else
|
||||
return obj.userId;
|
||||
}
|
||||
|
||||
var ownerId = await _getOwner(resource);
|
||||
|
||||
if ((ownerId is Iterable && !ownerId.contains(id)) || ownerId != id)
|
||||
throw new AngelHttpException.forbidden(
|
||||
message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
30
lib/src/hooks/variant_permission.dart
Normal file
30
lib/src/hooks/variant_permission.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import '../permissions.dart';
|
||||
|
||||
/// Generates a [Permission] based on the situation, and runs it as a hook.
|
||||
///
|
||||
/// This is ideal for cases when you want to limit permissions to a dynamic
|
||||
/// resource.
|
||||
HookedServiceEventListener variantPermission(
|
||||
createPermission(HookedServiceEvent e),
|
||||
{String errorMessage,
|
||||
String userKey,
|
||||
bool owner: false,
|
||||
getRoles(user),
|
||||
getId(user),
|
||||
getOwner(obj)}) {
|
||||
return (HookedServiceEvent e) async {
|
||||
var permission = await createPermission(e);
|
||||
|
||||
if (permission is! Permission)
|
||||
throw new ArgumentError(
|
||||
'createPermission must generate a Permission, whether synchronously or asynchronously.');
|
||||
await permission.toHook(
|
||||
errorMessage: errorMessage,
|
||||
userKey: userKey,
|
||||
owner: owner,
|
||||
getRoles: getRoles,
|
||||
getId: getId,
|
||||
getOwner: getOwner)(e);
|
||||
};
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'hooks/errors.dart';
|
||||
import 'hooks/restrict_to_owner.dart';
|
||||
|
||||
/// Easy mechanism to restrict access to services or routes.
|
||||
class Permission {
|
||||
/// A string representation of the minimum required privilege required
|
||||
/// to access a resource.
|
||||
final String minimum;
|
||||
|
||||
Permission(this.minimum);
|
||||
|
@ -10,28 +14,47 @@ class Permission {
|
|||
return toMiddleware()(req, res);
|
||||
}
|
||||
|
||||
/// Creates a hook that restricts a service method to users with this
|
||||
/// permission, or if they are the resource [owner].
|
||||
///
|
||||
/// [getId] and [getOwner] are passed to [restrictToOwner], along with
|
||||
/// [userKey] and [errorMessage].
|
||||
HookedServiceEventListener toHook(
|
||||
{String message, String userKey, getRoles(user)}) {
|
||||
{String errorMessage,
|
||||
String userKey,
|
||||
bool owner: false,
|
||||
getRoles(user),
|
||||
getId(user),
|
||||
getOwner(obj)}) {
|
||||
return (HookedServiceEvent e) async {
|
||||
if (e.params.containsKey('provider')) {
|
||||
var user = e.request.grab(userKey ?? 'user');
|
||||
|
||||
if (user == null)
|
||||
throw new AngelHttpException.forbidden(
|
||||
message: message ??
|
||||
'You have insufficient permissions to perform this action.');
|
||||
message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS);
|
||||
|
||||
var roleFinder = getRoles ?? (user) async => user.roles ?? [];
|
||||
List<String> roles = (await roleFinder(user)).toList();
|
||||
|
||||
if (!roles.any(verify))
|
||||
if (!roles.any(verify)) {
|
||||
// Try owner if the roles are not in-place
|
||||
if (owner == true) {
|
||||
var listener = restrictToOwner(
|
||||
userKey: userKey,
|
||||
errorMessage: errorMessage,
|
||||
getId: getId,
|
||||
getOwner: getOwner);
|
||||
await listener(e);
|
||||
} else
|
||||
throw new AngelHttpException.forbidden(
|
||||
message: message ??
|
||||
'You have insufficient permissions to perform this action.');
|
||||
message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Restricts a route to users who have sufficient permissions.
|
||||
RequestMiddleware toMiddleware(
|
||||
{String message, String userKey, getRoles(user)}) {
|
||||
return (RequestContext req, ResponseContext res) async {
|
||||
|
@ -54,6 +77,8 @@ class Permission {
|
|||
};
|
||||
}
|
||||
|
||||
/// Returns `true` if the [given] permission string
|
||||
/// represents a sufficient permission, matching the [minimum].
|
||||
bool verify(String given) {
|
||||
bool verifyOne(String minimum) {
|
||||
if (minimum == '*') return true;
|
||||
|
@ -88,24 +113,55 @@ class Permission {
|
|||
String toString() => 'Permission: $minimum';
|
||||
}
|
||||
|
||||
/// Builds [Permission]s.
|
||||
class PermissionBuilder {
|
||||
String _min;
|
||||
|
||||
/// A minimum
|
||||
PermissionBuilder(this._min);
|
||||
|
||||
factory PermissionBuilder.wildcard() => new PermissionBuilder('*');
|
||||
|
||||
PermissionBuilder operator +(other) {
|
||||
if (other is String)
|
||||
return add(other);
|
||||
else if (other is PermissionBuilder)
|
||||
return add(other._min);
|
||||
else if (other is Permission)
|
||||
return add(other.minimum);
|
||||
else
|
||||
throw new ArgumentError(
|
||||
'Cannot add a ${other.runtimeType} to a PermissionBuilder.');
|
||||
}
|
||||
|
||||
PermissionBuilder operator |(other) {
|
||||
if (other is String)
|
||||
return or(new PermissionBuilder(other));
|
||||
else if (other is PermissionBuilder)
|
||||
return or(other);
|
||||
else if (other is Permission)
|
||||
return or(new PermissionBuilder(other.minimum));
|
||||
else
|
||||
throw new ArgumentError(
|
||||
'Cannot or a ${other.runtimeType} and a PermissionBuilder.');
|
||||
}
|
||||
|
||||
call(RequestContext req, ResponseContext res) => toPermission()(req, res);
|
||||
|
||||
/// Adds another level of [constraint].
|
||||
PermissionBuilder add(String constraint) =>
|
||||
new PermissionBuilder('$_min:$constraint');
|
||||
|
||||
/// Adds a wildcard permission.
|
||||
PermissionBuilder allowAll() => add('*');
|
||||
|
||||
/// Duplicates this builder.
|
||||
PermissionBuilder clone() => new PermissionBuilder(_min);
|
||||
|
||||
/// Allows an alternative permission.
|
||||
PermissionBuilder or(PermissionBuilder other) =>
|
||||
new PermissionBuilder('$_min | ${other._min}');
|
||||
|
||||
/// Builds a [Permission].
|
||||
Permission toPermission() => new Permission(_min);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: angel_security
|
||||
version: 0.0.0-alpha+4
|
||||
version: 0.0.5
|
||||
description: Angel middleware designed to enhance application security by patching common Web security holes.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
environment:
|
||||
|
|
280
test/hooks_test.dart
Normal file
280
test/hooks_test.dart
Normal file
|
@ -0,0 +1,280 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_security/angel_security.dart';
|
||||
import 'package:angel_security/hooks.dart' as hooks;
|
||||
import 'package:angel_test/angel_test.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
main() {
|
||||
Angel app;
|
||||
TestClient client;
|
||||
|
||||
setUp(() async {
|
||||
app = new Angel()
|
||||
..before.add((RequestContext req, res) async {
|
||||
var xUser = req.headers.value('X-User');
|
||||
if (xUser != null)
|
||||
req.inject('user',
|
||||
new User(id: xUser, roles: xUser == 'John' ? ['foo:bar'] : []));
|
||||
return true;
|
||||
});
|
||||
|
||||
app
|
||||
..use('/user_data', new UserDataService())
|
||||
..use('/artists', new ArtistService())
|
||||
..use('/roled', new RoledService());
|
||||
|
||||
app.service('user_data')
|
||||
..beforeIndexed.listen(hooks.queryWithCurrentUser())
|
||||
..beforeCreated.listen(hooks.hashPassword());
|
||||
|
||||
app.service('artists')
|
||||
..beforeIndexed.listen(hooks.restrictToAuthenticated())
|
||||
..beforeRead.listen(hooks.restrictToOwner())
|
||||
..beforeCreated.listen(hooks.associateCurrentUser());
|
||||
|
||||
app.service('roled')
|
||||
..beforeIndexed.listen(new Permission('foo:*').toHook())
|
||||
..beforeRead.listen(new Permission('foo:*').toHook(owner: true));
|
||||
|
||||
app.fatalErrorStream.listen((e) {
|
||||
print('Fatal: ${e.error}');
|
||||
print(e.stack);
|
||||
});
|
||||
|
||||
client = await connectTo(app);
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
group('associateCurrentUser', () {
|
||||
test('fail', () async {
|
||||
try {
|
||||
var response = await client.service('artists').create({'foo': 'bar'});
|
||||
print(response);
|
||||
throw new StateError('Creating without userId bad request');
|
||||
} catch (e) {
|
||||
print(e);
|
||||
expect(e, new isInstanceOf<AngelHttpException>());
|
||||
var err = e as AngelHttpException;
|
||||
expect(err.statusCode, equals(403));
|
||||
}
|
||||
});
|
||||
|
||||
test('succeed', () async {
|
||||
var response = await client
|
||||
.post('/artists', headers: {'X-User': 'John'}, body: {'foo': 'bar'});
|
||||
print('Response: ${response.body}');
|
||||
expect(response, allOf(hasStatus(200), isJson({'foo': 'bar'})));
|
||||
});
|
||||
});
|
||||
|
||||
group('queryWithCurrentUser', () {
|
||||
test('fail', () async {
|
||||
try {
|
||||
var response = await client.service('user_data').index();
|
||||
print(response);
|
||||
throw new StateError('Indexing without user forbidden');
|
||||
} catch (e) {
|
||||
print(e);
|
||||
expect(e, new isInstanceOf<AngelHttpException>());
|
||||
var err = e as AngelHttpException;
|
||||
expect(err.statusCode, equals(403));
|
||||
}
|
||||
});
|
||||
|
||||
test('succeed', () async {
|
||||
var response = await client.get('user_data', headers: {'X-User': 'John'});
|
||||
print('Response: ${response.body}');
|
||||
expect(response, allOf(hasStatus(200), isJson(['foo', 'bar'])));
|
||||
});
|
||||
});
|
||||
|
||||
test('hashPassword', () async {
|
||||
var response = await client
|
||||
.service('user_data')
|
||||
.create({'username': 'foo', 'password': 'jdoe1'});
|
||||
print('Response: ${response}');
|
||||
expect(response, equals({'foo': 'bar'}));
|
||||
});
|
||||
|
||||
group('restrictToAuthenticated', () {
|
||||
test('fail', () async {
|
||||
try {
|
||||
var response = await client.service('artists').index();
|
||||
print(response);
|
||||
throw new StateError('Indexing without user forbidden');
|
||||
} catch (e) {
|
||||
print(e);
|
||||
expect(e, new isInstanceOf<AngelHttpException>());
|
||||
var err = e as AngelHttpException;
|
||||
expect(err.statusCode, equals(403));
|
||||
}
|
||||
});
|
||||
|
||||
test('succeed', () async {
|
||||
var response = await client.get('/artists', headers: {'X-User': 'John'});
|
||||
print('Response: ${response.body}');
|
||||
expect(
|
||||
response,
|
||||
allOf(
|
||||
hasStatus(200),
|
||||
isJson([
|
||||
{
|
||||
"id": "king_of_pop",
|
||||
"userId": "John",
|
||||
"name": "Michael Jackson"
|
||||
},
|
||||
{"id": "raymond", "userId": "Bob", "name": "Usher"}
|
||||
])));
|
||||
});
|
||||
});
|
||||
|
||||
group('restrictToOwner', () {
|
||||
test('fail', () async {
|
||||
try {
|
||||
var response = await client.service('artists').read('king_of_pop');
|
||||
print(response);
|
||||
throw new StateError('Reading without owner forbidden');
|
||||
} catch (e) {
|
||||
print(e);
|
||||
expect(e, new isInstanceOf<AngelHttpException>());
|
||||
var err = e as AngelHttpException;
|
||||
expect(err.statusCode, equals(401));
|
||||
}
|
||||
});
|
||||
|
||||
test('succeed', () async {
|
||||
var response =
|
||||
await client.get('/artists/king_of_pop', headers: {'X-User': 'John'});
|
||||
print('Response: ${response.body}');
|
||||
expect(
|
||||
response,
|
||||
allOf(
|
||||
hasStatus(200),
|
||||
isJson({
|
||||
"id": "king_of_pop",
|
||||
"userId": "John",
|
||||
"name": "Michael Jackson"
|
||||
})));
|
||||
});
|
||||
});
|
||||
|
||||
group('permission restrict', () {
|
||||
test('fail', () async {
|
||||
try {
|
||||
var response = await client.service('roled').index();
|
||||
print(response);
|
||||
throw new StateError('Reading without roles forbidden');
|
||||
} catch (e) {
|
||||
print(e);
|
||||
expect(e, new isInstanceOf<AngelHttpException>());
|
||||
var err = e as AngelHttpException;
|
||||
expect(err.statusCode, equals(403));
|
||||
}
|
||||
});
|
||||
|
||||
test('succeed', () async {
|
||||
var response =
|
||||
await client.get('/roled/king_of_pop', headers: {'X-User': 'John'});
|
||||
print('Response: ${response.body}');
|
||||
expect(
|
||||
response,
|
||||
allOf(
|
||||
hasStatus(200),
|
||||
isJson({
|
||||
"id": "king_of_pop",
|
||||
"userId": "John",
|
||||
"name": "Michael Jackson"
|
||||
})));
|
||||
});
|
||||
|
||||
test('owner', () async {
|
||||
var response =
|
||||
await client.get('/roled/raymond', headers: {'X-User': 'Bob'});
|
||||
print('Response: ${response.body}');
|
||||
expect(
|
||||
response,
|
||||
allOf(hasStatus(200),
|
||||
isJson({"id": "raymond", "userId": "Bob", "name": "Usher"})));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class User {
|
||||
String id;
|
||||
List<String> roles;
|
||||
User({this.id, this.roles: const []});
|
||||
}
|
||||
|
||||
class UserDataService extends Service {
|
||||
static const Map<String, List> _data = const {
|
||||
'John': const ['foo', 'bar']
|
||||
};
|
||||
|
||||
@override
|
||||
index([Map params]) async {
|
||||
print('Params: $params');
|
||||
if (params?.containsKey('query') != true)
|
||||
throw new AngelHttpException.badRequest(message: 'query required');
|
||||
|
||||
String name = params['query']['userId']?.toString();
|
||||
|
||||
if (!_data.containsKey(name))
|
||||
throw new AngelHttpException.notFound(
|
||||
message: "No data found for user '$name'.");
|
||||
|
||||
return _data[name];
|
||||
}
|
||||
|
||||
@override
|
||||
create(data, [Map params]) async {
|
||||
if (data is! Map || !data.containsKey('password'))
|
||||
throw new AngelHttpException.badRequest(message: 'Required password!');
|
||||
|
||||
var expected =
|
||||
new String.fromCharCodes(sha256.convert('jdoe1'.codeUnits).bytes);
|
||||
|
||||
if (data['password'] != (expected))
|
||||
throw new AngelHttpException.conflict(message: 'Passwords do not match.');
|
||||
return {'foo': 'bar'};
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistService extends Service {
|
||||
static const List<Artist> _ARTISTS = const [_MICHAEL_JACKSON, _USHER];
|
||||
|
||||
@override
|
||||
index([params]) async => _ARTISTS;
|
||||
|
||||
@override
|
||||
read(id, [params]) async => _ARTISTS.firstWhere((a) => a.id == id);
|
||||
|
||||
@override
|
||||
create(data, [params]) async {
|
||||
if (data is! Map || !data.containsKey('userId'))
|
||||
throw new AngelHttpException.badRequest(message: 'Required userId');
|
||||
|
||||
return {'foo': 'bar'};
|
||||
}
|
||||
}
|
||||
|
||||
class Artist {
|
||||
final String id, userId, name;
|
||||
const Artist({this.id, this.userId, this.name});
|
||||
}
|
||||
|
||||
const Artist _USHER = const Artist(id: 'raymond', userId: 'Bob', name: 'Usher');
|
||||
const Artist _MICHAEL_JACKSON =
|
||||
const Artist(id: 'king_of_pop', userId: 'John', name: 'Michael Jackson');
|
||||
|
||||
class RoledService extends Service {
|
||||
@override
|
||||
index([params]) {
|
||||
return ['foo'];
|
||||
}
|
||||
|
||||
@override
|
||||
read(id, [params]) async =>
|
||||
ArtistService._ARTISTS.firstWhere((a) => a.id == id);
|
||||
}
|
|
@ -14,12 +14,16 @@ main() {
|
|||
.chain(throttleRequests(1, new Duration(hours: 1)))
|
||||
.get('/once-per-hour', 'OK');
|
||||
|
||||
app
|
||||
.chain(throttleRequests(3, new Duration(minutes: 1)))
|
||||
.get('/thrice-per-minute', 'OK');
|
||||
|
||||
client = await connectTo(app);
|
||||
});
|
||||
|
||||
tearDown(() => client.close());
|
||||
|
||||
test('enforce limit', () async {
|
||||
test('once per hour', () async {
|
||||
// First request within the hour is fine
|
||||
var response = await client.get('/once-per-hour');
|
||||
print(response.body);
|
||||
|
@ -30,4 +34,28 @@ main() {
|
|||
print(response.body);
|
||||
expect(response, hasStatus(429));
|
||||
});
|
||||
|
||||
test('thrice per minute', () async {
|
||||
// First request within the minute is fine
|
||||
var response = await client.get('/thrice-per-minute');
|
||||
print(response.body);
|
||||
expect(response.body, contains('OK'));
|
||||
|
||||
|
||||
// Second request within the minute is fine
|
||||
response = await client.get('/thrice-per-minute');
|
||||
print(response.body);
|
||||
expect(response.body, contains('OK'));
|
||||
|
||||
|
||||
// Third request within the minute is fine
|
||||
response = await client.get('/thrice-per-minute');
|
||||
print(response.body);
|
||||
expect(response.body, contains('OK'));
|
||||
|
||||
// Fourth request within a minute? No no no!
|
||||
response = await client.get('/thrice-per-minute');
|
||||
print(response.body);
|
||||
expect(response, hasStatus(429));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue