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 }