Skip to content
This repository was archived by the owner on Sep 2, 2020. It is now read-only.

Commit 36c6f63

Browse files
committed
Add information on defining custom guards
1 parent d5cba31 commit 36c6f63

4 files changed

Lines changed: 258 additions & 62 deletions

File tree

README.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ component secured {
107107

108108
The methods available to you on the `Guard` component are as follows:
109109

110-
```
110+
```cfc
111111
public boolean function allows( required any permissions, struct additionalArgs = {} );
112112
public boolean function denies( required any permissions, struct additionalArgs = {} );
113113
public boolean function all( required any permissions, struct additionalArgs = {} );
@@ -120,8 +120,43 @@ In all cases `permissions` can be either a string, a list of strings, or an arra
120120
In the case of `authorize` the `errorMessage` replaces the thrown error message
121121
in the `NotAuthorized`. exception. It can also be a closure that takes the following shape:
122122

123+
```cfc
124+
string function errorMessage( array permissions, any user, struct additionalArgs );
123125
```
124-
string function errorMessage( string failedPermission, any user, struct additionalArgs );
126+
127+
#### Defining Custom Guards
128+
129+
While handling all of your guard clauses inside the `hasPermission` method on your user
130+
works fine, you may want to define a different way to handle permissions. You
131+
can do this by declaring custom guards using the `guard.define` method. Here's the signature:
132+
133+
```cfc
134+
public Guard function define( required string name, required any callback );
135+
```
136+
137+
The `name` will match against a permission name. If it matches, the guard is
138+
called instead of calling `hasPermission` on the `User` model. (You can always
139+
call `hasPermission` on the `User` inside your guard callback if you need.)
140+
141+
The callback can be: a closure or UDF, a component with an `authorize` function,
142+
or a WireBox mapping to a component with an `authorize` function. Please note
143+
that the authorize function must be explicitly defined and public (No `onMissingMethod`).
144+
This `authorize` function is called with two parameters: the `user` being authorized
145+
and a struct of `additionalArgs` and must return a `boolean`, like so:
146+
147+
```cfc
148+
public boolean function authorize( required any user, struct additionalArgs = {} );
149+
```
150+
151+
Using this approach, you can define custom guards anywhere in your application:
152+
`config/ColdBox.cfc`, `ModuleConfig.cfc` of your custom modules, etc. The
153+
`Guard` component is registered as a singleton, so it will keep track of all the
154+
guards registered, even from different sources.
155+
156+
If you have a need to remove a guard definition you can do so with the `removeDefinition` method:
157+
158+
```cfc
159+
public Guard function removeDefinition( required string name );
125160
```
126161

127162
### Redirects

models/Guard.cfc

Lines changed: 186 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,195 @@
1-
component singleton {
1+
component singleton accessors="true" {
22

33
property name="settings" inject="coldbox:moduleSettings:cbguard";
44
property name="moduleService" inject="coldbox:moduleService";
55
property name="wirebox" inject="wirebox";
66

7-
function allows( required any permissions, struct additionalArgs = {}, boolean negate = false ) {
8-
var context = preflight();
7+
property name="guards" type="struct";
98

10-
for ( var permission in arrayWrap( arguments.permissions ) ) {
11-
var hasPermission = invoke(
12-
context.user,
13-
context.props.methodNames[ "hasPermission" ],
14-
{
15-
"permission": permission,
16-
"additionalArgs": arguments.additionalArgs
17-
}
9+
/**
10+
* Creates a new Guard service
11+
*
12+
* @returns cbguard.models.Guard
13+
*/
14+
public Guard function init() {
15+
variables.guards = {};
16+
return this;
17+
}
18+
19+
/**
20+
* Defines a new custom guard. A custom guard is used instead of the
21+
* user model's `hasPermission` method.
22+
*
23+
* @name The name of the permission to match for this custom guard.
24+
* @callback The callback to resolve the custom guard. It should return a boolean.
25+
* This can be one of the following:
26+
* 1. A UDF or closure.
27+
* 2. A component with an `authorize` method.
28+
* 3. A WireBox mapping that resolves to a component with an `authorize` method.
29+
*
30+
* @throws InvalidGuardType
31+
*
32+
* @returns cbguard.models.Guard
33+
*/
34+
public Guard function define( required string name, required any callback ) {
35+
if ( isSimpleValue( arguments.callback ) ) {
36+
arguments.callback = variables.wirebox.getInstance( dsl = callback );
37+
if ( !structKeyExists( arguments.callback, "authorize" ) ) {
38+
throw(
39+
type = "InvalidGuardType",
40+
message = "A component guard must have an `authorize` method defined."
41+
);
42+
}
43+
} else if ( isClosure( arguments.callback ) || isCustomFunction( arguments.callback ) ) {
44+
arguments.callback = { "authorize": arguments.callback };
45+
} else {
46+
throw(
47+
type = "InvalidGuardType",
48+
message = "Cannot define a guard without either a component with an `authorize` method, a WireBox mapping to a component with an `authorize` method, or a closure or UDF function."
1849
);
50+
}
51+
52+
variables.guards[ arguments.name ] = arguments.callback;
53+
return this;
54+
}
1955

20-
if ( hasPermission ) {
56+
/**
57+
* Removes a custom guard definition.
58+
*
59+
* @name The name of the permission to remove the custom guard.
60+
* @returns cbguard.models.Guard
61+
*/
62+
public Guard function removeDefinition( required string name ) {
63+
structDelete( variables.guards, arguments.name );
64+
return this;
65+
}
66+
67+
68+
/**
69+
* Returns true if the logged in user is allowed for any of the permissions.
70+
*
71+
* @permissions A single string permission, list of string permissions,
72+
* or array of string permissions to check.
73+
* @additionalArgs A struct of any additional arguments to pass to the guard.
74+
*
75+
* @returns boolean
76+
*/
77+
public boolean function allows( required any permissions, struct additionalArgs = {} ) {
78+
var context = preflight();
79+
80+
for ( var permission in arrayWrap( arguments.permissions ) ) {
81+
if ( resolvePermission( permission, context, arguments.additionalArgs ) ) {
2182
return true;
2283
}
2384
}
2485

2586
return false;
2687
}
2788

28-
function denies( required any permissions, struct additionalArgs = {} ) {
89+
/**
90+
* Returns true if the logged in user is not allowed for at least one of the permissions.
91+
*
92+
* @permissions A single string permission, list of string permissions,
93+
* or array of string permissions to check.
94+
* @additionalArgs A struct of any additional arguments to pass to the guard.
95+
*
96+
* @returns boolean
97+
*/
98+
public boolean function denies( required any permissions, struct additionalArgs = {} ) {
2999
var context = preflight();
30100

31101
for ( var permission in arrayWrap( arguments.permissions ) ) {
32-
var hasPermission = invoke(
33-
context.user,
34-
context.props.methodNames[ "hasPermission" ],
35-
{
36-
"permission": permission,
37-
"additionalArgs": arguments.additionalArgs
38-
}
39-
);
40-
41-
if ( !hasPermission ) {
102+
if ( !resolvePermission( permission, context, arguments.additionalArgs ) ) {
42103
return true;
43104
}
44105
}
45106

46107
return false;
47108
}
48109

110+
/**
111+
* Returns true if the logged in user is allowed for all of the permissions.
112+
*
113+
* @permissions A single string permission, list of string permissions,
114+
* or array of string permissions to check.
115+
* @additionalArgs A struct of any additional arguments to pass to the guard.
116+
*
117+
* @returns boolean
118+
*/
49119
public boolean function all( required any permissions, struct additionalArgs = {} ) {
50120
var context = preflight();
51121

52122
for ( var permission in arrayWrap( arguments.permissions ) ) {
53-
var hasPermission = invoke(
54-
context.user,
55-
context.props.methodNames[ "hasPermission" ],
56-
{
57-
"permission": permission,
58-
"additionalArgs": arguments.additionalArgs
59-
}
60-
);
61-
62-
if ( !hasPermission ) {
123+
if ( !resolvePermission( permission, context, arguments.additionalArgs ) ) {
63124
return false;
64125
}
65126
}
66127

67128
return true;
68129
}
69130

131+
/**
132+
* Returns true if the logged in user is denied for all of the permissions.
133+
*
134+
* @permissions A single string permission, list of string permissions,
135+
* or array of string permissions to check.
136+
* @additionalArgs A struct of any additional arguments to pass to the guard.
137+
*
138+
* @returns boolean
139+
*/
70140
public boolean function none( required any permissions, struct additionalArgs = {} ) {
71141
var context = preflight();
72142

73143
for ( var permission in arrayWrap( arguments.permissions ) ) {
74-
var hasPermission = invoke(
75-
context.user,
76-
context.props.methodNames[ "hasPermission" ],
77-
{
78-
"permission": permission,
79-
"additionalArgs": arguments.additionalArgs
80-
}
81-
);
82-
83-
if ( hasPermission ) {
144+
if ( resolvePermission( permission, context, arguments.additionalArgs ) ) {
84145
return false;
85146
}
86147
}
87148

88149
return true;
89150
}
90151

91-
public void function authorize(
152+
/**
153+
* Throws an exception if the logged in user is not allowed for any of the permissions.
154+
*
155+
* @permissions A single string permission, list of string permissions,
156+
* or array of string permissions to check.
157+
* @additionalArgs A struct of any additional arguments to pass to the guard.
158+
* @errorMessage The error message to throw with the exception.
159+
* It can be either:
160+
* 1. A string error message
161+
* 2. A closure or UDF that will produce a string error
162+
* message. This callback receives the following arguments:
163+
* a. The `permissions` tried.
164+
* b. The logged in `user`.
165+
* c. The `additionalArgs` passed.
166+
*
167+
* @throws NotAuthorized
168+
*
169+
* @returns cbguard.models.Guard
170+
*/
171+
public Guard function authorize(
92172
required any permissions,
93173
struct additionalArgs = {},
94-
string errorMessage
174+
any errorMessage
95175
) {
96176
var context = preflight();
97177

98-
var failedPermission = "";
99-
for ( var permission in arrayWrap( arguments.permissions ) ) {
100-
var hasPermission = invoke(
101-
context.user,
102-
context.props.methodNames[ "hasPermission" ],
103-
{
104-
"permission": permission,
105-
"additionalArgs": arguments.additionalArgs
106-
}
107-
);
108-
109-
if ( !hasPermission ) {
110-
failedPermission = permission;
178+
arguments.permissions = arrayWrap( arguments.permissions );
179+
var passed = false;
180+
for ( var permission in arguments.permissions ) {
181+
if ( resolvePermission( permission, context, arguments.additionalArgs ) ) {
182+
passed = true;
111183
break;
112184
}
113185
}
114186

115-
if ( failedPermission != "" ) {
187+
if ( !passed ) {
116188
param arguments.errorMessage = "The logged in user is not authorized to access this resource";
117189

118190
if ( isClosure( arguments.errorMessage ) || isCustomFunction( arguments.errorMessage ) ) {
119191
arguments.errorMessage = arguments.errorMessage(
120-
failedPermission = failedPermission,
192+
permissions = arguments.permissions,
121193
user = context.user,
122194
additionalArgs = arguments.additionalArgs
123195
);
@@ -128,8 +200,19 @@ component singleton {
128200
message = arguments.errorMessage
129201
);
130202
}
203+
204+
return this;
131205
}
132206

207+
/**
208+
* Handles getting the current cbguard context for this guard.
209+
* Returns a struct with the current `RequestContext` (`event`), the current
210+
* settings for the request (`props`), and the currently logged in user (`user`).
211+
*
212+
* @throws NotLoggedIn
213+
*
214+
* @returns { "event", "props", "user" }
215+
*/
133216
private struct function preflight() {
134217
var event = variables.wirebox.getInstance( dsl = "coldbox:requestContext" );
135218

@@ -161,6 +244,49 @@ component singleton {
161244
};
162245
}
163246

247+
/**
248+
* Resolves the permission check. It first checks and tries any custom guards.
249+
* If no custom guards exist, it checks the current user's `hasPermission` method.
250+
*
251+
* @permission The permission being checked.
252+
* @context The guard context, including `event`, `props`, and `user`.
253+
* @additionalArgs A struct of any additional arguments to pass to the guard.
254+
*
255+
* @return boolean
256+
*/
257+
private boolean function resolvePermission(
258+
required string permission,
259+
required struct context,
260+
struct additionalArgs = {}
261+
) {
262+
if ( variables.guards.keyExists( arguments.permission ) ) {
263+
return invoke(
264+
variables.guards[ arguments.permission ],
265+
"authorize",
266+
{
267+
"user": arguments.context.user,
268+
"additionalArgs": arguments.additionalArgs
269+
}
270+
);
271+
}
272+
273+
return invoke(
274+
arguments.context.user,
275+
arguments.context.props.methodNames[ "hasPermission" ],
276+
{
277+
"permission": arguments.permission,
278+
"additionalArgs": arguments.additionalArgs
279+
}
280+
);
281+
}
282+
283+
/**
284+
* Ensures that the returned value is an array.
285+
* Returns a passed array unmodified. Calls `listToArray` on all other values.
286+
*
287+
* @doc_generic any
288+
* @return [any]
289+
*/
164290
private array function arrayWrap( required any items ) {
165291
return isArray( arguments.items ) ? items : items.listToArray();
166292
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
component {
2+
3+
function authorize( required user, struct additionalArgs = {} ) {
4+
return arguments.user.getId() == 2;
5+
}
6+
7+
}

0 commit comments

Comments
 (0)