Wipe existing project
This commit is contained in:
parent
c1b1c5a06e
commit
46733f311c
28 changed files with 1 additions and 1768 deletions
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
/// Angel middleware designed to enhance application security.
|
|
||||||
library angel_security;
|
|
||||||
|
|
||||||
export 'src/ban.dart';
|
|
||||||
export 'src/csrf.dart';
|
|
||||||
export 'src/permissions.dart';
|
|
||||||
export 'src/sanitize.dart';
|
|
||||||
export 'src/throttle.dart';
|
|
||||||
export 'src/trust_proxy.dart';
|
|
|
@ -1,11 +0,0 @@
|
||||||
/// 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';
|
|
|
@ -1,98 +0,0 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
|
|
||||||
/// Throws a 403 Forbidden if the user's IP is banned.
|
|
||||||
///
|
|
||||||
/// [filter] can be:
|
|
||||||
/// A `String`, `RegExp`, `InternetAddress`, or an `Iterable`.
|
|
||||||
///
|
|
||||||
/// String can take the following formats:
|
|
||||||
/// 1. 1.2.3.4
|
|
||||||
/// 2. 1.2.3.*, 1.2.*.*, etc.
|
|
||||||
RequestHandler banIp(filter,
|
|
||||||
{String message =
|
|
||||||
'Your IP address is forbidden from accessing this server.'}) {
|
|
||||||
var filters = [];
|
|
||||||
Iterable inputs = filter is Iterable ? filter : [filter];
|
|
||||||
|
|
||||||
for (var input in inputs) {
|
|
||||||
if (input is RegExp || input is InternetAddress)
|
|
||||||
filters.add(input);
|
|
||||||
else if (input is String) {
|
|
||||||
if (!input.contains('*'))
|
|
||||||
filters.add(input);
|
|
||||||
else {
|
|
||||||
filters.add(RegExp(input.replaceAll('*', '[0-9]+')));
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
throw ArgumentError('Cannot use $input as an IP filter.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (RequestContext req, ResponseContext res) async {
|
|
||||||
var ip = req.ip;
|
|
||||||
|
|
||||||
bool check() {
|
|
||||||
for (var input in filters) {
|
|
||||||
if (input is RegExp && input.hasMatch(ip))
|
|
||||||
return false;
|
|
||||||
else if (input is InternetAddress && input.address == ip)
|
|
||||||
return false;
|
|
||||||
else if (input is String && input == ip) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!check()) throw AngelHttpException.forbidden(message: message);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Throws a 403 Forbidden if the user's Origin header is banned.
|
|
||||||
///
|
|
||||||
/// [filter] can be:
|
|
||||||
/// A `String`, `RegExp`, or an `Iterable`.
|
|
||||||
///
|
|
||||||
/// String can take the following formats:
|
|
||||||
/// 1. example.com
|
|
||||||
/// 2. *.example.com, a.b.*.d.e.f, etc.
|
|
||||||
RequestHandler banOrigin(filter,
|
|
||||||
{String message = 'You are forbidden from accessing this server.',
|
|
||||||
bool allowEmptyOrigin = false}) {
|
|
||||||
var filters = [];
|
|
||||||
Iterable inputs = filter is Iterable ? filter : [filter];
|
|
||||||
|
|
||||||
for (var input in inputs) {
|
|
||||||
if (input is RegExp)
|
|
||||||
filters.add(input);
|
|
||||||
else if (input is String) {
|
|
||||||
if (!input.contains('*'))
|
|
||||||
filters.add(input);
|
|
||||||
else {
|
|
||||||
filters.add(RegExp(input.replaceAll('*', '[^\.]+')));
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
throw ArgumentError('Cannot use $input as an origin filter.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (RequestContext req, ResponseContext res) async {
|
|
||||||
var origin = req.headers.value('origin');
|
|
||||||
|
|
||||||
if ((origin == null || origin.isEmpty) && !allowEmptyOrigin)
|
|
||||||
throw AngelHttpException.badRequest(
|
|
||||||
message: "'Origin' header is required.");
|
|
||||||
|
|
||||||
bool check() {
|
|
||||||
for (var input in filters) {
|
|
||||||
if (input is RegExp && input.hasMatch(origin))
|
|
||||||
return false;
|
|
||||||
else if (input is String && input == origin) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!check()) throw AngelHttpException.forbidden(message: message);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
final Uuid _uuid = Uuid();
|
|
||||||
|
|
||||||
/// Ensures that the request contains a correct CSRF token.
|
|
||||||
///
|
|
||||||
/// For `POST` methods, or anything requiring, provide [parseBody] as `true`.
|
|
||||||
///
|
|
||||||
/// Note that this is useless without [setCsrfToken]. If no [name] is present in the
|
|
||||||
/// session, clients will pass through.
|
|
||||||
RequestHandler verifyCsrfToken(
|
|
||||||
{bool allowCookie = false,
|
|
||||||
bool allowQuery = true,
|
|
||||||
bool parseBody = false,
|
|
||||||
String name = 'csrf_token'}) {
|
|
||||||
return (RequestContext req, res) async {
|
|
||||||
if (!req.session.containsKey(name)) {
|
|
||||||
throw AngelHttpException.forbidden(
|
|
||||||
message: 'You cannot access this resource without a CSRF token.');
|
|
||||||
}
|
|
||||||
|
|
||||||
String csrfToken;
|
|
||||||
if (parseBody) {
|
|
||||||
await req.parseBody();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowQuery && req.queryParameters.containsKey(name))
|
|
||||||
csrfToken = req.queryParameters[name];
|
|
||||||
else if (req.hasParsedBody && req.bodyAsMap.containsKey(name))
|
|
||||||
csrfToken = req.bodyAsMap[name];
|
|
||||||
else if (allowCookie) {
|
|
||||||
var cookie =
|
|
||||||
req.cookies.firstWhere((c) => c.name == name, orElse: () => null);
|
|
||||||
if (cookie != null) csrfToken = cookie.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (csrfToken == null)
|
|
||||||
throw AngelHttpException.badRequest(message: 'Missing CSRF token.');
|
|
||||||
|
|
||||||
String correctToken = req.session[name];
|
|
||||||
|
|
||||||
if (csrfToken != correctToken)
|
|
||||||
throw AngelHttpException.badRequest(message: 'Invalid CSRF token.');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a CSRF token to the session, if none is present.
|
|
||||||
RequestHandler setCsrfToken({String name = 'csrf_token', bool cookie = false}) {
|
|
||||||
return (RequestContext req, res) async {
|
|
||||||
if (!req.session.containsKey(name)) req.session[name] = _uuid.v4();
|
|
||||||
if (cookie) res.cookies.add(Cookie(name, req.session[name]));
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
|
|
||||||
/// Adds the authed user to `e.params`, only if present in `req.container`.
|
|
||||||
HookedServiceEventListener addUserToParams<User>({String as}) {
|
|
||||||
return (HookedServiceEvent e) async {
|
|
||||||
var user = await e.request?.container?.makeAsync<User>();
|
|
||||||
if (user != null) e.params[as ?? 'user'] = user;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:mirrors';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'errors.dart';
|
|
||||||
import 'is_server_side.dart';
|
|
||||||
|
|
||||||
/// Adds the authed user's id to `data`.
|
|
||||||
///
|
|
||||||
///Default [idField] is `'id'`.
|
|
||||||
/// Default [ownerField] is `'userId'`.
|
|
||||||
HookedServiceEventListener associateCurrentUser<Id, Data, User>(
|
|
||||||
{String idField,
|
|
||||||
String ownerField,
|
|
||||||
String errorMessage,
|
|
||||||
bool allowNullUserId = false,
|
|
||||||
FutureOr<Id> Function(User) getId,
|
|
||||||
FutureOr<Data> Function(Id, Data) assignUserId}) {
|
|
||||||
return (HookedServiceEvent e) async {
|
|
||||||
var fieldName = ownerField?.isNotEmpty == true ? ownerField : 'userId';
|
|
||||||
var user = await e.request?.container?.makeAsync<User>();
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
if (!isServerSide(e))
|
|
||||||
throw AngelHttpException.forbidden(
|
|
||||||
message: errorMessage ?? Errors.NOT_LOGGED_IN);
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Id> _getId(User user) async {
|
|
||||||
if (getId != null)
|
|
||||||
return await getId(user);
|
|
||||||
else if (user is Map)
|
|
||||||
return user[idField ?? 'id'];
|
|
||||||
else
|
|
||||||
return reflect(user).getField(Symbol(idField ?? 'id')).reflectee;
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = await _getId(user);
|
|
||||||
|
|
||||||
if (id == null && allowNullUserId != true)
|
|
||||||
throw AngelHttpException.notProcessable(
|
|
||||||
message: 'Current user is missing a $fieldName field.');
|
|
||||||
|
|
||||||
Future<Data> _assignUserId(Id id, Data obj) async {
|
|
||||||
if (assignUserId != null)
|
|
||||||
return assignUserId(id, obj);
|
|
||||||
else if (obj is Map)
|
|
||||||
return obj..[fieldName] = id;
|
|
||||||
else {
|
|
||||||
reflect(obj).setField(Symbol(fieldName), id);
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
e.data = await _assignUserId(id, e.data);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
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.';
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:mirrors';
|
|
||||||
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.
|
|
||||||
@deprecated
|
|
||||||
HookedServiceEventListener hashPassword<Password, User>(
|
|
||||||
{Hash hasher,
|
|
||||||
String passwordField,
|
|
||||||
FutureOr<Password> Function(User) getPassword,
|
|
||||||
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 if (passwordField == 'password')
|
|
||||||
return user?.password;
|
|
||||||
else
|
|
||||||
return reflect(user)
|
|
||||||
.getField(Symbol(passwordField ?? 'password'))
|
|
||||||
.reflectee;
|
|
||||||
}
|
|
||||||
|
|
||||||
_setPassword(password, user) {
|
|
||||||
if (setPassword != null)
|
|
||||||
return setPassword(password, user);
|
|
||||||
else if (user is Map)
|
|
||||||
user[passwordField ?? 'password'] = password;
|
|
||||||
else
|
|
||||||
reflect(user).setField(Symbol(passwordField ?? 'password'), password);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.data != null) {
|
|
||||||
applyHash(user) async {
|
|
||||||
var password = (await _getPassword(user))?.toString();
|
|
||||||
|
|
||||||
if (password != null) {
|
|
||||||
var digest = h.convert(password.codeUnits);
|
|
||||||
return _setPassword(String.fromCharCodes(digest.bytes), user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.data is Iterable) {
|
|
||||||
var futures = await Future.wait(e.data.map((data) async {
|
|
||||||
await applyHash(data);
|
|
||||||
return data;
|
|
||||||
}));
|
|
||||||
|
|
||||||
e.data = futures.toList();
|
|
||||||
} else
|
|
||||||
await applyHash(e.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
|
|
||||||
/// Returns `true` if the event was triggered server-side.
|
|
||||||
bool isServerSide(HookedServiceEvent e) => !e.params.containsKey('provider');
|
|
|
@ -1,50 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:mirrors';
|
|
||||||
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 `'user_id'`.
|
|
||||||
/// Default [userKey] is `'user'`.
|
|
||||||
HookedServiceEventListener queryWithCurrentUser<Id, User>(
|
|
||||||
{String as,
|
|
||||||
String idField,
|
|
||||||
String errorMessage,
|
|
||||||
bool allowNullUserId = false,
|
|
||||||
FutureOr<Id> Function(User) getId}) {
|
|
||||||
return (HookedServiceEvent e) async {
|
|
||||||
var fieldName = idField?.isNotEmpty == true ? idField : 'id';
|
|
||||||
var user = await e.request?.container?.makeAsync<User>();
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
if (!isServerSide(e))
|
|
||||||
throw AngelHttpException.forbidden(
|
|
||||||
message: errorMessage ?? Errors.NOT_LOGGED_IN);
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Id> _getId(User user) async {
|
|
||||||
if (getId != null)
|
|
||||||
return getId(user);
|
|
||||||
else if (user is Map)
|
|
||||||
return user[fieldName] as Id;
|
|
||||||
else
|
|
||||||
return reflect(user).getField(Symbol(fieldName)).reflectee as Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = await _getId(user);
|
|
||||||
|
|
||||||
if (id == null && allowNullUserId != true)
|
|
||||||
throw AngelHttpException.notProcessable(
|
|
||||||
message: 'Current user is missing a \'$fieldName\' field.');
|
|
||||||
|
|
||||||
var data = {as?.isNotEmpty == true ? as : 'user_id': id};
|
|
||||||
|
|
||||||
e.params['query'] = e.params.containsKey('query')
|
|
||||||
? (e.params['query']..addAll(data))
|
|
||||||
: data;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
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<User>(
|
|
||||||
{String errorMessage}) {
|
|
||||||
return (HookedServiceEvent e) async {
|
|
||||||
var user = await e.request?.container?.makeAsync<User>();
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
if (!isServerSide(e))
|
|
||||||
throw AngelHttpException.forbidden(
|
|
||||||
message: errorMessage ?? Errors.NOT_LOGGED_IN);
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:mirrors';
|
|
||||||
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<Id, Data, User>(
|
|
||||||
{String idField,
|
|
||||||
String ownerField,
|
|
||||||
String errorMessage,
|
|
||||||
FutureOr<Id> Function(User) getId,
|
|
||||||
FutureOr<Id> Function(Data) getOwnerId}) {
|
|
||||||
return (HookedServiceEvent e) async {
|
|
||||||
if (!isServerSide(e)) {
|
|
||||||
var user = await e.request?.container?.makeAsync<User>();
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
throw AngelHttpException.notAuthenticated(
|
|
||||||
message:
|
|
||||||
'The current user is missing. You must not be authenticated.');
|
|
||||||
|
|
||||||
Future<Id> _getId(User user) async {
|
|
||||||
if (getId != null)
|
|
||||||
return getId(user);
|
|
||||||
else if (user is Map)
|
|
||||||
return user[idField ?? 'id'];
|
|
||||||
else
|
|
||||||
return reflect(user).getField(Symbol(idField ?? 'id')).reflectee;
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = await _getId(user);
|
|
||||||
|
|
||||||
if (id == null) throw Exception('The current user has no ID.');
|
|
||||||
|
|
||||||
var resource = await e.service.read(
|
|
||||||
e.id,
|
|
||||||
{}
|
|
||||||
..addAll(e.params ?? {})
|
|
||||||
..remove('provider')) as Data;
|
|
||||||
|
|
||||||
if (resource != null) {
|
|
||||||
Future<Id> _getOwner(Data obj) async {
|
|
||||||
if (getOwnerId != null)
|
|
||||||
return await getOwnerId(obj);
|
|
||||||
else if (obj is Map)
|
|
||||||
return obj[ownerField ?? 'user_id'];
|
|
||||||
else
|
|
||||||
return reflect(obj)
|
|
||||||
.getField(Symbol(ownerField ?? 'userId'))
|
|
||||||
.reflectee;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownerId = await _getOwner(resource);
|
|
||||||
|
|
||||||
if ((ownerId is Iterable && !ownerId.contains(id)) || ownerId != id)
|
|
||||||
throw AngelHttpException.forbidden(
|
|
||||||
message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
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,
|
|
||||||
userKey,
|
|
||||||
bool owner = false,
|
|
||||||
getRoles(user),
|
|
||||||
getId(user),
|
|
||||||
getOwner(obj)}) {
|
|
||||||
return (HookedServiceEvent e) async {
|
|
||||||
var permission = await createPermission(e);
|
|
||||||
|
|
||||||
if (permission is PermissionBuilder) permission = permission.toPermission();
|
|
||||||
|
|
||||||
if (permission is! Permission)
|
|
||||||
throw 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,164 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
/// [idField], [ownerField], [userKey] and [errorMessage].
|
|
||||||
HookedServiceEventListener toHook<Id, Data, User>(
|
|
||||||
{String errorMessage,
|
|
||||||
String idField,
|
|
||||||
String ownerField,
|
|
||||||
bool owner = false,
|
|
||||||
FutureOr<Iterable<String>> Function(User) getRoles,
|
|
||||||
FutureOr<Id> Function(User) getId,
|
|
||||||
FutureOr<Id> Function(Data) getOwnerId}) {
|
|
||||||
return (HookedServiceEvent e) async {
|
|
||||||
if (e.params.containsKey('provider')) {
|
|
||||||
var user = await e.request?.container?.makeAsync<User>();
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
throw AngelHttpException.forbidden(
|
|
||||||
message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS);
|
|
||||||
|
|
||||||
var roleFinder = getRoles ?? (user) => <String>[];
|
|
||||||
var roles = (await roleFinder(user)).toList();
|
|
||||||
|
|
||||||
if (!roles.any(verify)) {
|
|
||||||
// Try owner if the roles are not in-place
|
|
||||||
if (owner == true) {
|
|
||||||
var listener = restrictToOwner<Id, Data, User>(
|
|
||||||
idField: idField,
|
|
||||||
ownerField: ownerField,
|
|
||||||
errorMessage: errorMessage,
|
|
||||||
getId: getId,
|
|
||||||
getOwnerId: getOwnerId);
|
|
||||||
await listener(e);
|
|
||||||
} else
|
|
||||||
throw AngelHttpException.forbidden(
|
|
||||||
message: errorMessage ?? Errors.INSUFFICIENT_PERMISSIONS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restricts a route to users who have sufficient permissions.
|
|
||||||
RequestHandler toMiddleware<User>({String message, getRoles(user)}) {
|
|
||||||
return (RequestContext req, ResponseContext res) async {
|
|
||||||
var user = await req.container.makeAsync<User>();
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
throw AngelHttpException.forbidden(
|
|
||||||
message: message ??
|
|
||||||
'You have insufficient permissions to perform this action.');
|
|
||||||
|
|
||||||
var roleFinder = getRoles ?? (user) async => user.roles ?? [];
|
|
||||||
List<String> roles = (await roleFinder(user)).toList();
|
|
||||||
|
|
||||||
if (!roles.any(verify))
|
|
||||||
throw AngelHttpException.forbidden(
|
|
||||||
message: message ??
|
|
||||||
'You have insufficient permissions to perform this action.');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
|
|
||||||
var minSplit = minimum.split(':');
|
|
||||||
var split = given.split(':');
|
|
||||||
|
|
||||||
for (int i = 0; i < minSplit.length; i++) {
|
|
||||||
if (i >= split.length) return false;
|
|
||||||
var min = minSplit[i], giv = split[i];
|
|
||||||
|
|
||||||
if (min == '*' || min == giv) {
|
|
||||||
if (i >= minSplit.length - 1)
|
|
||||||
return true;
|
|
||||||
else
|
|
||||||
continue;
|
|
||||||
} else
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var minima = minimum
|
|
||||||
.split('|')
|
|
||||||
.map((str) => str.trim())
|
|
||||||
.where((str) => str.isNotEmpty);
|
|
||||||
return minima.any(verifyOne);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => 'Permission: $minimum';
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds [Permission]s.
|
|
||||||
class PermissionBuilder {
|
|
||||||
String _min;
|
|
||||||
|
|
||||||
/// A minimum
|
|
||||||
PermissionBuilder(this._min);
|
|
||||||
|
|
||||||
factory PermissionBuilder.wildcard() => 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 ArgumentError(
|
|
||||||
'Cannot add a ${other.runtimeType} to a PermissionBuilder.');
|
|
||||||
}
|
|
||||||
|
|
||||||
PermissionBuilder operator |(other) {
|
|
||||||
if (other is String)
|
|
||||||
return or(PermissionBuilder(other));
|
|
||||||
else if (other is PermissionBuilder)
|
|
||||||
return or(other);
|
|
||||||
else if (other is Permission)
|
|
||||||
return or(PermissionBuilder(other.minimum));
|
|
||||||
else
|
|
||||||
throw ArgumentError(
|
|
||||||
'Cannot or a ${other.runtimeType} and a PermissionBuilder.');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds another level of [constraint].
|
|
||||||
PermissionBuilder add(String constraint) =>
|
|
||||||
PermissionBuilder('$_min:$constraint');
|
|
||||||
|
|
||||||
/// Adds a wildcard permission.
|
|
||||||
PermissionBuilder allowAll() => add('*');
|
|
||||||
|
|
||||||
/// Duplicates this builder.
|
|
||||||
PermissionBuilder clone() => PermissionBuilder(_min);
|
|
||||||
|
|
||||||
/// Allows an alternative permission.
|
|
||||||
PermissionBuilder or(PermissionBuilder other) =>
|
|
||||||
PermissionBuilder('$_min | ${other._min}');
|
|
||||||
|
|
||||||
/// Builds a [Permission].
|
|
||||||
Permission toPermission() => Permission(_min);
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
|
|
||||||
final Map<Pattern, String> defaultSanitizers = {
|
|
||||||
RegExp(r'<\s*s\s*c\s*r\s*i\s*p\s*t\s*>.*<\s*\/\s*s\s*c\s*r\s*i\s*p\s*t\s*>',
|
|
||||||
caseSensitive: false): ''
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Mitigates XSS risk by sanitizing user HTML input.
|
|
||||||
///
|
|
||||||
/// You can also provide a Map of patterns to [replace].
|
|
||||||
///
|
|
||||||
/// You can sanitize the [body] or [query] (both `true` by default).
|
|
||||||
RequestHandler sanitizeHtmlInput(
|
|
||||||
{bool body = true,
|
|
||||||
bool query = true,
|
|
||||||
Map<Pattern, String> replace = const {}}) {
|
|
||||||
var sanitizers = Map<Pattern, String>.from(defaultSanitizers)
|
|
||||||
..addAll(replace ?? {});
|
|
||||||
|
|
||||||
return (req, res) async {
|
|
||||||
if (body) {
|
|
||||||
await req.parseBody();
|
|
||||||
_sanitizeMap(req.bodyAsMap, sanitizers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query) _sanitizeMap(req.queryParameters, sanitizers);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_sanitize(v, Map<Pattern, String> sanitizers) {
|
|
||||||
if (v is String) {
|
|
||||||
var str = v;
|
|
||||||
|
|
||||||
sanitizers.forEach((needle, replace) {
|
|
||||||
str = str.replaceAll(needle, replace);
|
|
||||||
});
|
|
||||||
|
|
||||||
return htmlEscape.convert(str);
|
|
||||||
} else if (v is Map) {
|
|
||||||
_sanitizeMap(v, sanitizers);
|
|
||||||
return v;
|
|
||||||
} else if (v is Iterable) {
|
|
||||||
bool isList = v is List;
|
|
||||||
var mapped = v.map((x) => _sanitize(x, sanitizers));
|
|
||||||
return isList ? mapped.toList() : mapped;
|
|
||||||
} else
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _sanitizeMap(Map data, Map<Pattern, String> sanitizers) {
|
|
||||||
data.forEach((k, v) {
|
|
||||||
data[k] = _sanitize(v, sanitizers);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
|
|
||||||
/// Prevents users from sending more than a given [max] number of
|
|
||||||
/// requests within a given [duration].
|
|
||||||
///
|
|
||||||
/// Use [identify] to create a unique identifier for each request.
|
|
||||||
/// The default is to identify requests by their source IP.
|
|
||||||
///
|
|
||||||
/// This works well attached to a `multiserver` instance.
|
|
||||||
RequestHandler throttleRequests(int max, Duration duration,
|
|
||||||
{String message = '429 Too Many Requests', identify(RequestContext req)}) {
|
|
||||||
var identifyRequest = identify ?? (RequestContext req) async => req.ip;
|
|
||||||
Map<String, int> table = {};
|
|
||||||
Map<String, List<int>> times = {};
|
|
||||||
|
|
||||||
return (RequestContext req, ResponseContext res) async {
|
|
||||||
var id = (await identifyRequest(req)).toString();
|
|
||||||
int currentCount;
|
|
||||||
|
|
||||||
var now = DateTime.now().millisecondsSinceEpoch;
|
|
||||||
int firstVisit;
|
|
||||||
|
|
||||||
// If the user has visited within the given duration...
|
|
||||||
if (times.containsKey(id)) {
|
|
||||||
firstVisit = times[id].first;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If difference in times is greater than duration, reset counter ;)
|
|
||||||
if (firstVisit != null) {
|
|
||||||
if (now - firstVisit > duration.inMilliseconds) {
|
|
||||||
table.remove(id);
|
|
||||||
times.remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to time table
|
|
||||||
if (times.containsKey(id))
|
|
||||||
times[id].add(now);
|
|
||||||
else
|
|
||||||
times[id] = [now];
|
|
||||||
|
|
||||||
if (table.containsKey(id))
|
|
||||||
currentCount = table[id] = table[id] + 1;
|
|
||||||
else
|
|
||||||
currentCount = table[id] = 1;
|
|
||||||
|
|
||||||
if (currentCount > max) {
|
|
||||||
throw AngelHttpException(null,
|
|
||||||
statusCode: 429, message: message ?? '429 Too Many Requests');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
|
|
||||||
/// Injects a [ForwardedClient] if the user comes from a
|
|
||||||
/// trusted proxy.
|
|
||||||
///
|
|
||||||
/// [filter] can be:
|
|
||||||
/// A `String`, `RegExp`, `InternetAddress`, or an `Iterable`.
|
|
||||||
///
|
|
||||||
/// String can take the following formats:
|
|
||||||
/// 1. 1.2.3.4
|
|
||||||
/// 2. 1.2.3.*, 1.2.*.*, etc.
|
|
||||||
RequestHandler trustProxy(filter) {
|
|
||||||
var filters = [];
|
|
||||||
Iterable inputs = filter is Iterable ? filter : [filter];
|
|
||||||
|
|
||||||
for (var input in inputs) {
|
|
||||||
if (input is RegExp || input is InternetAddress)
|
|
||||||
filters.add(input);
|
|
||||||
else if (input is String) {
|
|
||||||
if (!input.contains('*'))
|
|
||||||
filters.add(input);
|
|
||||||
else {
|
|
||||||
filters.add(RegExp(input.replaceAll('*', '[0-9]+')));
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
throw ArgumentError('Cannot use $input as a trusted proxy filter.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (RequestContext req, ResponseContext res) async {
|
|
||||||
var ip = req.ip;
|
|
||||||
|
|
||||||
bool check() {
|
|
||||||
for (var input in filters) {
|
|
||||||
if (input is RegExp && input.hasMatch(ip))
|
|
||||||
return true;
|
|
||||||
else if (input is InternetAddress && input.address == ip)
|
|
||||||
return true;
|
|
||||||
else if (input is String && input == ip) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (check()) {
|
|
||||||
Map<String, List<String>> headers = {};
|
|
||||||
|
|
||||||
req.headers.forEach((k, v) {
|
|
||||||
if (k.trim().toLowerCase().startsWith('x-forwarded')) headers[k] = v;
|
|
||||||
});
|
|
||||||
|
|
||||||
req.container
|
|
||||||
.registerSingleton<ForwardedClient>(_ForwardedClientImpl(headers));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Presents information about the client forwarded by a trusted
|
|
||||||
/// reverse proxy.
|
|
||||||
abstract class ForwardedClient {
|
|
||||||
Map<String, List<String>> get headers;
|
|
||||||
|
|
||||||
String get ip => headers['x-forwarded-for']?.join(',');
|
|
||||||
String get host => headers['x-forwarded-host']?.join(',');
|
|
||||||
String get protocol => headers['x-forwarded-proto']?.join(',');
|
|
||||||
|
|
||||||
int get port {
|
|
||||||
var portString = headers['x-forwarded-proto']?.join(',');
|
|
||||||
return portString != null ? int.parse(portString) : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ForwardedClientImpl extends ForwardedClient {
|
|
||||||
final Map<String, List<String>> _headers;
|
|
||||||
|
|
||||||
_ForwardedClientImpl(this._headers);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, List<String>> get headers =>
|
|
||||||
Map<String, List<String>>.unmodifiable(_headers);
|
|
||||||
}
|
|
|
@ -6,7 +6,7 @@ homepage: https://github.com/angel-dart/security
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev <3.0.0"
|
sdk: ">=2.0.0-dev <3.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
angel_framework: ^2.0.0-alpha
|
angel_framework: ^2.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
angel_auth: ^2.0.0
|
angel_auth: ^2.0.0
|
||||||
angel_test: ^2.0.0
|
angel_test: ^2.0.0
|
||||||
|
@ -14,6 +14,3 @@ dev_dependencies:
|
||||||
console: ^3.0.0
|
console: ^3.0.0
|
||||||
pedantic: ^1.0.0
|
pedantic: ^1.0.0
|
||||||
test: ^1.0.0
|
test: ^1.0.0
|
||||||
dependency_overrides:
|
|
||||||
angel_framework:
|
|
||||||
path: ../framework
|
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_security/angel_security.dart';
|
|
||||||
import 'package:angel_test/angel_test.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
Angel app;
|
|
||||||
TestClient client;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
app = Angel()
|
|
||||||
..chain([banIp('*.*.*.*')]).get('/ban', (req, res) => 'WTF')
|
|
||||||
..chain([banOrigin('*')]).get('/ban-origin', (req, res) => 'WTF')
|
|
||||||
..chain([banOrigin('*.foo.bar')])
|
|
||||||
.get('/allow-origin', (req, res) => 'YAY');
|
|
||||||
|
|
||||||
client = await connectTo(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() => client.close());
|
|
||||||
|
|
||||||
test('ban everyone', () async {
|
|
||||||
var response = await client.get('/ban');
|
|
||||||
print(response.body);
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
expect(response.body.contains('WTF'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
group('origin', () {
|
|
||||||
test('ban everyone', () async {
|
|
||||||
var response = await client
|
|
||||||
.get('/ban-origin', headers: {'Origin': 'www.example.com'});
|
|
||||||
print(response.body);
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
expect(response.body.contains('WTF'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('ban specific', () async {
|
|
||||||
var response =
|
|
||||||
await client.get('/allow-origin', headers: {'Origin': 'www.foo.bar'});
|
|
||||||
print(response.body);
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
expect(response.body.contains('YAY'), isFalse);
|
|
||||||
|
|
||||||
response = await client
|
|
||||||
.get('/allow-origin', headers: {'Origin': 'www.example.com'});
|
|
||||||
print(response.body);
|
|
||||||
expect(response, hasStatus(200));
|
|
||||||
expect(response.body, contains('YAY'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_security/angel_security.dart';
|
|
||||||
import 'package:angel_test/angel_test.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
final RegExp _sessId = RegExp(r'DARTSESSID=([^;]+);');
|
|
||||||
|
|
||||||
main() async {
|
|
||||||
Angel app;
|
|
||||||
TestClient client;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
app = Angel();
|
|
||||||
|
|
||||||
app
|
|
||||||
..chain([verifyCsrfToken()]).get('/valid', (req, res) => 'Valid!')
|
|
||||||
..fallback(setCsrfToken());
|
|
||||||
|
|
||||||
client = await connectTo(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() => client.close());
|
|
||||||
|
|
||||||
test('need pre-existing token', () async {
|
|
||||||
var response = await client.get('/valid?csrf_token=evil');
|
|
||||||
print(response.body);
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fake token', () async {
|
|
||||||
// Get a valid CSRF, but ignore it.
|
|
||||||
var response = await client.get('/');
|
|
||||||
var sessionId = getCookie(response);
|
|
||||||
response = await client.get(
|
|
||||||
Uri(path: '/valid', queryParameters: {'csrf_token': 'evil'}),
|
|
||||||
headers: {'cookie': 'DARTSESSID=$sessionId'});
|
|
||||||
print(response.body);
|
|
||||||
expect(response, hasStatus(400));
|
|
||||||
expect(response.body.contains('Valid'), isFalse);
|
|
||||||
expect(response.body, contains('Invalid CSRF token'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
String getCookie(http.Response response) {
|
|
||||||
if (response.headers.containsKey('set-cookie')) {
|
|
||||||
var header = response.headers['set-cookie'];
|
|
||||||
var match = _sessId.firstMatch(header);
|
|
||||||
return match?.group(1);
|
|
||||||
} else
|
|
||||||
return null;
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
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:logging/logging.dart';
|
|
||||||
import 'package:pedantic/pedantic.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'pretty_logging.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
Angel app;
|
|
||||||
TestClient client;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
var logger = Logger.detached('hooks_test')..onRecord.listen(prettyLog);
|
|
||||||
app = Angel(logger: logger);
|
|
||||||
client = await connectTo(app);
|
|
||||||
|
|
||||||
HookedService<String, T, Service<String, T>> serve<T>(String path,
|
|
||||||
T Function(Map) encoder, Map<String, dynamic> Function(T) decoder) {
|
|
||||||
var inner = MapService();
|
|
||||||
var mapped = inner.map(encoder, decoder);
|
|
||||||
return app.use<String, T, Service<String, T>>(path, mapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
var userService =
|
|
||||||
serve<User>('/api/users', User.fromMap, (u) => u.toJson());
|
|
||||||
var houseService =
|
|
||||||
serve<House>('/api/houses', House.fromMap, (h) => h.toJson());
|
|
||||||
|
|
||||||
// Seed things up.
|
|
||||||
var pSherman = await userService.create(User('0', 'P Sherman'));
|
|
||||||
await houseService.create(House('0', pSherman.id, '42 Wallaby Way'));
|
|
||||||
await houseService.create(House('1', pSherman.id, 'Finding Nemo'));
|
|
||||||
await houseService
|
|
||||||
.create(House('1', '4', 'Should Not Appear for P. Sherman'));
|
|
||||||
|
|
||||||
// Inject a user depending on the authorization header.
|
|
||||||
app.container.registerFactory<Future<User>>((container) async {
|
|
||||||
var req = container.make<RequestContext>();
|
|
||||||
var authValue =
|
|
||||||
req.headers.value('authorization')?.replaceAll('Bearer', '')?.trim();
|
|
||||||
if (authValue == null)
|
|
||||||
throw AngelHttpException.badRequest(
|
|
||||||
message: 'Missing "authorization".');
|
|
||||||
var user = await userService.read(authValue).catchError((_) => null);
|
|
||||||
if (user == null)
|
|
||||||
throw AngelHttpException.notAuthenticated(
|
|
||||||
message: 'Invalid "authorization" ($authValue).');
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ACCESS CONTROL:
|
|
||||||
|
|
||||||
// A user can only see their own houses.
|
|
||||||
houseService.beforeIndexed.listen(hooks.queryWithCurrentUser<String, User>(
|
|
||||||
as: 'owner_id',
|
|
||||||
getId: (user) => user.id,
|
|
||||||
));
|
|
||||||
|
|
||||||
// A house is associated with the current user.
|
|
||||||
houseService.beforeCreated
|
|
||||||
.listen(hooks.associateCurrentUser<String, House, User>(
|
|
||||||
getId: (user) => user.id,
|
|
||||||
assignUserId: (id, house) => house.withOwner(id),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async {
|
|
||||||
app.logger.clearListeners();
|
|
||||||
await app.close();
|
|
||||||
unawaited(client.close());
|
|
||||||
});
|
|
||||||
|
|
||||||
test('query with current user', () async {
|
|
||||||
client.authToken = '0';
|
|
||||||
var houseService = client.service('/api/houses');
|
|
||||||
expect(await houseService.index(), [
|
|
||||||
{'id': '0', 'owner_id': '0', 'address': '42 Wallaby Way'},
|
|
||||||
{'id': '1', 'owner_id': '0', 'address': 'Finding Nemo'}
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('associate current user', () async {
|
|
||||||
client.authToken = '0';
|
|
||||||
var houseService = client.service('/api/houses');
|
|
||||||
expect(
|
|
||||||
await houseService.create({'address': 'Hello'}),
|
|
||||||
allOf(
|
|
||||||
containsPair('address', 'Hello'),
|
|
||||||
containsPair('owner_id', '0'),
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class User {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
User(this.id, this.name);
|
|
||||||
|
|
||||||
static User fromMap(Map map) =>
|
|
||||||
User(map['id'] as String, map['name'] as String);
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {'id': id, 'name': name};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class House {
|
|
||||||
final String id;
|
|
||||||
final String ownerId;
|
|
||||||
final String address;
|
|
||||||
|
|
||||||
House(this.id, this.ownerId, this.address);
|
|
||||||
|
|
||||||
static House fromMap(Map map) {
|
|
||||||
print('In $map');
|
|
||||||
return House(map['id'] as String, map['owner_id'] as String,
|
|
||||||
map['address'] as String);
|
|
||||||
}
|
|
||||||
|
|
||||||
House withOwner(String newOwnerId) {
|
|
||||||
return House(id, newOwnerId, address);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
print('Out ${{'id': id, 'owner_id': ownerId, 'address': address}}');
|
|
||||||
return {'id': id, 'owner_id': ownerId, 'address': address};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,286 +0,0 @@
|
||||||
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 = Angel()
|
|
||||||
..fallback((req, res) async {
|
|
||||||
var xUser = req.headers.value('X-User');
|
|
||||||
if (xUser != null) {
|
|
||||||
req.container.registerSingleton(
|
|
||||||
User(id: xUser, roles: xUser == 'John' ? ['foo:bar'] : []));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
app
|
|
||||||
..use('/user_data', UserDataService())
|
|
||||||
..use('/artists', ArtistService())
|
|
||||||
..use('/roled', RoledService());
|
|
||||||
|
|
||||||
(app.findService('user_data') as HookedService)
|
|
||||||
..beforeIndexed.listen(hooks.queryWithCurrentUser<String, User>())
|
|
||||||
..beforeCreated.listen(hooks.hashPassword());
|
|
||||||
|
|
||||||
app.findService('artists') as HookedService
|
|
||||||
..beforeIndexed.listen(hooks.restrictToAuthenticated<Artist>())
|
|
||||||
..beforeRead.listen(hooks.restrictToOwner<String, Artist, Artist>())
|
|
||||||
..beforeCreated
|
|
||||||
.listen(hooks.associateCurrentUser<String, Artist, Artist>());
|
|
||||||
|
|
||||||
(app.findService('roled') as HookedService)
|
|
||||||
..beforeIndexed.listen(Permission('foo:*').toHook())
|
|
||||||
..beforeRead.listen(Permission('foo:*').toHook(owner: true));
|
|
||||||
|
|
||||||
var errorHandler = app.errorHandler;
|
|
||||||
app.errorHandler = (e, req, res) {
|
|
||||||
print(e.toJson());
|
|
||||||
print(e.stackTrace);
|
|
||||||
return errorHandler(e, req, res);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 StateError('Creating without userId bad request');
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
expect(e, const TypeMatcher<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}');
|
|
||||||
print('Status: ${response.statusCode}');
|
|
||||||
expect(response, allOf(hasStatus(201), isJson({'foo': 'bar'})));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('queryWithCurrentUser', () {
|
|
||||||
test('fail', () async {
|
|
||||||
try {
|
|
||||||
var response = await client.service('user_data').index();
|
|
||||||
print(response);
|
|
||||||
throw StateError('Indexing without user forbidden');
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
expect(e, const TypeMatcher<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 StateError('Indexing without user forbidden');
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
expect(e, const TypeMatcher<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 StateError('Reading without owner forbidden');
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
expect(e, const TypeMatcher<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 StateError('Reading without roles forbidden');
|
|
||||||
} catch (e) {
|
|
||||||
print(e);
|
|
||||||
expect(e, const TypeMatcher<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 AngelHttpException.badRequest(message: 'query required');
|
|
||||||
|
|
||||||
String name = params['query']['userId']?.toString();
|
|
||||||
|
|
||||||
if (!_data.containsKey(name))
|
|
||||||
throw 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 AngelHttpException.badRequest(message: 'Required password!');
|
|
||||||
|
|
||||||
var expected =
|
|
||||||
String.fromCharCodes(sha256.convert('jdoe1'.codeUnits).bytes);
|
|
||||||
|
|
||||||
if (data['password'] != (expected))
|
|
||||||
throw AngelHttpException.conflict(message: 'Passwords do not match.');
|
|
||||||
return {'foo': 'bar'};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArtistService extends Service<String, Artist> {
|
|
||||||
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 {
|
|
||||||
return data;
|
|
||||||
// if (data is! Map || !data.containsKey('userId'))
|
|
||||||
// throw 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<String, Artist> {
|
|
||||||
@override
|
|
||||||
index([params]) async {
|
|
||||||
return [Artist(name: 'foo')];
|
|
||||||
// return ['foo'];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
read(id, [params]) async =>
|
|
||||||
ArtistService._ARTISTS.firstWhere((a) => a.id == id);
|
|
||||||
}
|
|
|
@ -1,151 +0,0 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_security/angel_security.dart';
|
|
||||||
import 'package:angel_test/angel_test.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
class User {
|
|
||||||
final List<String> roles;
|
|
||||||
|
|
||||||
User(this.roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
Angel app;
|
|
||||||
TestClient client;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
app = Angel();
|
|
||||||
|
|
||||||
app.fallback((req, res) async {
|
|
||||||
// In real life, you'd use auth to check user roles,
|
|
||||||
// but in this case, let's just set the user manually
|
|
||||||
var xRoles = req.headers['X-Roles'];
|
|
||||||
|
|
||||||
if (xRoles?.isNotEmpty == true) {
|
|
||||||
req.container.registerSingleton(User(xRoles));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
app.chain([PermissionBuilder.wildcard().toPermission().toMiddleware()]).get(
|
|
||||||
'/', (req, res) => 'Hello, world!');
|
|
||||||
app.chain([Permission('foo').toMiddleware()]).get(
|
|
||||||
'/one', (req, res) => 'Hello, world!');
|
|
||||||
app.chain([Permission('two:foo').toMiddleware()]).get(
|
|
||||||
'/two', (req, res) => 'Hello, world!');
|
|
||||||
app.chain([Permission('two:*').toMiddleware()]).get(
|
|
||||||
'/two-star', (req, res) => 'Hello, world!');
|
|
||||||
app.chain([Permission('three:foo:bar').toMiddleware()]).get(
|
|
||||||
'/three', (req, res) => 'Hello, world!');
|
|
||||||
app.chain([Permission('three:*:bar').toMiddleware()]).get(
|
|
||||||
'/three-star', (req, res) => 'Hello, world!');
|
|
||||||
|
|
||||||
app.chain([
|
|
||||||
PermissionBuilder('super')
|
|
||||||
.add('specific')
|
|
||||||
.add('permission')
|
|
||||||
.allowAll()
|
|
||||||
.or(PermissionBuilder('admin'))
|
|
||||||
.toPermission()
|
|
||||||
.toMiddleware()
|
|
||||||
]).get('/or', (req, res) => 'Hello, world!');
|
|
||||||
|
|
||||||
client = await connectTo(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() => client.close());
|
|
||||||
|
|
||||||
test('open permission', () async {
|
|
||||||
var response = await client.get('/', headers: {'X-Roles': 'foo'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(200));
|
|
||||||
expect(response, isJson('Hello, world!'));
|
|
||||||
});
|
|
||||||
|
|
||||||
group('restrict', () {
|
|
||||||
test('one', () async {
|
|
||||||
var response = await client.get('/one', headers: {'X-Roles': 'foo'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(200));
|
|
||||||
expect(response, isJson('Hello, world!'));
|
|
||||||
|
|
||||||
response = await client.get('/one', headers: {'X-Roles': 'bar'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('two', () async {
|
|
||||||
var response = await client.get('/two', headers: {'X-Roles': 'two:foo'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(200));
|
|
||||||
expect(response, isJson('Hello, world!'));
|
|
||||||
|
|
||||||
response = await client.get('/two', headers: {'X-Roles': 'two:bar'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('two with star', () async {
|
|
||||||
var response =
|
|
||||||
await client.get('/two-star', headers: {'X-Roles': 'two:foo'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(200));
|
|
||||||
expect(response, isJson('Hello, world!'));
|
|
||||||
|
|
||||||
response =
|
|
||||||
await client.get('/two-star', headers: {'X-Roles': 'three:foo'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('three', () async {
|
|
||||||
var response =
|
|
||||||
await client.get('/three', headers: {'X-Roles': 'three:foo:bar'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(200));
|
|
||||||
expect(response, isJson('Hello, world!'));
|
|
||||||
|
|
||||||
response =
|
|
||||||
await client.get('/three', headers: {'X-Roles': 'three:foo:baz'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
|
|
||||||
response =
|
|
||||||
await client.get('/three', headers: {'X-Roles': 'three:foz:bar'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('three with star', () async {
|
|
||||||
var response = await client
|
|
||||||
.get('/three-star', headers: {'X-Roles': 'three:foo:bar'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(200));
|
|
||||||
expect(response, isJson('Hello, world!'));
|
|
||||||
|
|
||||||
response = await client
|
|
||||||
.get('/three-star', headers: {'X-Roles': 'three:foz:bar'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(200));
|
|
||||||
expect(response, isJson('Hello, world!'));
|
|
||||||
|
|
||||||
response = await client
|
|
||||||
.get('/three-star', headers: {'X-Roles': 'three:foo:baz'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('or', () async {
|
|
||||||
var response = await client.get('/or', headers: {'X-Roles': 'admin'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(200));
|
|
||||||
expect(response, isJson('Hello, world!'));
|
|
||||||
|
|
||||||
response = await client
|
|
||||||
.get('/or', headers: {'X-Roles': 'not:specific:enough:i:guess'});
|
|
||||||
print('Response: ${response.body}');
|
|
||||||
expect(response, hasStatus(403));
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import 'package:console/console.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
/// Prints the contents of a [LogRecord] with pretty colors.
|
|
||||||
prettyLog(LogRecord record) async {
|
|
||||||
var pen = TextPen();
|
|
||||||
chooseLogColor(pen.reset(), record.level);
|
|
||||||
pen(record.toString());
|
|
||||||
|
|
||||||
if (record.error != null) pen(record.error.toString());
|
|
||||||
if (record.stackTrace != null) pen(record.stackTrace.toString());
|
|
||||||
|
|
||||||
pen();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Chooses a color based on the logger [level].
|
|
||||||
void chooseLogColor(TextPen pen, Level level) {
|
|
||||||
if (level == Level.SHOUT)
|
|
||||||
pen.darkRed();
|
|
||||||
else if (level == Level.SEVERE)
|
|
||||||
pen.red();
|
|
||||||
else if (level == Level.WARNING)
|
|
||||||
pen.yellow();
|
|
||||||
else if (level == Level.INFO)
|
|
||||||
pen.magenta();
|
|
||||||
else if (level == Level.FINER)
|
|
||||||
pen.blue();
|
|
||||||
else if (level == Level.FINEST) pen.darkBlue();
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_security/angel_security.dart';
|
|
||||||
import 'package:angel_test/angel_test.dart';
|
|
||||||
import 'package:angel_validate/server.dart';
|
|
||||||
import 'package:http_parser/http_parser.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:matcher/matcher.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'pretty_logging.dart';
|
|
||||||
|
|
||||||
final Validator untrustedSchema = Validator({'html*': isString});
|
|
||||||
|
|
||||||
main() async {
|
|
||||||
Angel app;
|
|
||||||
TestClient client;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
var logger = Logger.detached('angel_security')..onRecord.listen(prettyLog);
|
|
||||||
app = Angel(logger: logger);
|
|
||||||
app.chain([validate(untrustedSchema), sanitizeHtmlInput()])
|
|
||||||
..post('/untrusted', (RequestContext req, ResponseContext res) async {
|
|
||||||
String untrusted = req.bodyAsMap['html'];
|
|
||||||
res
|
|
||||||
..contentType = MediaType('text', 'html')
|
|
||||||
..write('''
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Potential Security Hole</title>
|
|
||||||
</head>
|
|
||||||
<body>$untrusted</body>
|
|
||||||
</html>''');
|
|
||||||
})
|
|
||||||
..post('/attribute', (RequestContext req, ResponseContext res) async {
|
|
||||||
String untrusted = req.bodyAsMap['html'];
|
|
||||||
res
|
|
||||||
..contentType = MediaType('text', 'html')
|
|
||||||
..write('''
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Potential Security Hole</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<img src="$untrusted" />
|
|
||||||
</body>
|
|
||||||
</html>''');
|
|
||||||
});
|
|
||||||
|
|
||||||
var oldHandler = app.errorHandler;
|
|
||||||
app.errorHandler = (e, req, res) {
|
|
||||||
app.logger.severe(e, e.error, e.stackTrace);
|
|
||||||
return oldHandler(e, req, res);
|
|
||||||
};
|
|
||||||
|
|
||||||
client = await connectTo(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() => client.close());
|
|
||||||
|
|
||||||
group('script tag', () {
|
|
||||||
test('normal', () async {
|
|
||||||
var xss = "<script>alert('XSS')</script>";
|
|
||||||
var response = await client.post('/untrusted', body: {'html': xss});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body.contains(xss), isFalse);
|
|
||||||
expect(response.body.toLowerCase().contains('<script>'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('mixed case', () async {
|
|
||||||
var xss = "<scRIpT>alert('XSS')</sCRIpt>";
|
|
||||||
var response = await client.post('/untrusted', body: {'html': xss});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body.contains(xss), isFalse);
|
|
||||||
expect(response.body.toLowerCase().contains('<script>'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('spaces', () async {
|
|
||||||
var xss = "< s c rip t>alert('XSS')</scr ip t>";
|
|
||||||
var response = await client.post('/untrusted', body: {'html': xss});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body.contains(xss), isFalse);
|
|
||||||
expect(response.body.toLowerCase().contains('<script>'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('lines', () async {
|
|
||||||
var xss = "<scri\npt>\n\nalert('XSS')\t\n</sc\nri\npt>";
|
|
||||||
var response = await client.post('/untrusted', body: {'html': xss});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body.contains(xss), isFalse);
|
|
||||||
expect(response.body.toLowerCase().contains('<script>'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('accents', () async {
|
|
||||||
var xss = '''<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>''';
|
|
||||||
var response = await client.post('/untrusted', body: {'html': xss});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body.contains(xss), isFalse);
|
|
||||||
expect(response.body.toLowerCase().contains('<script>'), isFalse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('quotes', () async {
|
|
||||||
var xss = '" onclick="<script>alert(\'XSS!\')</script>"';
|
|
||||||
var response = await client.post('/attribute', body: {'html': xss});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body.contains(xss), isFalse);
|
|
||||||
expect(response.body.toLowerCase().contains('<script>'), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('javascript:evil', () async {
|
|
||||||
var xss = 'javascript:alert(\'XSS!\')';
|
|
||||||
var response = await client.post('/attribute', body: {'html': xss});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body.contains(xss), isFalse);
|
|
||||||
expect(response.body.toLowerCase().contains(xss), isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('style attribute', () async {
|
|
||||||
var xss = "background-image: url(jaVAscRiPt:alert('XSS'))";
|
|
||||||
var response = await client.post('/attribute', body: {'html': xss});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body.contains(xss), isFalse);
|
|
||||||
expect(response.body.toLowerCase().contains(xss), isFalse);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,63 +0,0 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_security/angel_security.dart';
|
|
||||||
import 'package:angel_test/angel_test.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
Angel app;
|
|
||||||
TestClient client;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
app = Angel();
|
|
||||||
|
|
||||||
app.chain([throttleRequests(1, Duration(hours: 1))]).get(
|
|
||||||
'/once-per-hour', (req, res) => 'OK');
|
|
||||||
|
|
||||||
app.chain([throttleRequests(3, Duration(minutes: 1))]).get(
|
|
||||||
'/thrice-per-minute', (req, res) => 'OK');
|
|
||||||
|
|
||||||
client = await connectTo(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() => client.close());
|
|
||||||
|
|
||||||
test('once per hour', () async {
|
|
||||||
// First request within the hour is fine
|
|
||||||
var response = await client.get('/once-per-hour', headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
});
|
|
||||||
print(response.body);
|
|
||||||
expect(response, hasBody('"OK"'));
|
|
||||||
|
|
||||||
// Second request within an hour? No no no!
|
|
||||||
response = await client.get('/once-per-hour', headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
});
|
|
||||||
print(response.body);
|
|
||||||
expect(response, isAngelHttpException(statusCode: 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, hasBody('"OK"'));
|
|
||||||
|
|
||||||
// Second request within the minute is fine
|
|
||||||
response = await client.get('/thrice-per-minute');
|
|
||||||
print(response.body);
|
|
||||||
expect(response, hasBody('"OK"'));
|
|
||||||
|
|
||||||
// Third request within the minute is fine
|
|
||||||
response = await client.get('/thrice-per-minute');
|
|
||||||
print(response.body);
|
|
||||||
expect(response, hasBody('"OK"'));
|
|
||||||
|
|
||||||
// Fourth request within a minute? No no no!
|
|
||||||
response = await client.get('/thrice-per-minute', headers: {
|
|
||||||
'accept': 'application/json',
|
|
||||||
});
|
|
||||||
print(response.body);
|
|
||||||
expect(response, isAngelHttpException(statusCode: 429));
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import 'package:angel_framework/angel_framework.dart';
|
|
||||||
import 'package:angel_security/angel_security.dart';
|
|
||||||
import 'package:angel_test/angel_test.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
verifyProxy(RequestContext req, ResponseContext res) =>
|
|
||||||
req.container.has<ForwardedClient>() ? 'Yep' : 'Nope';
|
|
||||||
|
|
||||||
main() {
|
|
||||||
Angel app;
|
|
||||||
TestClient client;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
app = Angel()
|
|
||||||
..chain([trustProxy('127.*.*.*')]).get('/hello', verifyProxy)
|
|
||||||
..chain([trustProxy('1.2.3.4')]).get('/foo', verifyProxy);
|
|
||||||
client = await connectTo(app);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() => client.close());
|
|
||||||
|
|
||||||
test('wildcard', () async {
|
|
||||||
var response =
|
|
||||||
await client.get('/hello', headers: {'X-Forwarded-Host': 'foo'});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body, contains('Yep'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exclude unknown', () async {
|
|
||||||
var response =
|
|
||||||
await client.get('/foo', headers: {'X-Forwarded-Host': 'foo'});
|
|
||||||
print(response.body);
|
|
||||||
expect(response.body, contains('Nope'));
|
|
||||||
});
|
|
||||||
}
|
|
Loading…
Reference in a new issue