1 /**
2 	Authentication and authorization framework based on fine-grained roles.
3 
4 	Copyright: © 2016 RejectedSoftware e.K.
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 hb.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 unittest {
19 	import vibe.http.router : URLRouter;
20 	import hb.web.web : noRoute, registerWebInterface;
21 
22 	static struct AuthInfo {
23 		string userName;
24 
25 		bool isAdmin() { return this.userName == "tom"; }
26 		bool isRoomMember(int chat_room) {
27 			if (chat_room == 0)
28 				return this.userName == "macy" || this.userName == "peter";
29 			else if (chat_room == 1)
30 				return this.userName == "macy";
31 			else
32 				return false;
33 		}
34 		bool isPremiumUser() { return this.userName == "peter"; }
35 	}
36 
37 	@requiresAuth
38 	static class ChatWebService {
39 		@noRoute AuthInfo authenticate(scope HTTPServerRequest req, scope HTTPServerResponse res)
40 		{
41 			if (req.headers["AuthToken"] == "foobar")
42 				return AuthInfo(req.headers["AuthUser"]);
43 			throw new HTTPStatusException(HTTPStatus.unauthorized);
44 		}
45 
46 		@noAuth
47 		void getLoginPage()
48 		{
49 			// code that can be executed for any client
50 		}
51 
52 		@anyAuth
53 		void getOverview()
54 		{
55 			// code that can be executed by any registered user
56 		}
57 
58 		@auth(Role.admin)
59 		void getAdminSection()
60 		{
61 			// code that may only be executed by adminitrators
62 		}
63 
64 		@auth(Role.admin | Role.roomMember)
65 		void getChatroomHistory(int chat_room)
66 		{
67 			// code that may execute for administrators or for chat room members
68 		}
69 
70 		@auth(Role.roomMember & Role.premiumUser)
71 		void getPremiumInformation(int chat_room)
72 		{
73 			// code that may only execute for users that are members of a room and have a premium subscription
74 		}
75 	}
76 
77 	void registerService(URLRouter router)
78 	{
79 		router.registerWebInterface(new ChatWebService);
80 	}
81 }
82 
83 
84 /**
85 	Enables authentication and authorization checks for an interface class.
86 
87 	Web/REST interface classes that have authentication enabled are required
88 	to specify either the `@auth` or the `@noAuth` attribute for every public
89 	method.
90 */
91 @property auto requiresAuth()
92 {
93 	return RequiresAuthAttribute!(null).init;
94 }
95 
96 /// ditto
97 @property auto requiresAuth(alias authenticate)()
98 {
99 	return RequiresAuthAttribute!(authenticate).init;
100 }
101 
102 /** Enforces authentication and authorization.
103 
104 	Params:
105 		roles = Role expression to control authorization. If no role
106 			set is given, any authenticated user is granted access.
107 */
108 AuthAttribute!R auth(R)(R roles) { return AuthAttribute!R.init; }
109 
110 /** Enforces only authentication.
111 */
112 @property AuthAttribute!void anyAuth() { return AuthAttribute!void.init; }
113 @property AuthAttribute!void auth() { return AuthAttribute!void.init; }
114 
115 /** Disables authentication checks.
116 */
117 @property NoAuthAttribute noAuth() { return NoAuthAttribute.init; }
118 
119 /// private
120 struct RequiresAuthAttribute(alias T = null) {}
121 
122 /// private
123 struct AuthAttribute(R) { alias Roles = R; }
124 
125 // private
126 struct NoAuthAttribute {}
127 
128 /** Represents a required authorization role.
129 
130 	Roles can be combined using logical or (`|` operator) or logical and (`&`
131 	operator). The role name is directly mapped to a method name of the
132 	authorization interface specified on the web interface class using the
133 	`@requiresAuth` attribute.
134 
135 	See_Also: `auth`
136 */
137 struct Role {
138 	@disable this();
139 
140 	static @property R!(Op.ident, name, void, void) opDispatch(string name)() { return R!(Op.ident, name, void, void).init; }
141 }
142 
143 package auto handleAuthentication(alias fun, C)(C c, HTTPServerRequest req, HTTPServerResponse res)
144 {
145 	import std.traits : MemberFunctionsTuple;
146 
147 	alias AI = AuthInfo!C;
148 	enum funname = __traits(identifier, fun);
149 
150 	static if (!is(AI == void)) {
151 		alias AR = GetAuthAttribute!fun;
152 		static if (findFirstUDA!(NoAuthAttribute, fun).found) {
153 			static assert (is(AR == void), "Method "~funname~" specifies both, @noAuth and @auth(...)/@anyAuth attributes.");
154 			static assert(!hasParameterType!(fun, AI), "Method "~funname~" is attributed @noAuth, but also has an "~AI.stringof~" paramter.");
155 			// nothing to do
156 		} else {
157 			static assert(!is(AR == void), "Missing @auth(...)/@anyAuth attribute for method "~funname~".");
158 
159 			alias assocFun = FunAuthInfo!C;
160 			static if (is(assocFun == void)) {
161 				static if (!__traits(compiles, () @safe { c.authenticate(req, res); } ()))
162 					pragma(msg, "Non-@safe .authenticate() methods are deprecated - annotate "~C.stringof~".authenticate() with @safe or @trusted.");
163 				return () @trusted { return c.authenticate(req, res); } ();
164 			} else {
165 				return assocFun(req, res);
166 			}
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 // gets the associated alias function
244 package template FunAuthInfo(C, CA = C)
245 {
246 	import std.traits : BaseTypeTuple, isInstanceOf, TemplateArgsOf;
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 				alias templArgs = TemplateArgsOf!(typeof(ATTS[idx]));
254 				static if (is(typeof(C.init.authenticate(HTTPServerRequest.init, HTTPServerResponse.init))))
255 					alias impl = void;
256 				else static if (is(typeof(templArgs[0](HTTPServerRequest.init, HTTPServerResponse.init))))
257 					alias impl = templArgs[0];
258 				else
259 					static assert (false,
260 						C.stringof~" must have an authenticate(...) method that takes HTTPServerRequest/HTTPServerResponse parameters and returns an authentication information object.");
261 			} else alias impl = impl!(idx+1);
262 		} else alias impl = void;
263 	}
264 
265 	template cimpl(size_t idx) {
266 		static if (idx < BASES.length) {
267 			alias AI = AuthInfo!(C, BASES[idx]);
268 			static if (is(AI == void)) alias cimpl = cimpl!(idx+1);
269 			else alias cimpl = AI;
270 		} else alias cimpl = void;
271 	}
272 
273 	static if (!is(impl!0 == void)) alias FunAuthInfo = impl!0;
274 	else alias FunAuthInfo = cimpl!0;
275 }
276 
277 package template AuthInfo(C, CA = C)
278 {
279 	import std.traits : BaseTypeTuple, isInstanceOf, TemplateArgsOf;
280 	alias ATTS = AliasSeq!(__traits(getAttributes, CA));
281 	alias BASES = BaseTypeTuple!CA;
282 
283 	template impl(size_t idx) {
284 		static if (idx < ATTS.length) {
285 			static if (is(typeof(ATTS[idx])) && isInstanceOf!(RequiresAuthAttribute, typeof(ATTS[idx]))) {
286 				alias templArgs = TemplateArgsOf!(typeof(ATTS[idx]));
287 				static if (is(typeof(C.init.authenticate(HTTPServerRequest.init, HTTPServerResponse.init))))
288 					alias impl = typeof(C.init.authenticate(HTTPServerRequest.init, HTTPServerResponse.init));
289 				else static if (is(typeof(templArgs[0](HTTPServerRequest.init, HTTPServerResponse.init))))
290 					alias impl = typeof(templArgs[0](HTTPServerRequest.init, HTTPServerResponse.init));
291 				else
292 					static assert (false,
293 						C.stringof~" must have an authenticate(...) method that takes HTTPServerRequest/HTTPServerResponse parameters and returns an authentication information object.");
294 			} else alias impl = impl!(idx+1);
295 		} else alias impl = void;
296 	}
297 
298 	template cimpl(size_t idx) {
299 		static if (idx < BASES.length) {
300 			alias AI = AuthInfo!(C, BASES[idx]);
301 			static if (is(AI == void)) alias cimpl = cimpl!(idx+1);
302 			else alias cimpl = AI;
303 		} else alias cimpl = void;
304 	}
305 
306 	static if (!is(impl!0 == void)) alias AuthInfo = impl!0;
307 	else alias AuthInfo = cimpl!0;
308 }
309 
310 unittest {
311 	@requiresAuth
312 	static class I {
313 		static struct A {}
314 	}
315 	static assert (!is(AuthInfo!I)); // missing authenticate method
316 
317 	@requiresAuth
318 	static class J {
319 		static struct A {
320 		}
321 		A authenticate(HTTPServerRequest, HTTPServerResponse) { return A.init; }
322 	}
323 	static assert (is(AuthInfo!J == J.A));
324 
325 	static class K {}
326 	static assert (is(AuthInfo!K == void));
327 
328 	static class L : J {}
329 	static assert (is(AuthInfo!L == J.A));
330 
331 	@requiresAuth
332 	interface M {
333 		static struct A {
334 		}
335 	}
336 	static class N : M {
337 		A authenticate(HTTPServerRequest, HTTPServerResponse) { return A.init; }
338 	}
339 	static assert (is(AuthInfo!N == M.A));
340 }
341 
342 private template GetAuthAttribute(alias fun)
343 {
344 	import std.traits : isInstanceOf;
345 	alias ATTS = AliasSeq!(__traits(getAttributes, fun));
346 
347 	template impl(size_t idx) {
348 		static if (idx < ATTS.length) {
349 			static if (is(typeof(ATTS[idx])) && isInstanceOf!(AuthAttribute, typeof(ATTS[idx]))) {
350 				alias impl = typeof(ATTS[idx]);
351 				static assert(is(impl!(idx+1) == void), "Method "~__traits(identifier, fun)~" may only specify one @auth attribute.");
352 			} else alias impl = impl!(idx+1);
353 		} else alias impl = void;
354 	}
355 	alias GetAuthAttribute = impl!0;
356 }
357 
358 unittest {
359 	@auth(Role.a) void c();
360 	static assert(is(GetAuthAttribute!c.Roles == typeof(Role.a)));
361 
362 	void d();
363 	static assert(is(GetAuthAttribute!d == void));
364 
365 	@anyAuth void a();
366 	static assert(is(GetAuthAttribute!a.Roles == void));
367 
368 	@anyAuth @anyAuth void b();
369 	static assert(!is(GetAuthAttribute!b));
370 
371 }
372 
373 private enum Op { none, and, or, ident }
374 
375 private struct R(Op op_, string ident_, Left_, Right_) {
376 	alias op = op_;
377 	enum ident = ident_;
378 	alias Left = Left_;
379 	alias Right = Right_;
380 
381 	R!(Op.or, null, R, O) opBinary(string op : "|", O)(O other) { return R!(Op.or, null, R, O).init; }
382 	R!(Op.and, null, R, O) opBinary(string op : "&", O)(O other) { return R!(Op.and, null, R, O).init; }
383 }
384 
385 private bool evaluate(string methodname, R, A, alias ParamNames, PARAMS...)(ref A a)
386 {
387 	import std.ascii : toUpper;
388 	import std.traits : ParameterTypeTuple, ParameterIdentifierTuple;
389 
390 	static if (R.op == Op.ident) {
391 		enum fname = "is" ~ toUpper(R.ident[0]) ~ R.ident[1 .. $];
392 		alias func = AliasSeq!(__traits(getMember, a, fname))[0];
393 		alias fpNames = ParameterIdentifierTuple!func;
394 		alias FPTypes = ParameterTypeTuple!func;
395 		FPTypes params;
396 		foreach (i, P; FPTypes) {
397 			enum name = fpNames[i];
398 			enum j = staticIndexOf!(name, ParamNames.expand);
399 			static assert(j >= 0, "Missing parameter "~name~" to evaluate @auth attribute for method "~methodname~".");
400 			static assert (is(typeof(PARAMS[j]) == P),
401 				"Parameter "~name~" of "~methodname~" is expected to have type "~P.stringof~" to match @auth attribute.");
402 			params[i] = PARAMS[j];
403 		}
404 		return __traits(getMember, a, fname)(params);
405 	}
406 	else static if (R.op == Op.and) return evaluate!(methodname, R.Left, A, ParamNames, PARAMS)(a) && evaluate!(methodname, R.Right, A, ParamNames, PARAMS)(a);
407 	else static if (R.op == Op.or) return evaluate!(methodname, R.Left, A, ParamNames, PARAMS)(a) || evaluate!(methodname, R.Right, A, ParamNames, PARAMS)(a);
408 	else return true;
409 }
410 
411 unittest {
412 	import vibe.internal.meta.typetuple : Group;
413 
414 	static struct AuthInfo {
415 		this(string u) { this.username = u; }
416 		string username;
417 
418 		bool isAdmin() { return this.username == "peter"; }
419 		bool isMember(int room) { return this.username == "tom"; }
420 	}
421 
422 	auto peter = AuthInfo("peter");
423 	auto tom = AuthInfo("tom");
424 
425 	{
426 		int room;
427 
428 		alias defargs = AliasSeq!(AuthInfo, Group!("room"), room);
429 
430 		auto ra = Role.admin;
431 		assert(evaluate!("x", typeof(ra), defargs)(peter) == true);
432 		assert(evaluate!("x", typeof(ra), defargs)(tom) == false);
433 
434 		auto rb = Role.member;
435 		assert(evaluate!("x", typeof(rb), defargs)(peter) == false);
436 		assert(evaluate!("x", typeof(rb), defargs)(tom) == true);
437 
438 		auto rc = Role.admin & Role.member;
439 		assert(evaluate!("x", typeof(rc), defargs)(peter) == false);
440 		assert(evaluate!("x", typeof(rc), defargs)(tom) == false);
441 
442 		auto rd = Role.admin | Role.member;
443 		assert(evaluate!("x", typeof(rd), defargs)(peter) == true);
444 		assert(evaluate!("x", typeof(rd), defargs)(tom) == true);
445 
446 		static assert(__traits(compiles, evaluate!("x", typeof(ra), AuthInfo, Group!())(peter)));
447 		static assert(!__traits(compiles, evaluate!("x", typeof(rb), AuthInfo, Group!())(peter)));
448 	}
449 
450 	{
451 		float room;
452 		static assert(!__traits(compiles, evaluate!("x", typeof(rb), AuthInfo, Group!("room"), room)(peter)));
453 	}
454 
455 	{
456 		int foo;
457 		static assert(!__traits(compiles, evaluate!("x", typeof(rb), AuthInfo, Group!("foo"), foo)(peter)));
458 	}
459 }