Skip to content
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

rework the user ldap logic to match the account table #125

Merged
merged 29 commits into from
Oct 25, 2017
Merged

Conversation

butonic
Copy link
Member

@butonic butonic commented Aug 17, 2017

I am sick of our ldap problems. The app is heavily used, yet undertested and quirky at best. It grew over time but its purpose changes with the introduction of the Account table in OC10.

  • getUsers() fetches and caches all necessary ldap attributes in a single paginated ldap search
    • no subsequent readAttribute that aborts the running paged search ...
    • most of the user backend interface calls only work correctly after a getUsers call. That is fine for now, because that is the way the sync works. checkPassword also correctly caches the attributes of the logged in user
    • only caches the UserEntries (which wrap ldap results) in the current process. no longer uses the distributed cache for users ... wasted effort on sync and for user search the account table is used.
  • groups code is unchanged for now.
    • could use some refactoring

requires owncloud/core#28729 to make the user sync correctly update quota and email

  • move some functions from Access to a helper class
  • review commented tests, some are already covered by UserEntryTest, others are better located in the Manager
  • add more tests
  • add Class structure / chain to php doc
  • test with user shibboleth
  • test users as group members if getUsers did not yet fetch the entry ... might there be a problem when groups have > 512 Users (the default size of the capped cache?)

yes, I am ripping this apart and putting it together anew. I don't care about the ui. I need the underlying sync logic to work reliably AND the code to be readable. That means not trying to be smart and use less reflection ...

@butonic butonic changed the title rework the user ldap logic to match the account table 🚧 rework the user ldap logic to match the account table Aug 17, 2017
@butonic butonic self-assigned this Aug 17, 2017
@butonic butonic added this to the development milestone Aug 17, 2017
@butonic
Copy link
Member Author

butonic commented Aug 18, 2017

fixes #121
obsoletes #119

@butonic
Copy link
Member Author

butonic commented Sep 1, 2017

  • fix and test GUID binary mapping

@mrow4a
Copy link
Contributor

mrow4a commented Sep 2, 2017

To me, it is cleaner version of the code + adjustment to accounts thing. Looks good in general, just difficult to review 👍

@tomneedham
Copy link
Contributor

Tested with master:

  • user sync
  • extended search terms
  • multi-value search terms
  • mail attribute sync
  • group sync
  • ui basic setup

* creates a unique name for internal ownCloud use.
* @param string $name the display name of the object
* @param boolean $isUser whether name should be created for a user (true) or a group (false)
* @return string|false with with the name to use in ownCloud or false if unsuccessful
*/
private function createAltInternalOwnCloudName($name, $isUser) {
public function createAltInternalOwnCloudName($name, $isUser) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some more context here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll also need context here.

If this needs to be public I wonder if this function should be here. It would make more sense to move this function to a specialized object.
Although we should move this function to another place, I think it's fine if we can make it private for now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate PR, see lib\User\Manager.php:292:

// FIXME move to a better place, naming related. eg DistinguishedNameUtils

@butonic butonic changed the title 🚧 rework the user ldap logic to match the account table rework the user ldap logic to match the account table Sep 18, 2017
Copy link
Member

@jvillafanez jvillafanez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still need to review the big diffs

@@ -82,7 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$uid = $input->getArgument('ocName');
$this->isAllowed($input->getOption('force'));
$this->confirmUserIsMapped($uid);
$exists = $this->backend->userExistsOnLDAP($uid);
$exists = $this->backend->userExists($uid);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reason for this change? I think originally this was intended to bypass the cache and check directly against the LDAP server.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation now always talks to LDAP but caches the result per request.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually userExistsOnLdap has been moved to User_LDAP. userExists tries to determine the answer based on the cache in the LDAP UserManager. It will fall back to LDAP just as before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so the redis cache has been removed and we cache the information per request... fair enough 👍

$username = $this->getAttributeValue($attr, null);
}
if ($username === null) {
$json = @json_encode($this->ldapEntry, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_NUMERIC_CHECK | JSON_PARTIAL_OUTPUT_ON_ERROR );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather have a comment here for the @ since error supression isn't common.

/**
* @param string $ownCloudUID
*/
public function setOwnCloudUID($ownCloudUID) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried this is the only "set" method here. Is it expected to be changed? Could it be set during the object creation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to store an already mapped uid somewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is if we can set the value only in the constructor in order to prevent modifications, or we need to modify the value once the object is already constructor.

}
}
if ($quota === null) {
$quota = 'default';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this changed recently to not set the "default" quota in order to avoid being overwritten

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix incoming

}

/**
* @return string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd comment here what is the expected behavior. I don't expect this method to return the username unless explicitly told.
My expectation is that this method returns null if the displayname can't be fetched, so anyone can fetch the username later if they want. Better to add a comment if this isn't the case

/**
* returns the home directory of the user if specified by LDAP settings
* @return string|null
* @throws \Exception
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment here when this exception is expected to be thrown.
In addition, we should narrow the exception.

if (isset($this->ldapEntry['memberof'])) {
return $this->ldapEntry['memberof'];
}
return [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather return something different because empty result is also a valid result, it would be difficult to distinguish this case and an error case (or if there isn't such entry)

*/
private function getAttributeValue($attributeName, $default = null, $trim = true) {
$attributeName = strtolower($attributeName); // all ldap keys are lowercase
if (isset($this->ldapEntry[$attributeName][0])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check for multivalued attributes? We should have a comment just in case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no. only search attributes may be multi value. updating phpdoc as well

* @param $attributeName string
* @param $default string
* @param $trim bool don't trim value, eg for binary data
* @return string|null|mixed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify when we should expect each data type.

use OCP\UserInterface;

class User_Proxy extends Proxy implements IUserBackend, UserInterface, IProvidesExtendedSearchBackend {
class User_Proxy extends Proxy implements IUserBackend, UserInterface, IProvidesQuotaBackend, IProvidesExtendedSearchBackend, IProvidesEMailBackend {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm forced to complain about the massive interface implementation here, but no change needed for now because it will require quite huge changes, and not just here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app used to update core itself, which is not the way it shoud be. This PR just corrects that by removing the related code and implementing the interfaces to get the correct behavior.

@@ -25,6 +26,9 @@
namespace OCA\User_LDAP;

abstract class LDAPUtility {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should review the usage if this class because right now I think it's quite pointless. Probably we can remove it.

Copy link
Member Author

@butonic butonic Oct 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate PR

lib/Access.php Outdated
* returns the Connection instance
* @return \OCA\User_LDAP\ILDAPWrapper
*/
public function getLDAP() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function used outside of the class? I'm fine to use this function as a shortcut but also private. Making it public would open new dependencies between components which doesn't seem a good idea.

$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);

return strtoupper($hex_guid_to_guid_str);
public static function binGUID2str($binGuid) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to use static methods here? I'd rather move this function to a helper object and inject it in the constructor. This way we have this function outside if this class (I don't think this is needed to be here), and also make the function testeable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate PR

@@ -1604,7 +1679,7 @@ public function getSID($dn) {
* @param string $sid
* @return string
*/
public function convertSID2Str($sid) {
public static function sid2str($sid) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separate PR

* The nsuniqueid values are generated based on the entryuuid value by moving the "-" to comply with the format of the ODSEE Nsuniqueid Virtual Attribute attribute.
*
* ## RedHat FreeIPA is defined as utf string
* {@link https://github.com/freeipa/freeipa/blob/master/install/share/uuid.ldif ipaUniqueID schema}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏
Really greatful for this references. Just this one should be an official redhat link. No need to do this now anyway.

*
* @package OCA\User_LDAP
*/
class User_LDAP implements IUserBackend, UserInterface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this class should implement these interfaces. It should be oriented to implement them so the User_Proxy class is easier to implement but other than that, the interfaces will be implemented by the User_Proxy.

$user = $this->access->userManager->get($uid);
if(!$user instanceof User) {
$user = $this->userManager->getByOwnCloudUID($uid);
if(!$user instanceof UserEntry) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the PHPDocs, the method returns a UserEntry or null. I'd change this to check against null because otherwise it seems that other type of objects can be returned by that function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getByOwnCloudUID now always returns a UserEntry

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or throws an exception

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so we can remove that check (or use a try-catch for the exception instead)

* Get a users email address
*
* @param string $uid The username
* @return string|null|false false if user was not found, null if no email is set
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy & paste 👀

* FIXME This is an expensive operation and takes roughly half a second to parse the data and create the image. This might be too slow for sync jobs.
*
* @param string $uid The username
* @return IImage|null|false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to specify each possible cases

/**
* Get avatar for a users account for core powered user search
*
* FIXME This is an expensive operation and takes roughly half a second to parse the data and create the image. This might be too slow for sync jobs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side question for the future: is it possible to store the raw image data (I think it's base64 encoded) and compare the data we have and the data we receive, and then create the image if the data is different? I'm not sure if it will improve the performance, but it might be worthy to have a try.

@jvillafanez
Copy link
Member

Review finished from my part. I think most of them are easy to solve, and the rest might be moved to tech debt if they're too complex to manage in this PR.

@tomneedham
Copy link
Contributor

bug here with user sync, suggested arribute is not found when it is present: https://github.com/owncloud/enterprise/issues/2275

@tomneedham
Copy link
Contributor

Rebased to include some other fixes and the tests should now run

@tomneedham
Copy link
Contributor

screen shot 2017-10-11 at 14 25 56

screen shot 2017-10-11 at 14 24 25

💥 after running a user sync twice

@tomneedham
Copy link
Contributor

"0" quota is recorded in the accounts table as NULL => correct or not?

if($userEntry === null) {
try {
$this->userManager->getCachedEntry($uid);
} catch (\Exception $e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@tomneedham
Copy link
Contributor

This mornings commit fixes the sync issue I found.

@tomneedham
Copy link
Contributor

Rebased + pushed a test. Will wait for CI. Then build test tarball which we can share round as a beta release 🎉

@tomneedham
Copy link
Contributor

Ideas for performance comparisons for release:

  • cold login time
  • user sync + single user sync
  • call for all users

@butonic
Copy link
Member Author

butonic commented Oct 25, 2017

Merging, next up testing with large installations before release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants