1 /**
2 Authentication and authorization framework based on fine-grained roles.
3
4 Copyright: © 2016 Sönke Ludwig
5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 Authors: Sönke Ludwig
7 */
8 module vibe.web.auth;
9
10 import vibe.http.common : HTTPStatusException;
11 import vibe.http.status : HTTPStatus;
12 import vibe.http.server : HTTPServerRequest, HTTPServerResponse;
13 import vibe.internal.meta.uda : findFirstUDA;
14
15 import std.meta : AliasSeq, staticIndexOf;
16
17 ///
18 @safe unittest {
19 import vibe.http.router : URLRouter;
20 import vibe.web.web : noRoute, registerWebInterface;
21
22 static struct AuthInfo {
23 @safe:
24 string userName;
25
26 bool isAdmin() { return this.userName == "tom"; }
27 bool isRoomMember(int chat_room) {
28 if (chat_room == 0)
29 return this.userName == "macy" || this.userName == "peter";
30 else if (chat_room == 1)
31 return this.userName == "macy";
32 else
33 return false;
34 }
35 bool isPremiumUser() { return this.userName == "peter"; }
36 }
37
38 @requiresAuth
39 static class ChatWebService {
40 @safe:
41 @noRoute AuthInfo authenticate(scope HTTPServerRequest req, scope HTTPServerResponse res)
42 {
43 if (req.headers["AuthToken"] == "foobar")
44 return AuthInfo(req.headers["AuthUser"]);
45 throw new HTTPStatusException(HTTPStatus.unauthorized);
46 }
47
48 @noAuth
49 void getLoginPage()
50 {
51 // code that can be executed for any client
52 }
53
54 @anyAuth
55 void getOverview()
56 {
57 // code that can be executed by any registered user
58 }
59
60 @auth(Role.admin)
61 void getAdminSection()
62 {
63 // code that may only be executed by adminitrators
64 }
65
66 @auth(Role.admin | Role.roomMember)
67 void getChatroomHistory(int chat_room)
68 {
69 // code that may execute for administrators or for chat room members
70 }
71
72 @auth(Role.roomMember & Role.premiumUser)
73 void getPremiumInformation(int chat_room)
74 {
75 // code that may only execute for users that are members of a room and have a premium subscription
76 }
77 }
78
79 void registerService(URLRouter router)
80 @safe {
81 router.registerWebInterface(new ChatWebService);
82 }
83 }
84
85
86 /**
87 Enables authentication and authorization checks for an interface class.
88
89 Web/REST interface classes that have authentication enabled are required
90 to specify either the `@auth` or the `@noAuth` attribute for every public
91 method.
92
93 The type of the authentication information, as returned by the
94 `authenticate()` method, can optionally be specified as a template argument.
95 This is useful if an `interface` is annotated and the `authenticate()`
96 method is only declared in the actual class implementation.
97 */
98 @property RequiresAuthAttribute!void requiresAuth()
99 {
100 return RequiresAuthAttribute!void.init;
101 }
102 /// ditto
103 @property RequiresAuthAttribute!AUTH_INFO requiresAuth(AUTH_INFO)()
104 {
105 return RequiresAuthAttribute!AUTH_INFO.init;
106 }
107
108 /** Enforces authentication and authorization.
109
110 Params:
111 roles = Role expression to control authorization. If no role
112 set is given, any authenticated user is granted access.
113 */
114 AuthAttribute!R auth(R)(R roles) { return AuthAttribute!R.init; }
115
116 /** Enforces only authentication.
117 */
118 @property AuthAttribute!void anyAuth() { return AuthAttribute!void.init; }
119
120 /** Disables authentication checks.
121 */
122 @property NoAuthAttribute noAuth() { return NoAuthAttribute.init; }
123
124 /// private
125 struct RequiresAuthAttribute(AUTH_INFO) { alias AuthInfo = AUTH_INFO; }
126
127 /// private
128 struct AuthAttribute(R) { alias Roles = R; }
129
130 // private
131 struct NoAuthAttribute {}
132
133 /** Represents a required authorization role.
134
135 Roles can be combined using logical or (`|` operator) or logical and (`&`
136 operator). The role name is directly mapped to a method name of the
137 authorization interface specified on the web interface class using the
138 `@requiresAuth` attribute.
139
140 See_Also: `auth`
141 */
142 struct Role {
143 @disable this();
144
145 static @property R!(Op.ident, name, void, void) opDispatch(string name)() { return R!(Op.ident, name, void, void).init; }
146 }
147
148 package auto handleAuthentication(alias fun, C)(C c, HTTPServerRequest req, HTTPServerResponse res)
149 {
150 import std.traits : MemberFunctionsTuple;
151
152 alias AI = AuthInfo!C;
153 enum funname = __traits(identifier, fun);
154
155 static if (!is(AI == void)) {
156 alias AR = GetAuthAttribute!fun;
157 static if (findFirstUDA!(NoAuthAttribute, fun).found) {
158 static assert (is(AR == void), "Method "~funname~" specifies both, @noAuth and @auth(...)/@anyAuth attributes.");
159 static assert(!hasParameterType!(fun, AI), "Method "~funname~" is attributed @noAuth, but also has an "~AI.stringof~" paramter.");
160 // nothing to do
161 } else {
162 static assert(!is(AR == void), "Missing @auth(...)/@anyAuth attribute for method "~funname~".");
163
164 static if (!__traits(compiles, () @safe { c.authenticate(req, res); } ()))
165 pragma(msg, "Non-@safe .authenticate() methods are deprecated - annotate "~C.stringof~".authenticate() with @safe or @trusted.");
166 return () @trusted { return c.authenticate(req, res); } ();
167 }
168 } else {
169 // make sure that there are no @auth/@noAuth annotations for non-authorizing classes
170 foreach (mem; __traits(allMembers, C))
171 foreach (fun; MemberFunctionsTuple!(C, mem)) {
172 static if (__traits(getProtection, fun) == "public") {
173 static assert (!findFirstUDA!(NoAuthAttribute, C).found,
174 "@noAuth attribute on method "~funname~" is not allowed without annotating "~C.stringof~" with @requiresAuth.");
175 static assert (is(GetAuthAttribute!fun == void),
176 "@auth(...)/@anyAuth attribute on method "~funname~" is not allowed without annotating "~C.stringof~" with @requiresAuth.");
177 }
178 }
179 }
180 }
181
182 package void handleAuthorization(C, alias fun, PARAMS...)(AuthInfo!C auth_info)
183 {
184 import std.traits : MemberFunctionsTuple, ParameterIdentifierTuple;
185 import vibe.internal.meta.typetuple : Group;
186
187 alias AI = AuthInfo!C;
188 alias ParamNames = Group!(ParameterIdentifierTuple!fun);
189
190 static if (!is(AI == void)) {
191 static if (!findFirstUDA!(NoAuthAttribute, fun).found) {
192 alias AR = GetAuthAttribute!fun;
193 static if (!is(AR.Roles == void)) {
194 static if (!__traits(compiles, () @safe { evaluate!(__traits(identifier, fun), AR.Roles, AI, ParamNames, PARAMS)(auth_info); } ()))
195 pragma(msg, "Non-@safe role evaluator methods are deprecated - annotate "~C.stringof~"."~__traits(identifier, fun)~"() with @safe or @trusted.");
196 if (!() @trusted { return evaluate!(__traits(identifier, fun), AR.Roles, AI, ParamNames, PARAMS)(auth_info); } ())
197 throw new HTTPStatusException(HTTPStatus.forbidden, "Not allowed to access this resource.");
198 }
199 // successfully authorized, fall-through
200 }
201 }
202 }
203
204 package template isAuthenticated(C, alias fun) {
205 static if (is(AuthInfo!C == void)) {
206 static assert(!findFirstUDA!(NoAuthAttribute, fun).found && !findFirstUDA!(AuthAttribute, fun).found,
207 C.stringof~"."~__traits(identifier, fun)~": @auth/@anyAuth/@noAuth attributes require @requiresAuth attribute on the containing class.");
208 enum isAuthenticated = false;
209 } else {
210 static assert(findFirstUDA!(NoAuthAttribute, fun).found || findFirstUDA!(AuthAttribute, fun).found,
211 C.stringof~"."~__traits(identifier, fun)~": Endpoint method must be annotated with either of @auth/@anyAuth/@noAuth.");
212 enum isAuthenticated = !findFirstUDA!(NoAuthAttribute, fun).found;
213 }
214 }
215
216 unittest {
217 class C {
218 @noAuth void a() {}
219 @auth(Role.test) void b() {}
220 @anyAuth void c() {}
221 void d() {}
222 }
223
224 static assert(!is(typeof(isAuthenticated!(C, C.a))));
225 static assert(!is(typeof(isAuthenticated!(C, C.b))));
226 static assert(!is(typeof(isAuthenticated!(C, C.c))));
227 static assert(!isAuthenticated!(C, C.d));
228
229 @requiresAuth
230 class D {
231 @noAuth void a() {}
232 @auth(Role.test) void b() {}
233 @anyAuth void c() {}
234 void d() {}
235 }
236
237 static assert(!isAuthenticated!(D, D.a));
238 static assert(isAuthenticated!(D, D.b));
239 static assert(isAuthenticated!(D, D.c));
240 static assert(!is(typeof(isAuthenticated!(D, D.d))));
241 }
242
243
244 package template AuthInfo(C, CA = C)
245 {
246 import std.traits : BaseTypeTuple, isInstanceOf;
247 alias ATTS = AliasSeq!(__traits(getAttributes, CA));
248 alias BASES = BaseTypeTuple!CA;
249
250 template impl(size_t idx) {
251 static if (idx < ATTS.length) {
252 static if (is(typeof(ATTS[idx])) && isInstanceOf!(RequiresAuthAttribute, typeof(ATTS[idx]))) {
253 static if (is(typeof(C.init.authenticate(HTTPServerRequest.init, HTTPServerResponse.init)))) {
254 alias impl = typeof(C.init.authenticate(HTTPServerRequest.init, HTTPServerResponse.init));
255 static assert(is(ATTS[idx].AuthInfo == void) || is(ATTS[idx].AuthInfo == impl),
256 "Type mismatch between the @requiresAuth annotation and the authenticate() method.");
257 } else static if (is(C == interface)) {
258 alias impl = ATTS[idx].AuthInfo;
259 static assert(!is(impl == void), "Interface "~C.stringof~" either needs to supply an authenticate method or must supply the authentication information via @requiresAuth!T.");
260 } else
261 static assert (false,
262 C.stringof~" must have an authenticate(...) method that takes HTTPServerRequest/HTTPServerResponse parameters and returns an authentication information object.");
263 } else alias impl = impl!(idx+1);
264 } else alias impl = void;
265 }
266
267 template cimpl(size_t idx) {
268 static if (idx < BASES.length) {
269 alias AI = AuthInfo!(C, BASES[idx]);
270 static if (is(AI == void)) alias cimpl = cimpl!(idx+1);
271 else alias cimpl = AI;
272 } else alias cimpl = void;
273 }
274
275 static if (!is(impl!0 == void)) alias AuthInfo = impl!0;
276 else alias AuthInfo = cimpl!0;
277 }
278
279 unittest {
280 @requiresAuth
281 static class I {
282 static struct A {}
283 }
284 static assert (!is(AuthInfo!I)); // missing authenticate method
285
286 @requiresAuth
287 static class J {
288 static struct A {
289 }
290 A authenticate(HTTPServerRequest, HTTPServerResponse) { return A.init; }
291 }
292 static assert (is(AuthInfo!J == J.A));
293
294 static class K {}
295 static assert (is(AuthInfo!K == void));
296
297 static class L : J {}
298 static assert (is(AuthInfo!L == J.A));
299
300 @requiresAuth
301 interface M {
302 static struct A {
303 }
304 }
305 static class N : M {
306 A authenticate(HTTPServerRequest, HTTPServerResponse) { return A.init; }
307 }
308 static assert (is(AuthInfo!N == M.A));
309 }
310
311 private template GetAuthAttribute(alias fun)
312 {
313 import std.traits : isInstanceOf;
314 alias ATTS = AliasSeq!(__traits(getAttributes, fun));
315
316 template impl(size_t idx) {
317 static if (idx < ATTS.length) {
318 static if (is(typeof(ATTS[idx])) && isInstanceOf!(AuthAttribute, typeof(ATTS[idx]))) {
319 alias impl = typeof(ATTS[idx]);
320 static assert(is(impl!(idx+1) == void), "Method "~__traits(identifier, fun)~" may only specify one @auth attribute.");
321 } else alias impl = impl!(idx+1);
322 } else alias impl = void;
323 }
324 alias GetAuthAttribute = impl!0;
325 }
326
327 unittest {
328 @auth(Role.a) void c();
329 static assert(is(GetAuthAttribute!c.Roles == typeof(Role.a)));
330
331 void d();
332 static assert(is(GetAuthAttribute!d == void));
333
334 @anyAuth void a();
335 static assert(is(GetAuthAttribute!a.Roles == void));
336
337 @anyAuth @anyAuth void b();
338 static assert(!is(GetAuthAttribute!b));
339
340 }
341
342 private enum Op { none, and, or, ident }
343
344 private struct R(Op op_, string ident_, Left_, Right_) {
345 alias op = op_;
346 enum ident = ident_;
347 alias Left = Left_;
348 alias Right = Right_;
349
350 R!(Op.or, null, R, O) opBinary(string op : "|", O)(O other) { return R!(Op.or, null, R, O).init; }
351 R!(Op.and, null, R, O) opBinary(string op : "&", O)(O other) { return R!(Op.and, null, R, O).init; }
352 }
353
354 private bool evaluate(string methodname, R, A, alias ParamNames, PARAMS...)(ref A a)
355 {
356 import std.ascii : toUpper;
357 import std.traits : ParameterTypeTuple, ParameterIdentifierTuple;
358
359 static if (R.op == Op.ident) {
360 enum fname = "is" ~ toUpper(R.ident[0]) ~ R.ident[1 .. $];
361 alias func = AliasSeq!(__traits(getMember, a, fname))[0];
362 alias fpNames = ParameterIdentifierTuple!func;
363 alias FPTypes = ParameterTypeTuple!func;
364 FPTypes params;
365 foreach (i, P; FPTypes) {
366 enum name = fpNames[i];
367 enum j = staticIndexOf!(name, ParamNames.expand);
368 static assert(j >= 0, "Missing parameter "~name~" to evaluate @auth attribute for method "~methodname~".");
369 static assert (is(typeof(PARAMS[j]) == P),
370 "Parameter "~name~" of "~methodname~" is expected to have type "~P.stringof~" to match @auth attribute.");
371 params[i] = PARAMS[j];
372 }
373 return __traits(getMember, a, fname)(params);
374 }
375 else static if (R.op == Op.and) return evaluate!(methodname, R.Left, A, ParamNames, PARAMS)(a) && evaluate!(methodname, R.Right, A, ParamNames, PARAMS)(a);
376 else static if (R.op == Op.or) return evaluate!(methodname, R.Left, A, ParamNames, PARAMS)(a) || evaluate!(methodname, R.Right, A, ParamNames, PARAMS)(a);
377 else return true;
378 }
379
380 unittest {
381 import vibe.internal.meta.typetuple : Group;
382
383 static struct AuthInfo {
384 this(string u) { this.username = u; }
385 string username;
386
387 bool isAdmin() { return this.username == "peter"; }
388 bool isMember(int room) { return this.username == "tom"; }
389 }
390
391 auto peter = AuthInfo("peter");
392 auto tom = AuthInfo("tom");
393
394 {
395 int room;
396
397 alias defargs = AliasSeq!(AuthInfo, Group!("room"), room);
398
399 auto ra = Role.admin;
400 assert(evaluate!("x", typeof(ra), defargs)(peter) == true);
401 assert(evaluate!("x", typeof(ra), defargs)(tom) == false);
402
403 auto rb = Role.member;
404 assert(evaluate!("x", typeof(rb), defargs)(peter) == false);
405 assert(evaluate!("x", typeof(rb), defargs)(tom) == true);
406
407 auto rc = Role.admin & Role.member;
408 assert(evaluate!("x", typeof(rc), defargs)(peter) == false);
409 assert(evaluate!("x", typeof(rc), defargs)(tom) == false);
410
411 auto rd = Role.admin | Role.member;
412 assert(evaluate!("x", typeof(rd), defargs)(peter) == true);
413 assert(evaluate!("x", typeof(rd), defargs)(tom) == true);
414
415 static assert(__traits(compiles, evaluate!("x", typeof(ra), AuthInfo, Group!())(peter)));
416 static assert(!__traits(compiles, evaluate!("x", typeof(rb), AuthInfo, Group!())(peter)));
417 }
418
419 {
420 float room;
421 static assert(!__traits(compiles, evaluate!("x", typeof(rb), AuthInfo, Group!("room"), room)(peter)));
422 }
423
424 {
425 int foo;
426 static assert(!__traits(compiles, evaluate!("x", typeof(rb), AuthInfo, Group!("foo"), foo)(peter)));
427 }
428 }