1 /** 2 Database abstraction layer 3 4 Copyright: © 2012-2014 RejectedSoftware e.K. 5 License: Subject to the terms of the General Public License version 3, as written in the included LICENSE.txt file. 6 Authors: David Suppiger 7 */ 8 module userman.db.redis; 9 10 import userman.db.controller; 11 12 import vibe.db.redis.redis; 13 import vibe.db.redis.idioms; 14 import vibe.db.redis.types; 15 import vibe.data.bson; 16 import vibe.data.json; 17 import vibe.utils.validation; 18 19 import std.datetime; 20 import std.exception; 21 import std.string; 22 import std.conv; 23 import std.range : front; 24 25 26 class RedisUserManController : UserManController { 27 @trusted: // The whole Redis API is not yet @safe 28 29 private { 30 RedisClient m_redisClient; 31 RedisDatabase m_redisDB; 32 33 RedisObjectCollection!(RedisStripped!User, RedisCollectionOptions.supportPaging) m_users; 34 RedisObjectCollection!(AuthInfo, RedisCollectionOptions.none) m_userAuthInfo; 35 RedisCollection!(RedisHash!string, RedisCollectionOptions.none) m_userProperties; 36 RedisObjectCollection!(RedisStripped!(Group, false), RedisCollectionOptions.supportPaging) m_groups; 37 RedisCollection!(RedisHash!string, RedisCollectionOptions.none) m_groupProperties; 38 //RedisCollection!(RedisSet!GroupMember, RedisCollectionOptions.none) m_groupMembers; 39 40 // secondary indexes 41 RedisHash!(long) m_usersByName; 42 RedisHash!(long) m_usersByEmail; 43 RedisCollection!(RedisSet!long, RedisCollectionOptions.none) m_userMemberships; 44 //RedisHash!(long[]) m_userMemberships; 45 RedisHash!long m_groupsByName; 46 } 47 48 this(UserManSettings settings) 49 { 50 super(settings); 51 52 string schema = "redis"; 53 auto idx = settings.databaseURL.indexOf("://"); 54 if (idx > 0) 55 schema = settings.databaseURL[0..idx]; 56 57 enforce(schema == "redis", "databaseURL must be a redis connection string"); 58 59 // Parse string by replacing schema with 'http' as URL won't parse redis 60 // URLs correctly. 61 string url_string = settings.databaseURL; 62 if (idx > 0) 63 url_string = url_string[idx+3..$]; 64 65 URL url = URL("http://" ~ url_string); 66 url.schema = "redis"; 67 68 long dbIndex = 0; 69 if (!url.path.empty) 70 dbIndex = to!long(url.path.bySegment.front.name); 71 72 m_redisClient = connectRedis(url.host, url.port == ushort.init ? 6379 : url.port); 73 m_redisDB = m_redisClient.getDatabase(dbIndex); 74 75 m_users = RedisObjectCollection!(RedisStripped!User, RedisCollectionOptions.supportPaging)(m_redisDB, "userman:user"); 76 m_userAuthInfo = RedisObjectCollection!(AuthInfo, RedisCollectionOptions.none)(m_redisDB, "userman:user", "auth"); 77 m_userProperties = RedisCollection!(RedisHash!string, RedisCollectionOptions.none)(m_redisDB, "userman:user", "properties"); 78 m_groups = RedisObjectCollection!(RedisStripped!(Group, false), RedisCollectionOptions.supportPaging)(m_redisDB, "userman:group"); 79 m_groupProperties = RedisCollection!(RedisHash!string, RedisCollectionOptions.none)(m_redisDB, "userman:group", "properties"); 80 //m_groupMembers = RedisCollection!(RedisSet!GroupMember, RedisCollectionOptions.none)(m_redisDB, "userman:group", "members"); 81 m_usersByName = RedisHash!long(m_redisDB, "userman:user:byName"); 82 m_usersByEmail = RedisHash!long(m_redisDB, "userman:user:byEmail"); 83 //m_userMemberships = RedisCollection!(RedisSet!long, RedisCollectionOptions.none)(m_redisDB, "userman:user", "memberships"); 84 m_groupsByName = RedisHash!long(m_redisDB, "userman:group:byName"); 85 } 86 87 override bool isEmailRegistered(string email) 88 { 89 auto uid = m_usersByEmail.get(email, -1); 90 if (uid >= 0){ 91 string method = m_userAuthInfo[uid].method; 92 return method != string.init && method.length > 0; 93 } 94 return false; 95 } 96 97 override User.ID addUser(ref User usr) 98 { 99 validateUser(usr); 100 101 enforce(!m_usersByName.exists(usr.name), "The user name is already taken."); 102 enforce(!m_usersByEmail.exists(usr.email), "The email address is already taken."); 103 104 auto uid = m_users.createID(); 105 scope (failure) m_users.remove(uid); 106 usr.id = User.ID(uid); 107 108 // Indexes 109 enforce(m_usersByEmail.setIfNotExist(usr.email, uid), "Failed to associate new user with e-mail address."); 110 scope (failure) m_usersByEmail.remove(usr.email); 111 enforce(m_usersByName.setIfNotExist(usr.name, uid), "Failed to associate new user with user name."); 112 scope (failure) m_usersByName.remove(usr.name); 113 114 // User 115 m_users[uid] = usr.redisStrip(); 116 117 // Credentials 118 m_userAuthInfo[uid] = usr.auth; 119 120 // Properties 121 auto props = m_userProperties[uid]; 122 foreach (string name, value; usr.properties) 123 props[name] = value.toString(); 124 125 // Group membership 126 foreach(string gid; usr.groups) 127 addGroupMember(gid, User.ID(uid)); 128 129 return usr.id; 130 } 131 132 override User getUser(User.ID id) 133 { 134 auto susr = m_users[id.longValue]; 135 enforce(susr.exists, "The specified user id is invalid."); 136 137 // Group membership 138 // TODO: avoid going over all (potentially large number of) groups 139 string[] groups; 140 foreach (gid, grp; m_groups) 141 if (m_redisDB.sisMember("userman:group:" ~ gid.to!string ~ ":members", id.toString())) 142 groups ~= grp.id; 143 144 // Credentials 145 auto auth = m_userAuthInfo[id.longValue]; 146 147 // Properties 148 Json[string] properties; 149 foreach(string name, string value; m_userProperties[id.longValue]) 150 properties[name] = parseJsonString(value); 151 152 return susr.unstrip(id, groups, auth, properties); 153 } 154 155 override User getUserByName(string name) 156 { 157 name = name.toLower(); 158 159 User.ID userId = m_usersByName.get(name, -1); 160 try return getUser(userId); 161 catch (Exception e) { 162 throw new Exception("The specified user name is not registered."); 163 } 164 } 165 166 override User getUserByEmail(string email) 167 { 168 email = email.toLower(); 169 170 User.ID uid = m_usersByEmail.get(email, -1); 171 try return getUser(uid); 172 catch (Exception e) { 173 throw new Exception("There is no user account for the specified email address."); 174 } 175 } 176 177 override User getUserByEmailOrName(string email_or_name) 178 { 179 long uid = m_usersByEmail.get(email_or_name, -1); 180 if (uid < 0) uid = m_usersByName.get(email_or_name, -1); 181 182 try return getUser(User.ID(uid)); 183 catch (Exception e) { 184 throw new Exception("The specified email address or user name is not registered."); 185 } 186 } 187 188 alias enumerateUsers = UserManController.enumerateUsers; 189 override void enumerateUsers(long first_user, long max_count, scope void delegate(ref User usr) @safe del) 190 { 191 foreach (userId; m_redisDB.zrange!string("userman:user:all", first_user, first_user + max_count)) { 192 auto usr = getUser(User.ID(userId.to!long)); 193 del(usr); 194 } 195 } 196 197 override long getUserCount() 198 { 199 return m_redisDB.zcard("userman:user:all"); 200 } 201 202 override void deleteUser(User.ID user_id) 203 { 204 User usr = getUser(user_id); 205 206 // Indexes 207 m_users.remove(user_id.longValue); 208 m_usersByEmail.remove(usr.email); 209 m_usersByName.remove(usr.name); 210 211 // Credentials 212 m_userAuthInfo.remove(user_id.longValue); 213 214 // Properties 215 m_userProperties[user_id.longValue].value.remove(); 216 217 // Group membership 218 foreach(string gid; usr.groups) 219 removeGroupMember(gid, user_id); 220 } 221 222 override void updateUser(in ref User user) 223 { 224 enforce(m_users.isMember(user.id.longValue), "Invalid user ID."); 225 validateUser(user); 226 enforce(m_settings.useUserNames || user.name == user.email, "User name must equal email address if user names are not used."); 227 228 auto exeid = m_usersByEmail.get(user.email, -1); 229 enforce(exeid < 0 || exeid == user.id.longValue, 230 "E-mail address is already in use."); 231 enforce(exeid == user.id.longValue || m_usersByEmail.setIfNotExist(user.email, user.id.longValue), 232 "Failed to associate new e-mail address to user."); 233 scope (failure) m_usersByEmail.remove(user.email); 234 235 auto exnid = m_usersByName.get(user.name, -1); 236 enforce(exnid < 0 || exnid == user.id.longValue, 237 "User name address is already in use."); 238 enforce(exnid == user.id.longValue || m_usersByName.setIfNotExist(user.name, user.id.longValue), 239 "Failed to associate new user name to user."); 240 scope (failure) m_usersByEmail.remove(user.name); 241 242 243 // User 244 m_users[user.id.longValue] = user.redisStrip(); 245 246 // Credentials 247 m_userAuthInfo[user.id.longValue] = user.auth; 248 249 // Properties 250 auto props = m_userProperties[user.id.longValue]; 251 props.value.remove(); 252 foreach (string name, value; user.properties) 253 props[name] = value.toString(); 254 255 // Group membership 256 foreach (gid, grp; m_groups) { 257 if (user.isInGroup(grp.id)) 258 addGroupMember(grp.id, user.id); 259 else 260 removeGroupMember(grp.id, user.id); 261 } 262 } 263 264 override void setEmail(User.ID user, string email) 265 { 266 validateEmail(email); 267 enforce(m_users.isMember(user.longValue), "Invalid user ID."); 268 269 auto exid = m_usersByEmail.get(email, -1); 270 enforce(exid < 0 || exid == user.longValue, 271 "E-mail address is already in use."); 272 enforce(exid == user.longValue || m_usersByEmail.setIfNotExist(email, user.longValue), 273 "Failed to associate new e-mail address to user."); 274 275 m_users[user.longValue].email = email; 276 } 277 278 override void setFullName(User.ID user, string full_name) 279 { 280 enforce(m_users.isMember(user.longValue), "Invalid user ID."); 281 m_users[user.longValue].fullName = full_name; 282 } 283 284 override void setPassword(User.ID user, string password) 285 { 286 enforce(m_users.isMember(user.longValue), "Invalid user ID."); 287 288 AuthInfo auth = m_userAuthInfo[user.longValue]; 289 auth.method = "password"; 290 auth.passwordHash = generatePasswordHash(password); 291 m_userAuthInfo[user.longValue] = auth; 292 } 293 294 override void setProperty(User.ID user, string name, Json value) 295 { 296 enforce(m_users.isMember(user.longValue), "Invalid user ID."); 297 298 m_userProperties[user.longValue][name] = value.toString(); 299 } 300 301 override void removeProperty(User.ID user, string name) 302 { 303 enforce(m_users.isMember(user.longValue), "Invalid user ID."); 304 305 m_userProperties[user.longValue].remove(name); 306 } 307 308 override void addGroup(string id, string description) 309 { 310 enforce(isValidGroupID(id), "Invalid group ID."); 311 312 // TODO: avoid iterating over all groups! 313 foreach (gid, grp; m_groups) { 314 enforce(grp.id != grp.id, "A group with this name already exists."); 315 } 316 317 // Add Group 318 long groupId = m_groups.createID(); 319 Group grp = { 320 id: id, 321 description: description 322 }; 323 324 m_groups[groupId] = grp.redisStrip!false(); 325 foreach (k, v; grp.properties) 326 m_groupProperties[groupId][k] = v.toString(); 327 328 m_groupsByName[id] = groupId; 329 } 330 331 override void removeGroup(string id) 332 { 333 auto gid = m_groupsByName[id]; 334 m_groups.remove(gid); 335 m_groupsByName.remove(id); 336 } 337 338 override void setGroupDescription(string name, string description) 339 { 340 auto gid = m_groupsByName[name]; 341 m_groups[gid].description = description; 342 } 343 344 override long getGroupCount() 345 { 346 return m_redisDB.zcard("userman:group:all"); 347 } 348 349 override Group getGroup(string name) 350 { 351 auto grpid = m_groupsByName.get(name, -1); 352 enforce(grpid != -1, "The specified group name is unknown."); 353 354 auto sgrp = m_groups[grpid]; 355 enforce(sgrp.exists, "The specified group id is invalid."); 356 357 // Properties 358 Json[string] properties; 359 foreach(string name, string value; m_groupProperties[grpid]) 360 properties[name] = parseJsonString(value); 361 362 return sgrp.unstrip(properties); 363 } 364 365 alias enumerateGroups = UserManController.enumerateGroups; 366 override void enumerateGroups(long first_group, long max_count, scope void delegate(ref Group grp) @safe del) 367 { 368 foreach (id; m_redisDB.zrange!string("userman:group:all", first_group, first_group + max_count)) { 369 auto grp = getGroup(id); 370 del(grp); 371 } 372 } 373 374 override void addGroupMember(string group, User.ID user) 375 { 376 auto grpid = m_groupsByName.get(group, -1); 377 enforce(grpid != -1, "The specified group name is unknown."); 378 m_redisDB.sadd("userman:group:" ~ grpid.to!string ~ ":members", user.toString()); 379 } 380 381 override void removeGroupMember(string group, User.ID user) 382 { 383 auto grpid = m_groupsByName.get(group, -1); 384 enforce(grpid != -1, "The specified group name is unknown."); 385 m_redisDB.srem("userman:group:" ~ grpid.to!string ~ ":members", user.toString()); 386 } 387 388 override long getGroupMemberCount(string group) 389 { 390 assert(false); 391 } 392 393 alias enumerateGroupMembers = UserManController.enumerateGroupMembers; 394 override void enumerateGroupMembers(string group, long first_member, long max_count, scope void delegate(User.ID usr) @safe del) 395 { 396 assert(false); 397 } 398 }