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, AUTH_ARGS...)(C c, AUTH_ARGS auth_args) 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(auth_args); } ())) 165 pragma(msg, "Non-@safe .authenticate() methods are deprecated - annotate "~C.stringof~".authenticate() with @safe or @trusted."); 166 return () @trusted { return c.authenticate(auth_args); } (); 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 }