DeveelDB  20151217
complete SQL database system, primarly developed for .NET/Mono frameworks
UserManager.cs
Go to the documentation of this file.
1 //
2 // Copyright 2010-2015 Deveel
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15 //
16 
17 using System;
18 using System.Collections.Generic;
19 using System.IO;
20 using System.Linq;
21 
23 using Deveel.Data.Sql.Objects;
24 using Deveel.Data.Sql.Tables;
25 
26 namespace Deveel.Data.Security {
27  public class UserManager : IUserManager {
28  private Dictionary<string, string[]> userGroupsCache;
29 
30  public UserManager(IQuery queryContext) {
31  QueryContext = queryContext;
32  }
33 
35  Dispose(false);
36  }
37 
38  public IQuery QueryContext { get; private set; }
39 
40  public void Dispose() {
41  Dispose(true);
42  GC.SuppressFinalize(this);
43  }
44 
45  protected virtual void Dispose(bool disposing) {
46  userGroupsCache = null;
47  QueryContext = null;
48  }
49 
50  public bool UserExists(string userName) {
51  var table = QueryContext.GetTable(SystemSchema.UserTableName);
52  var c1 = table.GetResolvedColumnName(0);
53 
54  // All password where UserName = %username%
55  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, SqlExpression.Constant(userName));
56  return t.RowCount > 0;
57  }
58 
59  public void CreateUser(UserInfo userInfo, string identifier) {
60  if (userInfo == null)
61  throw new ArgumentNullException("userInfo");
62  if (String.IsNullOrEmpty(identifier))
63  throw new ArgumentNullException("identifier");
64 
65  // TODO: make these rules configurable?
66 
67  var userName = userInfo.Name;
68 
69  if (UserExists(userName))
70  throw new SecurityException(String.Format("User '{0}' is already registered.", userName));
71 
72  // Add to the key 'user' table
73  var table = QueryContext.GetMutableTable(SystemSchema.UserTableName);
74  var row = table.NewRow();
75  row[0] = DataObject.String(userName);
76  table.AddRow(row);
77 
78  var method = userInfo.Identification.Method;
79  var methodArgs = SerializeArguments(userInfo.Identification.Arguments);
80 
81  if (method != "plain")
82  throw new NotImplementedException("Only mechanism implemented right now is plain text (it sucks!)");
83 
84  table = QueryContext.GetMutableTable(SystemSchema.PasswordTableName);
85  row = table.NewRow();
86  row.SetValue(0, userName);
87  row.SetValue(1, method);
88  row.SetValue(2, methodArgs);
89  row.SetValue(3, identifier);
90  table.AddRow(row);
91  }
92 
93  private static byte[] SerializeArguments(IDictionary<string, object> args) {
94  using (var stream = new MemoryStream()) {
95  using (var writer = new BinaryWriter(stream)) {
96  writer.Write(args.Count);
97 
98  foreach (var arg in args) {
99  writer.Write(arg.Key);
100 
101  if (arg.Value is bool) {
102  writer.Write((byte)1);
103  writer.Write((bool) arg.Value);
104  } else if (arg.Value is short ||
105  arg.Value is int ||
106  arg.Value is long) {
107  var value = (long) arg.Value;
108  writer.Write((byte)2);
109  writer.Write(value);
110  } else if (arg.Value is string) {
111  writer.Write((byte)3);
112  writer.Write((string) arg.Value);
113  }
114  }
115 
116  writer.Flush();
117  return stream.ToArray();
118  }
119  }
120  }
121 
122  private static IDictionary<string, object> DeserializeArguments(byte[] bytes) {
123  using (var stream = new MemoryStream(bytes)) {
124  using (var reader = new BinaryReader(stream)) {
125  var argCount = reader.ReadInt32();
126 
127  var args = new Dictionary<string, object>(argCount);
128  for (int i = 0; i < argCount; i++) {
129  var argName = reader.ReadString();
130  var argType = reader.ReadByte();
131  object value = null;
132  if (argType == 1) {
133  value = reader.ReadBoolean();
134  } else if (argType == 2) {
135  value = reader.ReadInt64();
136  } else if (argType == 3) {
137  value = reader.ReadString();
138  }
139 
140  args[argName] = value;
141  }
142 
143  return args;
144  }
145  }
146  }
147 
148  private string[] QueryUserGroups(string userName) {
149  var table = QueryContext.GetTable(SystemSchema.UserGroupTableName);
150  var c1 = table.GetResolvedColumnName(0);
151  // All 'user_group' where UserName = %username%
152  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, SqlExpression.Constant(DataObject.String(userName)));
153  int sz = t.RowCount;
154  var groups = new string[sz];
155  var rowEnum = t.GetEnumerator();
156  int i = 0;
157  while (rowEnum.MoveNext()) {
158  groups[i] = t.GetValue(rowEnum.Current.RowId.RowNumber, 1).Value.ToString();
159  ++i;
160  }
161 
162  return groups;
163  }
164 
165  private bool TryGetUserGroupsFromCache(string userName, out string[] groups) {
166  if (userGroupsCache == null) {
167  groups = null;
168  return false;
169  }
170 
171  return userGroupsCache.TryGetValue(userName, out groups);
172  }
173 
174  private void ClearUserGroupsCache(string userName) {
175  if (userGroupsCache == null)
176  return;
177 
178  userGroupsCache.Remove(userName);
179  }
180 
181  private void ClearUserGroupsCache() {
182  if (userGroupsCache == null)
183  return;
184 
185  userGroupsCache.Clear();
186  }
187 
188  private void SetUserGroupsInCache(string userName, string[] groups) {
189  if (userGroupsCache == null)
190  userGroupsCache = new Dictionary<string, string[]>();
191 
192  userGroupsCache[userName] = groups;
193  }
194 
195 
196  public bool DropUser(string userName) {
197  var userExpr = SqlExpression.Constant(DataObject.String(userName));
198 
199  RemoveUserFromAllGroups(userName);
200 
201  // TODO: Remove all object-level privileges from the user...
202 
203  //var table = QueryContext.GetMutableTable(SystemSchema.UserConnectPrivilegesTableName);
204  //var c1 = table.GetResolvedColumnName(0);
205  //var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, userExpr);
206  //table.Delete(t);
207 
208  var table = QueryContext.GetMutableTable(SystemSchema.PasswordTableName);
209  var c1 = table.GetResolvedColumnName(0);
210  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, userExpr);
211  table.Delete(t);
212 
213  table = QueryContext.GetMutableTable(SystemSchema.UserTableName);
214  c1 = table.GetResolvedColumnName(0);
215  t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, userExpr);
216  return table.Delete(t) > 0;
217  }
218 
219  private void RemoveUserFromAllGroups(string username) {
220  var userExpr = SqlExpression.Constant(DataObject.String(username));
221 
222  var table = QueryContext.GetMutableTable(SystemSchema.UserGroupTableName);
223  var c1 = table.GetResolvedColumnName(0);
224  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, userExpr);
225 
226  if (t.RowCount > 0) {
227  table.Delete(t);
228 
229  ClearUserGroupsCache(username);
230  }
231  }
232 
233  public void AlterUser(UserInfo userInfo, string identifier) {
234  var userName = userInfo.Name;
235 
236  var userExpr = SqlExpression.Constant(DataObject.String(userName));
237 
238  // Delete the current username from the 'password' table
239  var table = QueryContext.GetMutableTable(SystemSchema.PasswordTableName);
240  var c1 = table.GetResolvedColumnName(0);
241  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, userExpr);
242  if (t.RowCount != 1)
243  throw new SecurityException(String.Format("User '{0}' was not found.", userName));
244 
245  table.Delete(t);
246 
247  // TODO: get the hash algorithm and hash ...
248 
249  var method = userInfo.Identification.Method;
250  var methodArgs = SerializeArguments(userInfo.Identification.Arguments);
251 
252  if (method != "plain")
253  throw new NotImplementedException("Only mechanism implemented right now is plain text (it sucks!)");
254 
255  // Add the new username
256  table = QueryContext.GetMutableTable(SystemSchema.PasswordTableName);
257  var row = table.NewRow();
258  row.SetValue(0, userName);
259  row.SetValue(1, method);
260  row.SetValue(2, methodArgs);
261  row.SetValue(3, identifier);
262  table.AddRow(row);
263 
264  }
265 
266  public void SetUserStatus(string userName, UserStatus status) {
267  // Internally we implement this by adding the user to the #locked group.
268  var table = QueryContext.GetMutableTable(SystemSchema.UserGroupTableName);
269  var c1 = table.GetResolvedColumnName(0);
270  var c2 = table.GetResolvedColumnName(1);
271  // All 'user_group' where UserName = %username%
272  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, SqlExpression.Constant(userName));
273  // All from this set where PrivGroupName = %group%
275 
276  bool userBelongsToLockGroup = t.RowCount > 0;
277  if (status == UserStatus.Locked &&
278  !userBelongsToLockGroup) {
279  // Lock the user by adding the user to the Lock group
280  // Add this user to the locked group.
281  var rdat = new Row(table);
282  rdat.SetValue(0, userName);
283  rdat.SetValue(1, SystemGroups.LockGroup);
284  table.AddRow(rdat);
285  } else if (status == UserStatus.Unlocked &&
286  userBelongsToLockGroup) {
287  // Unlock the user by removing the user from the Lock group
288  // Remove this user from the locked group.
289  table.Delete(t);
290  }
291  }
292 
293  public UserStatus GetUserStatus(string userName) {
294  if (IsUserInGroup(userName, SystemGroups.LockGroup))
295  return UserStatus.Locked;
296 
297  return UserStatus.Unlocked;
298  }
299 
300  public UserInfo GetUser(string userName) {
301  var table = QueryContext.GetTable(SystemSchema.PasswordTableName);
302  var unameColumn = table.GetResolvedColumnName(0);
303  var methodColumn = table.GetResolvedColumnName(1);
304  var methodArgsColumn = table.GetResolvedColumnName(2);
305 
306  var t = table.SimpleSelect(QueryContext, unameColumn, SqlExpressionType.Equal, SqlExpression.Constant(userName));
307  if (t.RowCount == 0)
308  throw new SecurityException(String.Format("User '{0}' is not registered.", userName));
309 
310  var method = t.GetValue(0, methodColumn);
311  var methodArgs = t.GetValue(0, methodArgsColumn);
312  var argBytes = ((SqlBinary) methodArgs.Value).ToByteArray();
313  var args = DeserializeArguments(argBytes);
314 
315  var identification = new UserIdentification(method);
316  foreach (var arg in args) {
317  identification.Arguments[arg.Key] = arg.Value;
318  }
319 
320  return new UserInfo(userName, identification);
321  }
322 
323  public bool CheckIdentifier(string userName, string identifier) {
324  var table = QueryContext.GetTable(SystemSchema.PasswordTableName);
325  var unameColumn = table.GetResolvedColumnName(0);
326  var idColumn = table.GetResolvedColumnName(3);
327 
328  var t = table.SimpleSelect(QueryContext, unameColumn, SqlExpressionType.Equal, SqlExpression.Constant(userName));
329  if (t.RowCount == 0)
330  throw new SecurityException(String.Format("User '{0}' is not registered.", userName));
331 
332  var stored = t.GetValue(0, idColumn);
333  return stored.Value.ToString().Equals(identifier);
334  }
335 
336 
337  public void CreateUserGroup(string groupName) {
338  if (String.IsNullOrEmpty(groupName))
339  throw new ArgumentNullException("groupName");
340 
341  var c = groupName[0];
342  if (c == '$' || c == '%' || c == '@')
343  throw new ArgumentException(String.Format("Group name '{0}' starts with an invalid character.", groupName));
344 
345  var table = QueryContext.GetMutableTable(SystemSchema.GroupsTableName);
346 
347  var row = table.NewRow();
348  row.SetValue(0, groupName);
349 
350  table.AddRow(row);
351  }
352 
353  public bool DropUserGroup(string groupName) {
354  var table = QueryContext.GetMutableTable(SystemSchema.UserGroupTableName);
355  var c1 = table.GetResolvedColumnName(0);
356 
357  // All password where name = %groupName%
358  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, SqlExpression.Constant(groupName));
359 
360  if (t.RowCount > 0) {
361  table.Delete(t);
362  ClearUserGroupsCache();
363  return true;
364  }
365 
366  return false;
367  }
368 
369  public bool UserGroupExists(string groupName) {
370  var table = QueryContext.GetTable(SystemSchema.UserGroupTableName);
371  var c1 = table.GetResolvedColumnName(0);
372 
373  // All password where name = %groupName%
374  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, SqlExpression.Constant(groupName));
375  return t.RowCount > 0;
376  }
377 
378  public void AddUserToGroup(string userName, string groupName, bool asAdmin) {
379  if (String.IsNullOrEmpty(groupName))
380  throw new ArgumentNullException("group");
381  if (String.IsNullOrEmpty(userName))
382  throw new ArgumentNullException("username");
383 
384  char c = groupName[0];
385  if (c == '@' || c == '&' || c == '#' || c == '$')
386  throw new ArgumentException(String.Format("Group name '{0}' is invalid: cannot start with {1}", groupName, c), "groupName");
387 
388  if (!IsUserInGroup(userName, groupName)) {
389  var table = QueryContext.GetMutableTable(SystemSchema.UserGroupTableName);
390  var row = table.NewRow();
391  row.SetValue(0, userName);
392  row.SetValue(1, groupName);
393  row.SetValue(2, asAdmin);
394  table.AddRow(row);
395  }
396  }
397 
398  public bool IsUserGroupAdmin(string userName, string groupName) {
399  // This is a special query that needs to access the lowest level of ITable, skipping
400  // other security controls
401  var table = QueryContext.GetTable(SystemSchema.UserGroupTableName);
402  var c1 = table.GetResolvedColumnName(0);
403  var c2 = table.GetResolvedColumnName(1);
404 
405  // All 'user_group' where UserName = %username%
406  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, SqlExpression.Constant(userName));
407 
408  // All from this set where PrivGroupName = %group%
409  t = t.SimpleSelect(QueryContext, c2, SqlExpressionType.Equal, SqlExpression.Constant(groupName));
410  if (t.RowCount == 0)
411  return false;
412 
413  return t.GetValue(0, 2).AsBoolean();
414  }
415 
416  public bool RemoveUserFromGroup(string userName, string groupName) {
417  // This is a special query that needs to access the lowest level of ITable, skipping
418  // other security controls
419  var table = QueryContext.GetMutableTable(SystemSchema.UserGroupTableName);
420  var c1 = table.GetResolvedColumnName(0);
421  var c2 = table.GetResolvedColumnName(1);
422 
423  // All 'user_group' where UserName = %username%
424  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, SqlExpression.Constant(userName));
425 
426  // All from this set where PrivGroupName = %group%
427  t = t.SimpleSelect(QueryContext, c2, SqlExpressionType.Equal, SqlExpression.Constant(groupName));
428 
429  if (t.RowCount > 0) {
430  table.Delete(t);
431 
432  ClearUserGroupsCache(userName);
433 
434  return true;
435  }
436 
437  return false;
438  }
439 
440  public bool IsUserInGroup(string userName, string groupName) {
441  string[] userGroups;
442  if (TryGetUserGroupsFromCache(userName, out userGroups) &&
443  userGroups.Any(x => String.Equals(groupName, x, StringComparison.OrdinalIgnoreCase)))
444  return true;
445 
446  // This is a special query that needs to access the lowest level of ITable, skipping
447  // other security controls
448  var table = QueryContext.GetTable(SystemSchema.UserGroupTableName);
449  var c1 = table.GetResolvedColumnName(0);
450  var c2 = table.GetResolvedColumnName(1);
451 
452  // All 'user_group' where UserName = %username%
453  var t = table.SimpleSelect(QueryContext, c1, SqlExpressionType.Equal, SqlExpression.Constant(userName));
454 
455  // All from this set where PrivGroupName = %group%
456  t = t.SimpleSelect(QueryContext, c2, SqlExpressionType.Equal, SqlExpression.Constant(groupName));
457  return t.RowCount > 0;
458  }
459 
460  public string[] GetUserGroups(string userName) {
461  string[] groups;
462  if (!TryGetUserGroupsFromCache(userName, out groups)) {
463  groups = QueryUserGroups(userName);
464  SetUserGroupsInCache(userName, groups);
465  }
466 
467  return groups;
468  }
469 
470  }
471 }
bool UserExists(string userName)
Definition: UserManager.cs:50
virtual void Dispose(bool disposing)
Definition: UserManager.cs:45
const string LockGroup
The name of the lock group.
Definition: SystemGroups.cs:28
bool UserGroupExists(string groupName)
Definition: UserManager.cs:369
UserStatus GetUserStatus(string userName)
Definition: UserManager.cs:293
UserInfo GetUser(string userName)
Definition: UserManager.cs:300
UserManager(IQuery queryContext)
Definition: UserManager.cs:30
Implements a BINARY object that handles a limited number of bytes, not exceding MaxLength.
Definition: SqlBinary.cs:27
void CreateUser(UserInfo userInfo, string identifier)
Definition: UserManager.cs:59
static readonly ObjectName UserTableName
Gets the fully qualified name of the user table.
void ClearUserGroupsCache(string userName)
Definition: UserManager.cs:174
bool IsUserGroupAdmin(string userName, string groupName)
Definition: UserManager.cs:398
bool CheckIdentifier(string userName, string identifier)
Definition: UserManager.cs:323
A single row in a table of a database.
Definition: Row.cs:44
static DataObject String(string s)
Definition: DataObject.cs:592
static byte[] SerializeArguments(IDictionary< string, object > args)
Definition: UserManager.cs:93
void AddUserToGroup(string userName, string groupName, bool asAdmin)
Definition: UserManager.cs:378
SqlExpressionType
All the possible type of SqlExpression supported
void AlterUser(UserInfo userInfo, string identifier)
Definition: UserManager.cs:233
static readonly ObjectName UserGroupTableName
string[] QueryUserGroups(string userName)
Definition: UserManager.cs:148
Represents a dynamic object that encapsulates a defined SqlType and a compatible constant ISqlObject ...
Definition: DataObject.cs:35
static readonly ObjectName GroupsTableName
bool RemoveUserFromGroup(string userName, string groupName)
Definition: UserManager.cs:416
Dictionary< string, string[]> userGroupsCache
Definition: UserManager.cs:28
Provides utilities and properties for handling the SYSTEN schema of a database.
Definition: SystemSchema.cs:37
UserIdentification Identification
Definition: UserInfo.cs:35
void SetUserGroupsInCache(string userName, string[] groups)
Definition: UserManager.cs:188
static IDictionary< string, object > DeserializeArguments(byte[] bytes)
Definition: UserManager.cs:122
void CreateUserGroup(string groupName)
Definition: UserManager.cs:337
bool IsUserInGroup(string userName, string groupName)
Definition: UserManager.cs:440
Defines the base class for instances that represent SQL expression tree nodes.
static SqlConstantExpression Constant(object value)
IDictionary< string, object > Arguments
bool DropUserGroup(string groupName)
Definition: UserManager.cs:353
string[] GetUserGroups(string userName)
Definition: UserManager.cs:460
void RemoveUserFromAllGroups(string username)
Definition: UserManager.cs:219
bool DropUser(string userName)
Definition: UserManager.cs:196
bool TryGetUserGroupsFromCache(string userName, out string[] groups)
Definition: UserManager.cs:165
static readonly ObjectName PasswordTableName
void SetUserStatus(string userName, UserStatus status)
Definition: UserManager.cs:266