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 }