-
Notifications
You must be signed in to change notification settings - Fork 551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve ACL SETUSER Thread Handling #989
base: main
Are you sure you want to change the base?
Changes from all commits
65fabeb
02b38ee
6f330f8
21b69e0
7a4a76d
dc08dcf
2872177
b9ad4b2
d5629a7
4ca6db5
20f8a7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -18,16 +18,31 @@ public class AccessControlList | |||||||||||
/// </summary> | ||||||||||||
const string DefaultUserName = "default"; | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Arbitrary Key for new user lock object. | ||||||||||||
/// </summary> | ||||||||||||
const string NewUserLockObjectKey = "441a61e2-4d4e-498e-8ca0-715cf550e5be"; | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Dictionary containing all users defined in the ACL | ||||||||||||
/// </summary> | ||||||||||||
ConcurrentDictionary<string, User> _users = new(); | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Dictionary containing stable lock objects for each user in the ACL. | ||||||||||||
/// </summary> | ||||||||||||
ConcurrentDictionary<string, object> _userLockObjects = new(); | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't quite get why we're introducing lock objects - we can just lock on the For new users things could be phrased in terms of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I considered using the user as the lock object, however we are replacing the user object in the access control list's dictionary when ACL SETUSER is called. This makes it a bad candidate for a lock object because we would introduce additional threading issues (different threads would lock on different objects for the user). We need a stable object for each thread to obtain the user level lock on. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair. The dance with ConcurrentDictionaries isn't quite right though, which makes me think there might be a simpler approach that does enable User as a lock. |
||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// The currently configured default user (for fast default lookups) | ||||||||||||
/// </summary> | ||||||||||||
User _defaultUser; | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// The <see cref="RespServerSession"/>s that will receive access control list change notifications. | ||||||||||||
/// </summary> | ||||||||||||
private readonly ConcurrentDictionary<string, RespServerSession> _subscribedSessions = new(); | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: it's not clear to me why this is keyed by a string? You could just use the |
||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Creates a new Access Control List from an optional ACL configuration file | ||||||||||||
/// and sets the default user's password, if not provided by the configuration. | ||||||||||||
|
@@ -47,6 +62,7 @@ public AccessControlList(string defaultPassword = "", string aclConfigurationFil | |||||||||||
// If no ACL file is defined, only create the default user | ||||||||||||
_defaultUser = CreateDefaultUser(defaultPassword); | ||||||||||||
} | ||||||||||||
_userLockObjects[NewUserLockObjectKey] = new object(); | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
|
@@ -63,6 +79,33 @@ public User GetUser(string username) | |||||||||||
return null; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Returns the lock object for the user with the given name. This allows user level locks, which should only be | ||||||||||||
/// used for rare cases where modifications must be made to a user object, most notably ACL SETUSER. | ||||||||||||
/// | ||||||||||||
/// If modifications to a user are necessary the following pattern is suggested: | ||||||||||||
/// | ||||||||||||
/// 1. Obtain the lock object for the user using this method. | ||||||||||||
/// 2. Immediately take a lock on the object. | ||||||||||||
/// 3. Read the user from the <see cref="AccessControlList"/> and make a copy with the copy constructor. | ||||||||||||
/// 4. Modify the copy of the user object. | ||||||||||||
/// 5. Replace the user in the <see cref="AccessControlList"/> using the AddOrReplace(User user) method. | ||||||||||||
/// | ||||||||||||
/// Note: This pattern will make the critical section under lock single threaded across all sessions, use very | ||||||||||||
/// sparingly. | ||||||||||||
/// </summary> | ||||||||||||
/// <param name="username">Username of the user to retrieve.</param> | ||||||||||||
/// <returns>Matching user lock object.</returns> | ||||||||||||
public object GetUserLockObject(string username) | ||||||||||||
{ | ||||||||||||
if (_userLockObjects.TryGetValue(username, out var userLockObject)) | ||||||||||||
{ | ||||||||||||
return userLockObject; | ||||||||||||
} | ||||||||||||
|
||||||||||||
return _userLockObjects[NewUserLockObjectKey]; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Returns the currently configured default user. | ||||||||||||
/// </summary> | ||||||||||||
|
@@ -73,17 +116,15 @@ public User GetDefaultUser() | |||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Adds the given user to the ACL. | ||||||||||||
/// Adds or replaces the given user in the ACL. | ||||||||||||
/// </summary> | ||||||||||||
/// <param name="user">User to add to the list.</param> | ||||||||||||
/// <exception cref="ACLUserAlreadyExistsException">Thrown if a user with the given username already exists.</exception> | ||||||||||||
public void AddUser(User user) | ||||||||||||
/// <param name="user">User to add or replaces in the list.</param> | ||||||||||||
public void AddOrReplaceUser(User user) | ||||||||||||
{ | ||||||||||||
// If a user with the given name already exists in the ACL, the new user cannot be added | ||||||||||||
if (!_users.TryAdd(user.Name, user)) | ||||||||||||
{ | ||||||||||||
throw new ACLUserAlreadyExistsException(user.Name); | ||||||||||||
} | ||||||||||||
// If a user with the given name already exists replace the user, otherwise add the new user. | ||||||||||||
_users[user.Name] = user; | ||||||||||||
_ = _userLockObjects.TryAdd(user.Name, new object()); | ||||||||||||
this.NotifySubscribers(user); | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
|
@@ -98,7 +139,15 @@ public bool DeleteUser(string username) | |||||||||||
{ | ||||||||||||
throw new ACLException("The special 'default' user cannot be removed from the system"); | ||||||||||||
} | ||||||||||||
return _users.TryRemove(username, out _); | ||||||||||||
|
||||||||||||
bool userDeleted = _users.TryRemove(username, out _); | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a race here. If Session A deletes a user and Session B adds the user it is possible for this to happen:
At the end |
||||||||||||
|
||||||||||||
if (userDeleted) | ||||||||||||
{ | ||||||||||||
_userLockObjects.TryRemove(username, out _); | ||||||||||||
} | ||||||||||||
|
||||||||||||
return userDeleted; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
|
@@ -150,7 +199,7 @@ User CreateDefaultUser(string defaultPassword = "") | |||||||||||
// Add the user to the user list | ||||||||||||
try | ||||||||||||
{ | ||||||||||||
AddUser(defaultUser); | ||||||||||||
AddOrReplaceUser(defaultUser); | ||||||||||||
break; | ||||||||||||
} | ||||||||||||
catch (ACLUserAlreadyExistsException) | ||||||||||||
|
@@ -282,5 +331,36 @@ void Import(StreamReader input, string configurationFile = "<undefined>") | |||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Registers a <see cref="RespServerSession"/> to receive notifications when modifications are performed to the <see cref="AccessControlList"/>. | ||||||||||||
/// </summary> | ||||||||||||
/// <param name="respSession">The <see cref="RespServerSession"/> to register.</param> | ||||||||||||
internal void Subscribe(RespServerSession respSession) | ||||||||||||
{ | ||||||||||||
_subscribedSessions[respSession.AclSubscriberKey] = respSession; | ||||||||||||
} | ||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Unregisters a <see cref="RespServerSession"/> to receive notifications when modifications are performed to the <see cref="AccessControlList"/>. | ||||||||||||
/// </summary> | ||||||||||||
/// <param name="respSession">The <see cref="RespServerSession"/> to register.</param> | ||||||||||||
internal void Unsubscribe(RespServerSession respSession) | ||||||||||||
{ | ||||||||||||
_ = _subscribedSessions.TryRemove(respSession.AclSubscriberKey, out _); | ||||||||||||
} | ||||||||||||
|
||||||||||||
|
||||||||||||
/// <summary> | ||||||||||||
/// Notify the registered <see cref="RespServerSession"/> when modifications are performed to the <see cref="AccessControlList"/>. | ||||||||||||
/// </summary> | ||||||||||||
/// <param name="user">The created or updated <see cref="User"/> that triggered the notification.</param> | ||||||||||||
private void NotifySubscribers(User user) | ||||||||||||
{ | ||||||||||||
foreach (RespServerSession respSession in _subscribedSessions.Values) | ||||||||||||
{ | ||||||||||||
respSession.NotifyAclChange(user); | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} | ||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
acl?.AddOrReplaceUser(user);