IsRoleEnabled Function for Non-Explicit ACL Entries
Here's a painful lesson I learnt by not testing code thoroughly before delivery. I'd written some code that checked whether the current user had a role enabled in the ACL using the IsRoleEnabled method. All worked well until the customer started to use the code and it broke.
In my testing I had been logged in as users who had explicit ACL entries. The customer was using groups to organise the ACL. The IsRoleEnabled method only works for explicit entries (bug?). To get round this I had to quickly write a custom function to do the job. Here it is:
Function UserHasRole(db As NotesDatabase, UserName As String, RoleName As String ) Dim found As Variant Found = False roles = db.QueryAccessRoles(UserName) Forall role In roles If role = RoleName Then Found = True End If End Forall UserHasRole = Found End Function
It was a quick fix that worked. Now, I'm no LotusScript expert and I don't doubt there's a better way to do this. However, when Notes throws it in your face like this, you just want to get it fixed. Note that I didn't dim the roles variant, as Notes wouldn't let me. The whole thing smells of bugs to me.
In script, I always Evaluate "@IsMember("[Role]"; @UserRoles)". Never had a problem with it.
Jan. That assumes the agent is "run as web user". Unfortunately, mine weren't.
IsRoleEnabled method is under NotesACLEntry Class and helps says that it "Represents a single entry in an access control list. An entry may be for a person, a group, or a server."
So it's only for an explicit entry not for computed ACL for a person.
Also from designer help: "If the name you specify is listed explicitly in the ACL, then QueryAccessRoles returns the roles for that ACL entry and does not check groups."
Since its documented, it doesn't sound like a bug to me.
I think your bypass is very good; I don't think there's another way for server based agents.
I would suggest that IsRoleEnabled is doing exactly what it says on the tin.
flag = notesACLEntry.IsRoleEnabled( name$ )
If you have the ACLEntry for an explicit entry in the ACL then it will return a true/false depending on the named role.
But this is NOT what you actually need. Your QueryAccessRoles *is* what you need. So this is a case of using the right call for the job. Smells of user bugs rather than system bugs to me. Time for an air freshener I think. (^_^)
Dragon. But surely the fact that you can get a handle on a valid NotesACLEntry for any user who is in a group, which is listed in the ACL, means that the IsRoleEnabled method should work. It doesn't say it won't as far as I can see.
But with IsRoleEnabled you're not checking if such a user is *in* the group are you? All you are doing is reading what settings you have in the ACL and checking against the roles for that entry. Sure if you are searching for a particular named entry and it happens to coincide with a username, you would get the valid roles, but you're not explicitly checking the users entry. In fact it could be searching for any user - not necessarily that of the person using the database.
What QueryAccessRoles is doing is actively evaluating the situation. Say as an administrator determining what a given user can/cannot do.
I stand by my statement. It was the wrong tool for the job at hand. Your replacement code does, however, rectify the situation.
Maybe a CFD field on the form with the results of the @IsMember?
Dragon, IMHO the only thing I can think of is, QueryAccessRoles could have been a part of ACL class instead of database class - sounds more logical that way.
Dragon. I'm not convinced. Say you have a user called Joe Bloggs who is listed in the ACL by way of a group and not listed by name at all. The code to get to IsRoleEnabled would look something like this:
Dim entry As NotesACLEntry
Set acl = db.ACL
Set entry = acl.GetEntry( "Joe Bloggs" )
msgbox entry.IsRoleEnabled( "[Supervisor]" )
So, in this case, "entry" is a valid NotesACLEntry that we've gotten using the user's name. Now, as far as I'm concerned, I'd expect any method of the NotesACLEntry class to work on that object, whether it was obtained by user name, group or whatever.
If it's meant to function the way you say then I'd think it was a little counter-intuitive.
One more option:
create hidden single value field 'UserRoles' in your htmlheader subform or directly on the form with the following formula:
@Implode(@Unique(@UserRoles:@UserNamesList);",")
Then in your agent code you can do this:
If Instr(1,doc.UserRoles(0),"[RoleName]") Then
Your Code here ...
End If
What about using the @UserNamesList function?
Jake, you're right on the mark with QueryAccessRoles. The only thing I'd change is to replace the For loop with Arraygetindex:
If Not IsNull(Arraygetindex(roles,RoleName)) Then
Found = True
End If
Oh, and the behaviour of IsRoleEnabled isn't a bug -- that's the way it's documented as behaving. It does, after all, belong to NotesACLEntry, so you can only check the entry. Finding out which entries apply to the user, well, that's another story. The only real way to do this before 6.5 was to use @UserRoles, and that generally meant using a computed field and the NotesDocument parameter in an Evaluate (for the not as web user case).
Stan. I normally take anything you say as gospel, but I'm having real trouble accepting this one as "as designed".
Let's say I'm the code monkey developer who designs and test the database, but never gets to see the ACL. I write the code like so:
Set entry = acl.GetEntry( session.username )
msgbox entry.IsRoleEnabled( "[Supervisor]" )
As I said earlier "entry" is a bona fide NotesACLEntry. Whether or not the user is actually in the ACL is irrelevant to me, the developer. How should I be expected to know that when coding anyway?! The fact is I've got a handle on the NotesACLEntry for that user and I'd expect any method of that class to work. The fact that this one doesn't might be "as designed" but it makes no sense at all to me.
Sorry Jake, but I'm on the side of Stan here.
acl.GetEntry is working EXACTLY as advertised. You're just using the wrong tool for the job.
Mmmm. I think I might have just realised where I was going wrong. I was assuming that getentry worked but isroleenabled didn't. So, you're saying that GetEntry wouldn't have returned a valid NotesACLEntry for the user in a group? My code was breaking one line earlier than I though, by the sounds of it. Doh. Now, where's that Delete Blog button gone...
I think it is much easier than that:
roles = Evaluate("@UserRoles")
forall r in roles
if CStr(r)="[MyRole]" then
found=true
end if
end forall
To get a collection of user roles, I've always used the method that pablosang describes. Works like a charm. It's amazing what you can leverage from Formula language using Evaluate.
By Jove, I think he's got it.
Thats the problem with Domino. There is just so much going on. The documentation leaves a lot to be desired in many places. The fact that they use the same parameters for the various calls but they mean different things.
Set notesACLEntry = notesACL.GetEntry( name$ )
vs
roles = notesDatabase.QueryAccessRoles( name$ )
I've often thought of writing a quickie guide (ala Visibone {Link} ) to detail the structure of LotusScript calls. A bit of colour coding would go a long way to improving the understanding.
Evaluate is slow. Not the execution of the formula itself, but switching between LotusScript run-time and formula execution is (which almost rules out it's use inside of loops). So, evaluate is great, where one line of formula code can replace multiple, possibly complicated lines of LotusScript, especially if it can replace a lengthly loop. And of course, where there's no alternative.
In this case, we do have a native LS alternative that works just as well. Why would you even bother ot introduce potential extra complexity (using an additional language) and potential extra error-proneness (no syntax checking)? In the case of a single @UserRoles this might not be too much of an issue. But I have seen really cruel examples of formula abuse in LS with my own eyes. My current favorite:
currentDate = Evaluate("@Today")
Written by a well known book author in a current Notes 6.5 application ...
Jake
Is there any reason why you declared your Found variable as a variant rather than boolean? I can always remember being told in a Notes class I attended (back in 1952 or something like that) to avoid declaring variants unless absolutely neccessary because of memory allocation issues. Just a thought...
Richard. That's just the way I've always dealt with booleans in LS. In fact I didn't even realise there was the option to declare it as an actual boolean. Doh! That's what you get for never having been to any classes.
I believe the boolean data type is new as of Notes 6, previous to that I always used Integer - as false and true equate to a zero and one.
Yes the boolean type is new with N6. Before N6 you could only use a variant and set it to true/false.
-
In a Forall loop you cannot previosly declare the holding variable. And the holding variable is always of type variant. That is how it is supposed to work.
/Patrix - NotesScript kiddie(tm)
Patrix,
Before N6 (at least on R5!) you could, as Jono said, use an integer var, instead of a variant, and set it to True or False.
Jono,
On Lotusscript, True is -1 , not 1 :)
Pedro, You you could/can but it will not be a boolean type variable then. It will be an integer. To get a real boolean before R6 you had to use the variant type. The code below illustrates the difference. However in practice this doesent matter much.
/Patrik
Sub Initialize
Dim itg As Integer
Dim vari As Boolean
Dim bool As Boolean
itg = True
vari = True
bool = True
Msgbox itg 'Displays -1
Msgbox vari 'Displays True
Msgbox bool 'Displays True
End Sub
Correction:
Dim vari as Boolean
should of course read
Dim vari as Variant