-
Notifications
You must be signed in to change notification settings - Fork 304
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
HPCC-31761 LDAP only uses first cached default file scope permission #18651
Conversation
Jira Issue: https://hpccsystems.atlassian.net//browse/HPCC-31761 Jirabot Action Result: |
@ghalliday @jakesmith preliminary review. Will add a flag to operate as it did. Also need to add read and write locks for the default file scope permission cache. Both will be in the next commit. |
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.
@kenrowland - looks good in principle.
But it needs to be made thread safe, afaics as it stands, 1 thread could be clearing or altering m_userDefaultFileScopePermissions, whilst another read from it, which would lead to a crash.
I think if the clear() goes in removeAllManagedFileScopes (with it's WriteLockBlock lock) , and the query is protected by a ReadLockBlock on m_scopesRWLock, it should make it thread safe.
It looks like m_managedFileScopesMap can change whilst it is being processed by another thread
system/security/shared/caching.cpp
Outdated
m_defaultPermission = SecAccess_None; | ||
defaultPermission = m_secMgr->queryDefaultPermission(user); | ||
std::string userName(username); | ||
m_userDefaultFileScopePermissions.insert(std::pair<std::string, SecAccessFlags>(userName, defaultPermission)); |
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.
trivial: more concisely expressed (and more efficient) as:
m_userDefaultFileScopePermissions.emplace(username, defaultPermission);
NB: no need for explicit std::string conversion.
@jakesmith Don't know if you saw my previous comment, but locks were something I am adding. |
No, sorry, I missed it. |
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.
@kenrowland - please see comments.
@@ -664,6 +664,7 @@ class CLdapConfig : implements ILdapConfig, public CInterface | |||
m_sdfieldname.append("aci"); | |||
else if(m_serverType == OPEN_LDAP) | |||
m_sdfieldname.append("aci"); | |||
|
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.
trivial: unintended new newline (?) - nice to not alter this file for git history if so.
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.
Hmm, not sure why, but removed it.
system/security/shared/caching.cpp
Outdated
defaultScopesReadLock.clear(); | ||
WriteLockBlock defaultScopesWriteLock(m_defaultScopesRWLock); | ||
defaultPermission = m_secMgr->queryDefaultPermission(user); | ||
std::string userName(username); |
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.
at least in my setup, this line is not necessary, i.e. you can pass const char * to the emplace and it will implicitly construct a std::string
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.
The find, in my experience, has to construct an object of the key type. So, I left the username std::string variable, but cleaned it up a bit.
system/security/shared/caching.cpp
Outdated
@@ -711,8 +741,16 @@ void CPermissionsCache::flush() | |||
delete (*ui).second; | |||
m_userCache.clear(); | |||
} | |||
if (m_useLegacyDefaultFileScopePermissionCache) | |||
{ | |||
m_defaultPermission = SecAccess_None; |
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.
should this be :
m_defaultPermission = SecAccess_Unknown
.. to trigger the next call to queryDefaultPermission to get new permissions?
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.
Yes, good catch.
@@ -664,6 +664,7 @@ class CLdapConfig : implements ILdapConfig, public CInterface | |||
m_sdfieldname.append("aci"); | |||
else if(m_serverType == OPEN_LDAP) | |||
m_sdfieldname.append("aci"); | |||
|
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.
Hmm, not sure why, but removed it.
system/security/shared/caching.cpp
Outdated
defaultScopesReadLock.clear(); | ||
WriteLockBlock defaultScopesWriteLock(m_defaultScopesRWLock); | ||
defaultPermission = m_secMgr->queryDefaultPermission(user); | ||
std::string userName(username); |
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.
The find, in my experience, has to construct an object of the key type. So, I left the username std::string variable, but cleaned it up a bit.
system/security/shared/caching.cpp
Outdated
@@ -711,8 +741,16 @@ void CPermissionsCache::flush() | |||
delete (*ui).second; | |||
m_userCache.clear(); | |||
} | |||
if (m_useLegacyDefaultFileScopePermissionCache) | |||
{ | |||
m_defaultPermission = SecAccess_None; |
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.
Yes, good catch.
system/security/shared/caching.cpp
Outdated
if (m_secMgr) | ||
m_defaultPermission = m_secMgr->queryDefaultPermission(user); | ||
else | ||
m_defaultPermission = SecAccess_None; |
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.
I changed this to SecAccess_Full. If no security manager then full access. I'm pretty sure the code essentially prevents this from being called if there is no security manager, so it may be moot, but correct none the less.
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.
@kenrowland - looks good. Please squash for final scan.
@jakesmith squashed as requested. You mentioned final scan, so I re-requested review. |
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.
@kenrowland Looks good.
@ghalliday - should this go into 9.6 ?
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.
@kenrowland a couple of questions/comments. No need to change to a critical section since tested as-is.
system/security/shared/caching.cpp
Outdated
if (m_secMgr) | ||
{ | ||
const std::string username(user.getName()); | ||
ReadLockBlock defaultScopesReadLock(m_defaultScopesRWLock); |
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.
I would have used a simpler critical section.
- The R/W lock complicates this code (relock and potential for deadlock you have avoided).
- This is unlikely to be heavily contended.
- R/W lock is less efficient than a a critical section.
- If it contended and needs a write then the other threads will need to wait anyway.
The R/W blocks are good for situations where parallel reads can continue and not have the write block them. (Even then it is only likely to be helpful when slow and heavily contended.)
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.
From what I have seen, it appears that default file scopes are used a lot. Making this code as efficient as possible is probably the right thing to do. I agree that lock for read, release, lock for write and check again, is not optimal. Since you mentioned a critical section, I think the time to change it is now. I will make that change in the interest of keeping default scope checking efficient.
system/security/shared/caching.cpp
Outdated
if (m_secMgr) | ||
m_defaultPermission = m_secMgr->queryDefaultPermission(user); | ||
else | ||
m_defaultPermission = SecAccess_Full; |
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.
This is different from the code in 9.2.x/9.4.x. Is this deliberate? I suspect it is, but could do with an indication why.
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.
A previous outdated comment indicates why it is set to Full. If there is no security manager, access should be full for all users and all scopes. However, technically as currently implemented, this code will never be called if there is no security manager. However, must still handle the case. I will add short comment on that line.
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.
The case where the legacy cache is not used still returns SecAccess_None. It be clearer to test for m_secMgr at the start of the function and return SecAccess_Full.
else | ||
m_defaultPermission = SecAccess_Full; | ||
|
||
DBGLOG("Legacy default file scope permission set to %s(%d) for all users, based on User '%s'", getSecAccessFlagName(m_defaultPermission), |
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.
This looks like this tracing could be excessive.
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.
That line will appear once whenever the cache is reset and when using the legacy code that has the error. I think it is important to leave in if the legacy option is chosen, then you can search back in the logs, if needed, to see what default scope permission is in effect.
system/security/shared/caching.cpp
Outdated
{ | ||
defaultPermission = m_secMgr->queryDefaultPermission(user); | ||
m_userDefaultFileScopePermissions.emplace(username, defaultPermission); | ||
DBGLOG("Added user '%s' to default file scope permissions with access %s(%d)", username.c_str(), getSecAccessFlagName(defaultPermission), |
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.
Should this logging be protected with a flag e.g. traceLDAP?
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.
As we move towards more secure permissions and removal of Authenticated users from default file scope permissions, I think this information is useful in helping lock down a cluster. In fact, I debated making it a PROGLOG so that the information on default scopes assigned to users is available. If you think a flag is more appropriate, I can add one. I think now is the time if we are going to do it.
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.
This will be logged for the first lookup for each user every time the cache is cleared. Given the potential cost of logging that might add up. Keep as is for the moment, but review how much logging it is generating once this is being used on a live system.
(Simple support for trace flags is not yet available in esp, when it is it will be much simpler to add a guard.)
@jakesmith @ghalliday I converted the synchronization protection for the default scope permission map to use a mutex. This was based on the complexity of having to release the read lock, acquire the write lock, and recheck for a user's permission. A critical section was suggested, but the map needed to be protected when cleared after a cache timeout and when flushed. |
curious, can you elaborate, why did this preclude using a CriticalSection ? |
The semantics of a critical section is to protect a section of code from being reentered. The scope permission map needed to be protected in three different places, so a critical section did not seem appropriate. Instead a mutex protecting access to the data structure seemed appropriate. |
That is probably the original use for critical sections, but they provide very similar functionality to mutxes. I previously thought that the only difference was that Mutexes supported named inter-process mutexs. However it was a great question - because it made me look at the current implementation of mutexes. They appear to be using condition variables in addition to the mutex. The code is ancient, but I am wondering why they are implemented like that.
and
i.e. they perform terribly. They perform even worse as amount of work performed in the mutex increases. I will open a jira to switch Mutexes to use the critical section code! |
@kenrowland having taken a closer look at the mutex implementation/performance this code should be switched to use a CriticalSection instead of a Mutex. (I agree we should really use Mutex in many places we use CriticalSection.) Longer term we will rewrite the Mutex class so it is efficient. |
@ghalliday Switched to use a critical section |
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.
@kenrowland - 1 question, but looks fine.
@@ -549,7 +549,8 @@ inline void CPermissionsCache::removeAllManagedFileScopes() | |||
|
|||
etc. Until full scope path checked, or no read permissions hit on ancestor scope. | |||
*/ | |||
static CriticalSection msCacheSyncCS;//for managed scopes cache syncronization | |||
static CriticalSection msCacheSyncCS;//for managed scopes cache synchronization | |||
static CriticalSection syncDefaultScopePermissions;//for cached default file scope permissions |
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.
curios why this (and msCacheSyncCS) aren't members of CPermissionCache? (
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.
The protection for managed scopes was implemented that way, so I kept the same paradigm. I'd have to look deeper, but perhaps it is related to the option to share the cache across multiple instances of the security manager (as is the case with an ESP with multiple services loaded).
@ghalliday Any final comments? |
@ghalliday please merge if no further comments |
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.
@kenrowland I'm not sure. I think best to merge as is, and have a follow on PR that moves the code out of the CriticalSection
|
||
SecAccessFlags defaultPermission = SecAccess_None; | ||
CriticalBlock defaultScopePermissionBlock(syncDefaultScopePermissions); | ||
const std::string username(user.getName()); |
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.
minor: This code doesn't need to be inside the critical section. Minimising the code with a mutex helps reduce contention. I will merge as-is.
system/security/shared/caching.cpp
Outdated
{ | ||
defaultPermission = m_secMgr->queryDefaultPermission(user); | ||
m_userDefaultFileScopePermissions.emplace(username, defaultPermission); | ||
DBGLOG("Added user '%s' to default file scope permissions with access %s(%d)", username.c_str(), getSecAccessFlagName(defaultPermission), |
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.
It is best to avoid any logging inside a critical section - because it could be blocked on io. Better to set a flag and trace once the critical section is left.
@kenrowland I have created a Jira to further clean up the code. |
First, do you want the logging code moved as part of a group of PRs to be merged at once? Second, IMO, this fix should target both 9.6 and 9.8, both with the option to use the old code disabled. It fixes security holes. Plus any dependence on the older "way" is not predictable. I sent a separate email with information about how we can handle the fix. |
@ghalliday request for merging may have gotten lost in the recent comments. Once merged, making the changes for the follow up Jira will be much easier. |
ok. @kenrowland please can you retarget this to 9.6.x with the option defaulting to legacy, and then when that is merged create a separate pr enabling it targetting master. |
@ghalliday Retargeted as requested |
… uses it for future requests Added a per user default file scope permission cache Signed-Off-By: Kenneth Rowland [email protected]
Added a per user default file scope permission cache
Signed-Off-By: Kenneth Rowland [email protected]
Type of change:
Checklist:
Smoketest:
Testing: