From 377410171348b1c242054a5223915703cf7a0adb Mon Sep 17 00:00:00 2001 From: thosakwe Date: Fri, 20 Jan 2017 22:09:37 -0500 Subject: [PATCH] Permissions --- README.md | 8 ++- lib/angel_security.dart | 1 + lib/src/permissions.dart | 109 ++++++++++++++++++++++++++++ pubspec.yaml | 3 +- test/permission_test.dart | 145 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 263 insertions(+), 3 deletions(-) create mode 100644 lib/src/permissions.dart create mode 100644 test/permission_test.dart diff --git a/README.md b/README.md index e9a0f1d2..cfc78ee9 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # security -[![version 0.0.0-alpha+2](https://img.shields.io/badge/pub-v0.0.0--alpha+2-red.svg)](https://pub.dartlang.org/packages/angel_security) +[![version 0.0.0-alpha+3](https://img.shields.io/badge/pub-v0.0.0--alpha+3-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 @@ -15,6 +15,7 @@ Currently unfinished, with incomplete code coverage - **USE AT YOUR OWN RISK!!!* * [Throttling Requests](#throttling-requests) * [Helmet Port](#helmet) * [Service Hooks](#service-hooks) +* [Permissions](#permissions) ## Sanitizing HTML @@ -83,4 +84,7 @@ Also included are a set of service hooks, [ported from FeathersJS](https://githu ```dart import 'package:angel_security/hooks.dart'; -``` \ No newline at end of file +``` + +# Permissions +See the tests. \ No newline at end of file diff --git a/lib/angel_security.dart b/lib/angel_security.dart index 4a4b4554..771373cd 100644 --- a/lib/angel_security.dart +++ b/lib/angel_security.dart @@ -3,6 +3,7 @@ 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'; diff --git a/lib/src/permissions.dart b/lib/src/permissions.dart new file mode 100644 index 00000000..248d869c --- /dev/null +++ b/lib/src/permissions.dart @@ -0,0 +1,109 @@ +import 'package:angel_framework/angel_framework.dart'; + +/// Easy mechanism to restrict access to services or routes. +class Permission { + final String minimum; + + Permission(this.minimum); + + call(RequestContext req, ResponseContext res) { + return toMiddleware()(req, res); + } + + HookedServiceEventListener toHook( + {String message, String userKey, getRoles(user)}) { + return (HookedServiceEvent e) async { + var user = e.request.grab(userKey ?? 'user'); + + if (user == null) + throw new AngelHttpException.forbidden( + message: message ?? + 'You have insufficient permissions to perform this action.'); + + var roleFinder = getRoles ?? (user) async => user.roles ?? []; + List roles = (await roleFinder(user)).toList(); + + if (!roles.any(verify)) + throw new AngelHttpException.forbidden( + message: message ?? + 'You have insufficient permissions to perform this action.'); + }; + } + + RequestMiddleware toMiddleware( + {String message, String userKey, getRoles(user)}) { + return (RequestContext req, ResponseContext res) async { + var user = req.grab(userKey ?? 'user'); + + if (user == null) + throw new AngelHttpException.forbidden( + message: message ?? + 'You have insufficient permissions to perform this action.'); + + var roleFinder = getRoles ?? (user) async => user.roles ?? []; + List roles = (await roleFinder(user)).toList(); + + if (!roles.any(verify)) + throw new AngelHttpException.forbidden( + message: message ?? + 'You have insufficient permissions to perform this action.'); + + return true; + }; + } + + 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'; +} + +class PermissionBuilder { + String _min; + + PermissionBuilder(this._min); + + factory PermissionBuilder.wildcard() => new PermissionBuilder('*'); + + call(RequestContext req, ResponseContext res) => toPermission()(req, res); + + PermissionBuilder add(String constraint) => + new PermissionBuilder('$_min:$constraint'); + + PermissionBuilder allowAll() => add('*'); + + PermissionBuilder clone() => new PermissionBuilder(_min); + + PermissionBuilder or(PermissionBuilder other) => + new PermissionBuilder('$_min | ${other._min}'); + + Permission toPermission() => new Permission(_min); +} diff --git a/pubspec.yaml b/pubspec.yaml index fecf0efd..4afc0874 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: angel_security -version: 0.0.0-alpha+2 +version: 0.0.0-alpha+3 description: Angel middleware designed to enhance application security by patching common Web security holes. author: Tobe O environment: @@ -8,6 +8,7 @@ homepage: https://github.com/angel-dart/security dependencies: angel_framework: ^1.0.0-dev dev_dependencies: + angel_auth: ^1.0.0-dev angel_diagnostics: ^1.0.0-dev angel_validate: ^1.0.0-beta angel_test: ^1.0.0-dev diff --git a/test/permission_test.dart b/test/permission_test.dart new file mode 100644 index 00000000..86359e2b --- /dev/null +++ b/test/permission_test.dart @@ -0,0 +1,145 @@ +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 roles; + + User(this.roles); +} + +main() { + Angel app; + TestClient client; + + setUp(() async { + app = new Angel(); + + app.before.add((RequestContext 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.value('X-Roles'); + + if (xRoles?.isNotEmpty == true) { + req.inject('user', new User(req.headers['X-Roles'])); + } + + return true; + }); + + app.chain(new PermissionBuilder.wildcard()).get('/', 'Hello, world!'); + app.chain(new Permission('foo')).get('/one', 'Hello, world!'); + app.chain(new Permission('two:foo')).get('/two', 'Hello, world!'); + app.chain(new Permission('two:*')).get('/two-star', 'Hello, world!'); + app.chain(new Permission('three:foo:bar')).get('/three', 'Hello, world!'); + app + .chain(new Permission('three:*:bar')) + .get('/three-star', 'Hello, world!'); + + app + .chain(new PermissionBuilder('super') + .add('specific') + .add('permission') + .allowAll() + .or(new PermissionBuilder('admin'))) + .get('/or', '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)); + }); +}