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 }