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 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 unittest {
19 	import vibe.http.router : URLRouter;
20 	import vibe.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 RequiresAuthAttribute requiresAuth()
92 {
93 	return RequiresAuthAttribute.init;
94 }
95 
96 /** Enforces authentication and authorization.
97 
98 	Params:
99 		roles = Role expression to control authorization. If no role
100 			set is given, any authenticated user is granted access.
101 */
102 AuthAttribute!R auth(R)(R roles) { return AuthAttribute!R.init; }
103 
104 /** Enforces only authentication.
105 */
106 @property AuthAttribute!void anyAuth() { return AuthAttribute!void.init; }
107 
108 /** Disables authentication checks.
109 */
110 @property NoAuthAttribute noAuth() { return NoAuthAttribute.init; }
111 
112 /// private
113 struct RequiresAuthAttribute {}
114 
115 /// private
116 struct AuthAttribute(R) { alias Roles = R; }
117 
118 // private
119 struct NoAuthAttribute {}
120 
121 /** Represents a required authorization role.
122 
123 	Roles can be combined using logical or (`|` operator) or logical and (`&`
124 	operator). The role name is directly mapped to a method name of the
125 	authorization interface specified on the web interface class using the
126 	`@requiresAuth` attribute.
127 
128 	See_Also: `auth`
129 */
130 struct Role {
131 	@disable this();
132 
133 	static @property R!(Op.ident, name, void, void) opDispatch(string name)() { return R!(Op.ident, name, void, void).init; }
134 }
135 
136 package auto handleAuthentication(alias fun, C)(C c, HTTPServerRequest req, HTTPServerResponse res)
137 {
138 	import std.traits : MemberFunctionsTuple;
139 
140 	alias AI = AuthInfo!C;
141 	enum funname = __traits(identifier, fun);
142 
143 	static if (!is(AI == void)) {
144 		alias AR = GetAuthAttribute!fun;
145 		static if (findFirstUDA!(NoAuthAttribute, fun).found) {
146 			static assert (is(AR == void), "Method "~funname~" specifies both, @noAuth and @auth(...)/@anyAuth attributes.");
147 			static assert(!hasParameterType!(fun, AI), "Method "~funname~" is attributed @noAuth, but also has an "~AI.stringof~" paramter.");
148 			// nothing to do
149 		} else {
150 			static assert(!is(AR == void), "Missing @auth(...)/@anyAuth attribute for method "~funname~".");
151 
152 			static if (!__traits(compiles, () @safe { c.authenticate(req, res); } ()))
153 				pragma(msg, "Non-@safe .authenticate() methods are deprecated - annotate "~C.stringof~".authenticate() with @safe or @trusted.");
154 			return () @trusted { return c.authenticate(req, res); } ();
155 		}
156 	} else {
157 		// make sure that there are no @auth/@noAuth annotations for non-authorizing classes
158 		foreach (mem; __traits(allMembers, C))
159 			foreach (fun; MemberFunctionsTuple!(C, mem)) {
160 				static if (__traits(getProtection, fun) == "public") {
161 					static assert (!findFirstUDA!(NoAuthAttribute, C).found,
162 						"@noAuth attribute on method "~funname~" is not allowed without annotating "~C.stringof~" with @requiresAuth.");
163 					static assert (is(GetAuthAttribute!fun == void),
164 						"@auth(...)/@anyAuth attribute on method "~funname~" is not allowed without annotating "~C.stringof~" with @requiresAuth.");
165 				}
166 			}
167 	}
168 }
169 
170 package void handleAuthorization(C, alias fun, PARAMS...)(AuthInfo!C auth_info)
171 {
172 	import std.traits : MemberFunctionsTuple, ParameterIdentifierTuple;
173 	import vibe.internal.meta.typetuple : Group;
174 
175 	alias AI = AuthInfo!C;
176 	alias ParamNames = Group!(ParameterIdentifierTuple!fun);
177 
178 	static if (!is(AI == void)) {
179 		static if (!findFirstUDA!(NoAuthAttribute, fun).found) {
180 			alias AR = GetAuthAttribute!fun;
181 			static if (!is(AR.Roles == void)) {
182 				static if (!__traits(compiles, () @safe { evaluate!(__traits(identifier, fun), AR.Roles, AI, ParamNames, PARAMS)(auth_info); } ()))
183 					pragma(msg, "Non-@safe role evaluator methods are deprecated - annotate "~C.stringof~"."~__traits(identifier, fun)~"() with @safe or @trusted.");
184 				if (!() @trusted { return evaluate!(__traits(identifier, fun), AR.Roles, AI, ParamNames, PARAMS)(auth_info); } ())
185 					throw new HTTPStatusException(HTTPStatus.forbidden, "Not allowed to access this resource.");
186 			}
187 			// successfully authorized, fall-through
188 		}
189 	}
190 }
191 
192 package template isAuthenticated(C, alias fun) {
193 	static if (is(AuthInfo!C == void)) {
194 		static assert(!findFirstUDA!(NoAuthAttribute, fun).found && !findFirstUDA!(AuthAttribute, fun).found,
195 			C.stringof~"."~__traits(identifier, fun)~": @auth/@anyAuth/@noAuth attributes require @requiresAuth attribute on the containing class.");
196 		enum isAuthenticated = false;
197 	} else {
198 		static assert(findFirstUDA!(NoAuthAttribute, fun).found || findFirstUDA!(AuthAttribute, fun).found,
199 			C.stringof~"."~__traits(identifier, fun)~": Endpoint method must be annotated with either of @auth/@anyAuth/@noAuth.");
200 		enum isAuthenticated = !findFirstUDA!(NoAuthAttribute, fun).found;
201 	}
202 }
203 
204 unittest {
205 	class C {
206 		@noAuth void a() {}
207 		@auth(Role.test) void b() {}
208 		@anyAuth void c() {}
209 		void d() {}
210 	}
211 
212 	static assert(!is(typeof(isAuthenticated!(C, C.a))));
213 	static assert(!is(typeof(isAuthenticated!(C, C.b))));
214 	static assert(!is(typeof(isAuthenticated!(C, C.c))));
215 	static assert(!isAuthenticated!(C, C.d));
216 
217 	@requiresAuth
218 	class D {
219 		@noAuth void a() {}
220 		@auth(Role.test) void b() {}
221 		@anyAuth void c() {}
222 		void d() {}
223 	}
224 
225 	static assert(!isAuthenticated!(D, D.a));
226 	static assert(isAuthenticated!(D, D.b));
227 	static assert(isAuthenticated!(D, D.c));
228 	static assert(!is(typeof(isAuthenticated!(D, D.d))));
229 }
230 
231 
232 package template AuthInfo(C, CA = C)
233 {
234 	import std.traits : BaseTypeTuple, isInstanceOf;
235 	alias ATTS = AliasSeq!(__traits(getAttributes, CA));
236 	alias BASES = BaseTypeTuple!CA;
237 
238 	template impl(size_t idx) {
239 		static if (idx < ATTS.length) {
240 			static if (is(typeof(ATTS[idx])) && is(typeof(ATTS[idx]) == RequiresAuthAttribute)) {
241 				static if (is(typeof(C.init.authenticate(HTTPServerRequest.init, HTTPServerResponse.init))))
242 					alias impl = typeof(C.init.authenticate(HTTPServerRequest.init, HTTPServerResponse.init));
243 				else
244 					static assert (false,
245 						C.stringof~" must have an authenticate(...) method that takes HTTPServerRequest/HTTPServerResponse parameters and returns an authentication information object.");
246 			} else alias impl = impl!(idx+1);
247 		} else alias impl = void;
248 	}
249 
250 	template cimpl(size_t idx) {
251 		static if (idx < BASES.length) {
252 			alias AI = AuthInfo!(C, BASES[idx]);
253 			static if (is(AI == void)) alias cimpl = cimpl!(idx+1);
254 			else alias cimpl = AI;
255 		} else alias cimpl = void;
256 	}
257 
258 	static if (!is(impl!0 == void)) alias AuthInfo = impl!0;
259 	else alias AuthInfo = cimpl!0;
260 }
261 
262 unittest {
263 	@requiresAuth
264 	static class I {
265 		static struct A {}
266 	}
267 	static assert (!is(AuthInfo!I)); // missing authenticate method
268 
269 	@requiresAuth
270 	static class J {
271 		static struct A {
272 		}
273 		A authenticate(HTTPServerRequest, HTTPServerResponse) { return A.init; }
274 	}
275 	static assert (is(AuthInfo!J == J.A));
276 
277 	static class K {}
278 	static assert (is(AuthInfo!K == void));
279 
280 	static class L : J {}
281 	static assert (is(AuthInfo!L == J.A));
282 
283 	@requiresAuth
284 	interface M {
285 		static struct A {
286 		}
287 	}
288 	static class N : M {
289 		A authenticate(HTTPServerRequest, HTTPServerResponse) { return A.init; }
290 	}
291 	static assert (is(AuthInfo!N == M.A));
292 }
293 
294 private template GetAuthAttribute(alias fun)
295 {
296 	import std.traits : isInstanceOf;
297 	alias ATTS = AliasSeq!(__traits(getAttributes, fun));
298 
299 	template impl(size_t idx) {
300 		static if (idx < ATTS.length) {
301 			static if (is(typeof(ATTS[idx])) && isInstanceOf!(AuthAttribute, typeof(ATTS[idx]))) {
302 				alias impl = typeof(ATTS[idx]);
303 				static assert(is(impl!(idx+1) == void), "Method "~__traits(identifier, fun)~" may only specify one @auth attribute.");
304 			} else alias impl = impl!(idx+1);
305 		} else alias impl = void;
306 	}
307 	alias GetAuthAttribute = impl!0;
308 }
309 
310 unittest {
311 	@auth(Role.a) void c();
312 	static assert(is(GetAuthAttribute!c.Roles == typeof(Role.a)));
313 
314 	void d();
315 	static assert(is(GetAuthAttribute!d == void));
316 
317 	@anyAuth void a();
318 	static assert(is(GetAuthAttribute!a.Roles == void));
319 
320 	@anyAuth @anyAuth void b();
321 	static assert(!is(GetAuthAttribute!b));
322 
323 }
324 
325 private enum Op { none, and, or, ident }
326 
327 private struct R(Op op_, string ident_, Left_, Right_) {
328 	alias op = op_;
329 	enum ident = ident_;
330 	alias Left = Left_;
331 	alias Right = Right_;
332 
333 	R!(Op.or, null, R, O) opBinary(string op : "|", O)(O other) { return R!(Op.or, null, R, O).init; }
334 	R!(Op.and, null, R, O) opBinary(string op : "&", O)(O other) { return R!(Op.and, null, R, O).init; }
335 }
336 
337 private bool evaluate(string methodname, R, A, alias ParamNames, PARAMS...)(ref A a)
338 {
339 	import std.ascii : toUpper;
340 	import std.traits : ParameterTypeTuple, ParameterIdentifierTuple;
341 
342 	static if (R.op == Op.ident) {
343 		enum fname = "is" ~ toUpper(R.ident[0]) ~ R.ident[1 .. $];
344 		alias func = AliasSeq!(__traits(getMember, a, fname))[0];
345 		alias fpNames = ParameterIdentifierTuple!func;
346 		alias FPTypes = ParameterTypeTuple!func;
347 		FPTypes params;
348 		foreach (i, P; FPTypes) {
349 			enum name = fpNames[i];
350 			enum j = staticIndexOf!(name, ParamNames.expand);
351 			static assert(j >= 0, "Missing parameter "~name~" to evaluate @auth attribute for method "~methodname~".");
352 			static assert (is(typeof(PARAMS[j]) == P),
353 				"Parameter "~name~" of "~methodname~" is expected to have type "~P.stringof~" to match @auth attribute.");
354 			params[i] = PARAMS[j];
355 		}
356 		return __traits(getMember, a, fname)(params);
357 	}
358 	else static if (R.op == Op.and) return evaluate!(methodname, R.Left, A, ParamNames, PARAMS)(a) && evaluate!(methodname, R.Right, A, ParamNames, PARAMS)(a);
359 	else static if (R.op == Op.or) return evaluate!(methodname, R.Left, A, ParamNames, PARAMS)(a) || evaluate!(methodname, R.Right, A, ParamNames, PARAMS)(a);
360 	else return true;
361 }
362 
363 unittest {
364 	import vibe.internal.meta.typetuple : Group;
365 
366 	static struct AuthInfo {
367 		this(string u) { this.username = u; }
368 		string username;
369 
370 		bool isAdmin() { return this.username == "peter"; }
371 		bool isMember(int room) { return this.username == "tom"; }
372 	}
373 
374 	auto peter = AuthInfo("peter");
375 	auto tom = AuthInfo("tom");
376 
377 	{
378 		int room;
379 
380 		alias defargs = AliasSeq!(AuthInfo, Group!("room"), room);
381 
382 		auto ra = Role.admin;
383 		assert(evaluate!("x", typeof(ra), defargs)(peter) == true);
384 		assert(evaluate!("x", typeof(ra), defargs)(tom) == false);
385 
386 		auto rb = Role.member;
387 		assert(evaluate!("x", typeof(rb), defargs)(peter) == false);
388 		assert(evaluate!("x", typeof(rb), defargs)(tom) == true);
389 
390 		auto rc = Role.admin & Role.member;
391 		assert(evaluate!("x", typeof(rc), defargs)(peter) == false);
392 		assert(evaluate!("x", typeof(rc), defargs)(tom) == false);
393 
394 		auto rd = Role.admin | Role.member;
395 		assert(evaluate!("x", typeof(rd), defargs)(peter) == true);
396 		assert(evaluate!("x", typeof(rd), defargs)(tom) == true);
397 
398 		static assert(__traits(compiles, evaluate!("x", typeof(ra), AuthInfo, Group!())(peter)));
399 		static assert(!__traits(compiles, evaluate!("x", typeof(rb), AuthInfo, Group!())(peter)));
400 	}
401 
402 	{
403 		float room;
404 		static assert(!__traits(compiles, evaluate!("x", typeof(rb), AuthInfo, Group!("room"), room)(peter)));
405 	}
406 
407 	{
408 		int foo;
409 		static assert(!__traits(compiles, evaluate!("x", typeof(rb), AuthInfo, Group!("foo"), foo)(peter)));
410 	}
411 }