I am currently using the Services module for a project where an external application is responsible for creating, updating and deleting user accounts in a Drupal based intranet.

Most things are working just fine (thank you all), however there is one point with the User services which I have doubts about:

The user.get service
Requiring the external application to know the uid of the user account a priori doesn't seem to make much sense to me. It would be better to 'get' the user via user-name or e-mail IMHO. This could be implemented with an additonal user.load service.

I have attached a patch which adds this service to the User services. Any comments, observations or improvements gratefully received...

Best regards,

John

Comments

jhl.verona’s picture

StatusFileSize
new4.44 KB

Forgot the attachment, sorry.

An example JSON string for the attributes could be:
{ "Name": "admin", "status": true }

marcingy’s picture

Status: Needs review » Needs work

Please explain why you are replicating code from core user_load in this patch specifically in _user_service_load_user($attrs). User load deals with arrays or uid anyway as a result if we do need a second service then a lot of this code can be removed as it is duplication that we would need to maintain going forwards. The ideal of course would a single user.get service which either takes a uid or an array.

jhl.verona’s picture

Thank you for pointing that out. I was worried about the information coming in from outside. However, I see that _db_query_callback seems to handle all that. The only other thing that springs to mind is that a JSON supplied value would arrive as an object, which the foreach takes care of. Though I have warbled on about that elsewhere.

All in all, it could be reduced to:

function _user_service_load_user($attrs) {
  if (is_object($attrs) {
    $attrs = (array)$attrs
  }
  return user_load($attrs);
}

Badly formed requests will cause SQL errors (but they probably still could even with all that checking code) which will be logged - no bad thing. But it won't stop the request and the external caller will simply get a 404 not found error - not quite right, but good enough.

I agree that it would be better if the user.get service accepted either, but I don't know if that can be done for XML-RPC. It would need an either/or specification for the parameters to avoid breaking current client side code. Something like user.get(int (optional), struct (optional)) perhaps.

gdd’s picture

When you originally brought up this idea I tried to come up with a way to have user_get support both, but couldn't think of one, which is too bad.

I am in favor of reducing it down to a call to user_load(). In general Drupal does not offer a lot of protection from people against themselves, and given that I'm less than inclined to add it in. I wouldn't mind throwing a services_error() in cases where an account isn't found maybe (as user.get does.)

All in all I think this is a good addition though.

jhl.verona’s picture

Merging into user_get would be the best solution, but changing the external API is a dangerous game, there's probably a lot of code out there using Services.

I'm in favour of reducing it to a call to user_load once the input data has been forced to an array.
This could be:

function user_service_load($attrs) {
  $account = user_load((array)$attrs);
  ...
}

and

function user_service_load_access($attrs) {
  global $user;
  $account = user_load((array)$attrs);
  ...
}

At which point, further code reductions can be made, simply by passing the original calls to xxx_get along to xxx_load.
For example:

function user_service_get_access($uid) {
  return user_service_load_access(array("uid" => $uid)); 
}

and

function user_service_get($uid) {
  return user_service_load(array("uid" => $uid));
}

HTH

John

greg.harvey’s picture

Status: Needs work » Needs review
StatusFileSize
new1.59 KB

Hello everyone! After a long absence, I am using Services again for a new client. Yay! I love this module. Anyway...

Where are we with this? Need a new patch?

@heyrocker, what if user.get expected *either* a string or an integer. You could do it by type. If an integer is sent by the calling client then assume $user->uid (all existing code will still function fine) and if a string is sent assume $user->name.

Patch attached. Works like a charm for me! =)

greg.harvey’s picture

Hmmm, is the test bot running against 6.x-2.2 for this thread or against 6.x-2.x? I rolled that patch against 6.x-2.2, which may be why it failed? =/

greg.harvey’s picture

StatusFileSize
new1.72 KB

Hmm, no, Eclipse screwed up the paths to files, trying again having manually corrected.

greg.harvey’s picture

Been thinking about this. While this works, it's probably "illegal" in web services/XML land to have an argument that can be either one thing or another. I'm not sure - would be good to hear from a maintainer on that one. If this is not the way forwards, how about this:

Additional argument, username, string, optional ... *and* make uid optional too. I guess the problem with making uid optional is with protocols like XML-RPC, where all arguments are required, uid must have some "ignore me" value, and it can't be a zero because feasibly someone might want to alter the Anonymous user. Or should we call that an edge case and say if uid == 0 then carry on and check the username argument?

greg.harvey’s picture

I should note I think this patch is really important, as it's highly unlikely an external client wanting to alter a user in Drupal will know Drupal's uid. It's far more likely they'll know the username or email address. uid-only look-up is not very useful. I'd be inclined to "critical" it. Just thinking.

jhl.verona’s picture

@greg.harvey I think that the patch should take into account the various front-ends, as you yourself have remarked with respect to XMLRPC.

FYI Apart from this issue, I am also attempting to get a small group of patches applied to the users service:
http://drupal.org/node/806856 the error code goes AWOL when using the XMLRPC front end
http://drupal.org/node/807638 allow user.services to transparently apply additional protection against specific user modification/deletion - using the User Protect module (a necessary patch to that module has already been committed)
http://drupal.org/node/679494 a plea to change the argument to user.save back to <struct> (as it was) instead of <array> (as it is now)
You may not be interested in all of these requests, obviously.

Personally my opinion is that it is best not to change the existing published user.service API at all - it's an external API, and there will be all sorts of applications which use it. The last thing I want is to offer a patch which could potentially break these applications.

We seem to agree that while user.get(uid) is useful within the Drupal environment, it has less sense to an outside application. Better to use the username or email. I also agree with #10, though if things have been this way for so long, I don't see it as "critical".

Which is why I came to the conclusion of adding a new method - user.load - which would connect directly to the Drupal function user_load. Since this is essentially a superset of the user.get method, I don't think this would actually be such a huge increase in code (see #5).

I'll gladly join forces with you in getting this patch approved, though things seem to have gone very quiet recently. Of course this may be due to other reasons on the part of the maintainers, we all have lives to lead...

greg.harvey’s picture

Status: Needs review » Needs work

Hi John - thanks for jumping back in. Of course, I had not considered that because the new field would be obligatory for the XML-RPC server (and probably others) then it *will* break people's production code.

Oops! Setting back to "needs work". Good catch/point.

Happy with a new method based on #5. We should offer it as a patch, but if the maintainers don't want to take it we can package it as a separate contrib module. I will write it today.

Ps - it's critical to me because I need to look up and alter usernames as a part of a single sign-on implementation and the other application has no concept of Drupal's user ID. Unless it's a Drupal-to-Drupal synchronisation, the current method is virtually unusable, especially since the developers of the other system have absolutely no intention of incorporating the ability to store a Drupal UID either. Why should they? So I think it is critical, because I think my use case is one of the more common ones for people needing this service (distributed single sign-on, username changes in another app and it needs to tell Drupal) - but clearly not many people are using it for this, or, as you say, it would've been raised earlier. Or perhaps they're reviewing the module and walking away/coding their own method and not giving it back? Anyway... I'm ranting ... ;-)

jhl.verona’s picture

Greg - I'd left #5 as an idea, because I was expecting some feedback before writing (yet another) patch. Look forward to seeing/reviewing your patch.

I'm doing (more or less) the same thing - an external application (not mine, and I really don't want to know about it ;-) wants to add/modify/delete users and their roles. Be wary of the fact that, much as within Drupal Web UI itself, uid = 1 can be deleted via Services. Uid = 0 can also be deleted via Services, though it's a little more difficult to do via the Drupal Web UI. The first will cause problems for administrators, the second will probably break Drupal. This is why I am also pushing for a transparent User Protect integration.

greg.harvey’s picture

Status: Needs work » Needs review
StatusFileSize
new2.35 KB

New patch attached. Works fine for me.

jhl.verona’s picture

Status: Needs review » Needs work
StatusFileSize
new3.23 KB

Sorry Greg, but doesn't work for me...

Operations carried out:

  1. Disabled user service
  2. Applied patch
  3. Enabled user service
  4. Went to admin/build/services/browse/user.get set 51 for the uid - perfect
  5. Went to admin/build/services/browse/user.load set { "uid": 51 } as the JSON input for 'array' - got the error "The parameters you supplied did not match any user in the system."

One minor point. I've battled the last two weeks trying to clear up the difference between "array" and "struct" in services (particularly user.save) - now you've called the parameter for user.load "array", of type "struct"! Can we call it "attrs" or something?

A quick dsm($uid); shows that the function user_service_load_access($uid) doesn't receive a number (how could it?), but the attributes. These need to be passed to user_load to get the actual uid.

Here then the patched patch... (I'm not trying to be facetious, I appreciate your help, and I'd really like to see this get through)

greg.harvey’s picture

Hi,

Summary, since this has ended up being longer than I'd like:

1. Works fine with XML-RPC. Feels like a JSON server bug, since your data looks fine - user 51 *does* exist, right?
2. Naming convention inline with core - I'd be reluctant to change that - maintainer needs to make a decision
3. Think this is ok, reasoning below.

Details:

Ok, with XML-RPC same steps, user arguments for method in an array, like so:

$user_args = array(
  0 => array('name' => 'newname'),
);

Just to be sure, tried:

$user_args = array(
  0 => array('uid' => 55),
);

Also worked.

So, I think there must be an issue with the JSON server or perhaps the test UI? Either way, if you try it in code it should work. It certainly works with XML-RPC, I just double-checked.

Onwards: $array is there because it matches the name of the parameter expected by user_load() in the core API. Since we refer users to the API docs, shouldn't the parameters match? I guess what I'm saying is if you don't like the param name $array, I hear you, but we should do what core does and they called it $array. =(

With regard to $uid on access, I couldn't work that out. I think Services handles this and we don't need to worry about it. The function works fine, and anyway the $uid required is the ID of the user wanting to load the user, which was never available in the arguments in hook_service() - hence me thinking this "just works". Thinking about it, if it didn't then we would always get FALSE and an Access Denied message and the method wouldn't work. It does work for me, so it must be ok?

jhl.verona’s picture

Status: Needs work » Needs review
StatusFileSize
new3.23 KB

Sorry if I'm being a pain - and yes, I've spent some time on this too. The service should run irrespective of the server.

1. Yes it does work fine with XMLRPC. I checked too. I won't bore you with all the XML - just the request, which I also checked:

<?xml version="1.0"?>
<methodCall>
<methodName>user.load</methodName>
<params>
  <param>
    <value><string>6018d475e8ddf59814ad89d0c3f8d6e0</string></value>
  </param>
  <param>
    <value><struct>
      <member><name>uid</name><value><int>51</int></value></member>
    </struct></value>
  </param>
</params>
</methodCall>

Yes, uid 51 does exist (user.get worked just fine in my previous post). Yes, XMLRPC gives me back the user <struct> correctly. Same values as user.get(51).

2. Well, yes I see your point. Of course it *is* an array in PHP, called $array, just a <struct> in XMLRPC.

Maybe there *is* an issue with the JSON server, or Web UI. I won't argue that one. Let me just say that it causes the error message because you're getting an object (via the JSON parser), not an array. That's why I simply cast to an array - just as node.save does (services/services/node_service.inc node_service_save($edit) and node_service_save_access($node)).

Ok, let's keep the Drupalers happy, if we're mapping a drupal function, we map the parameter names too - however confusing that may be to a non drupaler/XMLRPCer.

With regard to $uid on access, I simply put a watchdog in there:
watchdog('xmlrpc', print_r($uid, TRUE));
just before global $user; Try it, $uid is an array: Array ( [uid] => 51 ) . In your (first) case it would probably be Array ( [name] => 'newname' )
It works because in the test, while the first test fails; $user->uid is not equal to $uid, the second test passes because $user->uid is still not equal to $uid, *and* user_access('get any user data') is true. So as long as the user logged in ($user) has "get any user data" permission (a very high probability, given the service), it will always return TRUE, not FALSE.

I rest my case, m'lud. New patch with 'array' name attached.

greg.harvey’s picture

Thanks for the updated patch for the uid! Looks good to me - will apply shortly. Don't know how it worked for me, as - as you rightly point out - it should be broken! =/

Does that mean it doesn't work with JSON server at all at the moment? Or it does with a slight workaround?

Anyways, if this works I'll R&BC this patch and maybe it'll get in some time soon. =)

(Ps - don't worry, I never think anyone taking an active interest in a patch is a PITA... appreciate your time as much as you appreciate mine. Nothing wrong with a bit of back and forth when solving a problem!)

jhl.verona’s picture

Yes it works for JSON - at least via the Web UI. The workaround is to cast to an array, as per node.save. However user.save is broken in that respect (IMHO), but that's another battle.
Give it a try XMLRPC and JSON (web UI) It works for me, but it's nice to know if it works for others too!

That it gets in soon is another matter ;-)

Good. A healthy discussion is always better than pistols at dawn (I'm a poor shot). Bon chance!

greg.harvey’s picture

Just looking at this, and reading you notes on the $uid problem with access more carefully, it seems the access function tries to behave like the access function for user profiles, which doesn't make sense. Web services can either access user profiles or they can't. I can't think of a situation where you would want individual users to be able to access their *own* user object via a web service - it's not something individual "people" would be doing, right?

I think we could safely alter that to simply check user_access('get any user data') and that would be fine. Thoughts?

jhl.verona’s picture

As always, it's a bit more complicated than that. If you use API keys (which I'm inferring you do) then you're quite likely to be an anonymous user - see http://drupal.org/node/807638#comment-3013154. I don't use that technique, preferring that the XMLRPC client logs in as a specific user, with obviously a specific role and permissions. (Call me paranoid if you like ;-)

So I think this is the reason for the double test. Though I admit that reading the last paragraph back, I can't see how an anon user can do anything with the user services, given the test. But (my) life is too short for such details...

I'm not prepared to touch that code per se, but the mentioned thread is mine (http://drupal.org/node/807638) where I am trying to intregrate User Protect in a fairly transparent way (using menu_get_item) so that the XMLRPC client can't delete users that I'd prefer to keep, for sentimental reasons, such as uid = 0, and uid = 1, for example.

Again, after a couple of abortive patches, I stopped with a question, or idea if you prefer, but again, it's still waiting a reply...

The point is that I'm coming to the conclusion that I have a different perspective of 'protection' to the maintainers - or the original module developers. This may not be very fair, because I haven't looked at the development code, and hayrocker does mention that "things are changing down the road".

While it is perfectly reasonable that there are no sanity checks within the drupal core functions, that is, if you're creating a module or theme, it's your responsibility to "do the right thing"TM. But if you make these functions available to the outside world, via XMLRPC for example, then some sanity checking is necessary - otherwise a zealous (to use just one adjective) client can easily "do the wrong thing"TM - delete uid = 0, uid = 1, and so on. True, the argument goes that you can do that via the Web UI too, well at least uid = 1, but does that make it right? OK, you say, "there's a module for that", so I'll add User Protect, for example, but I have to hack services because it's not using the 'classic' access functions that I can 'override' via hook_menu_alter().

As another example the XMLRPC parser doesn't take into account the declared parameters and their types. You can easily send a <struct> to user.get and that works just fine (just tried it ;-) - because it doesn't check the type, the access method returns TRUE for the wrong reasons explained above, and you get the user back. Very flexible I admit, but by design? Hmm. Ah, it won't work in the Web UI because that *does* check the types, so there's no JSON filter...

I think I'm ranting, but it's mostly frustration. This is a good module, wildly useful, I just don't want to be clobbered by a malicious competitor who is writing the client side code - hope he doesn't read these threads...

John

greg.harvey’s picture

I understand. I too use the XML-RPC client logs in approach, so anonymous would never be able to use any services... but let's leave it as is. Sounds like tearing it out would probably hold up the patch and open a can of worms.

kylebrowning’s picture

Version: 6.x-2.2 » 7.x-3.x-dev

Unfortunately 2.x is feature frozen. We can move this to 3.x thought.

Renee S’s picture

Is it possible to get this committed? I see in the latest 6 it still ain't :)

gdd’s picture

Status: Needs review » Closed (won't fix)

Well in 3.x you can do this with the index service, so the new branch doesn't need a patch. However, as kylebrowning pointed out, the 2.x branch is feature frozen. Therefore this is a won't fix. Sorry all.

mrryanjohnston’s picture

I was hoping to bring this up again. This feature would be pretty nice for the retrieve method on the user resource for the same reason; users shouldn't have to know uids. This could be a little more difficult since node_load() works a little bit differently than it did in D6: http://api.drupal.org/api/drupal/modules--node--node.module/function/nod.... Should I open up a new issue for this or will this one suffice?

marcingy’s picture

You can use the index sevices as per comment #25 so this stays won't fix, and there is no point in opening a news issue as that will be marked won't as well.

mrryanjohnston’s picture

Are you able to do the index service while using a REST server? The gist on the services page (https://gist.github.com/affc9864487bb1b9c918) only lists Retrieve, Create, Update, Delete, Login, Logout.

marcingy’s picture

Yes take a look at the simpletests they perform an index call for rest. Plus I would look at services pages on d.o rather than git as the project lives on d.o rather than git.

mrryanjohnston’s picture

Ok, I will check out those simpletests! Also, I did find that link on the services page: http://drupal.org/project/services

Look under "Examples"

marcingy’s picture

Yes but is an off site link which means that it may not be maintained, all I meant is don't depend on data about services that is on third party sites.

mrryanjohnston’s picture

Good call! I understand, and thanks for your help with this matter. Just to clarify, you suggest running an index which returns all of the users, then finding from this result set the user with a given username?