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 }