Matching multiple LDAP-groups during post-auth
We use LDAP-groups (from AD, if that matters) to map radius-reply-items for general network access on switches, routers, optical gear, etc. Each vendor is their own special flower so needs its own thing. The way we're handling this currently is in post-auth where we specify reply-items per group using if/elseif/else per https://networkradius.com/doc/3.0.10/unlang/keywords.html. This works great if things are simple - you're a member of one group or another. However, we've not yet gotten it to work if one is a member of multiple groups, and to have each group match and get reply-items from each. Example of now: if (LDAP-Group == "network-administrators") { update reply { cisco-avpair = "shell:priv-lvl=15", Service-Type = "6", Juniper-Local-User-Name = "superuser", PaloAlto-Admin-Role = "superuser", } } elsif (LDAP-Group == "network-users") { update reply { cisco-avpair = "shell:priv-lvl=1", Juniper-Local-User-Name = "op", PaloAlto-Admin-Role = "devicereader", } } else { reject } Note that this will do a single match and bail out of the if statement. What we'd like to do is to keep matching accumulatively. The use case is per above it's layer2/3 devices, we'd like to add some other types of gear to the list where someone in the above groups shouldnt have access to, but someone in network-administrators AND $othergroup should have access to. Per Unlang, it looks like there is a case/switch statement but I believe we'd be hitting the same limitation there. tldr; how can we match multiple LDAP-groups and get reply-items from all that match? Cheers, Chris
On Thu, Mar 25, 2021 at 9:01 AM Chris Wopat <me@falz.net> wrote: [snipped]
Note that this will do a single match and bail out of the if statement. What we'd like to do is to keep matching accumulatively. The use case is per above it's layer2/3 devices, we'd like to add some other types of gear to the list where someone in the above groups shouldnt have access to, but someone in network-administrators AND $othergroup should have access to.
Per Unlang, it looks like there is a case/switch statement but I believe we'd be hitting the same limitation there.
tldr; how can we match multiple LDAP-groups and get reply-items from all that match?
Chris, Thank you for posting your snippet, as it was helpful for me to develop a similar setup in my org. :) I've been doing some reading on LDAP-Group, and from what I could find, switch/case doesn't work with it. I'm not entirely sure why, but I saw Alan explicitly tell someone else to use if/elseif instead... that reply was also from several years back, so maybe that's no longer accurate? Have you considered using nested ifs to query twice for the "network-admins AND $othergroup" scenario? If you're concerned about LDAP load / query time, you can enable caching in the ldap module; then the LDAP-Group list is saved and lookups happen against the cache instead of being re-queried each time you ask for it. I'd suggest investigating the syntax "&LDAP-Group[*]" as it seems to be preferred over just "LDAP-Group", as well. Regards, Braden
On 31.03.21 01:56, Braden McGrath via Freeradius-Users wrote:
On Thu, Mar 25, 2021 at 9:01 AM Chris Wopat <me@falz.net> wrote: [snipped]
Note that this will do a single match and bail out of the if statement. What we'd like to do is to keep matching accumulatively. The use case is per above it's layer2/3 devices, we'd like to add some other types of gear to the list where someone in the above groups shouldnt have access to, but someone in network-administrators AND $othergroup should have access to.
Per Unlang, it looks like there is a case/switch statement but I believe we'd be hitting the same limitation there.
tldr; how can we match multiple LDAP-groups and get reply-items from all that match? Chris, Thank you for posting your snippet, as it was helpful for me to develop a similar setup in my org. :) I've been doing some reading on LDAP-Group, and from what I could find, switch/case doesn't work with it. I'm not entirely sure why, but I saw Alan explicitly tell someone else to use if/elseif instead... that reply was also from several years back, so maybe that's no longer accurate?
Have you considered using nested ifs to query twice for the "network-admins AND $othergroup" scenario? If you're concerned about LDAP load / query time, you can enable caching in the ldap module; then the LDAP-Group list is saved and lookups happen against the cache instead of being re-queried each time you ask for it.
I'd suggest investigating the syntax "&LDAP-Group[*]" as it seems to be preferred over just "LDAP-Group", as well.
Regards, Braden - List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html
If the user is member of multiple groups, the LDAP-Group is a Attribute-List (=Array). see the man pages of unlang. I used LDAP-Group array checks to select VPN connections. Please see https://blog.sys4.de/strongswan-vpn-based-on-groups-en.html for my solution. Hope my ideas can help you. Mit freundlichen Grüßen, -- [*] sys4 AG https://sys4.de, +49 (89) 30 90 46 64 Schleißheimer Straße 26/MG,80333 München Sitz der Gesellschaft: München, Amtsgericht München: HRB 199263 Vorstand: Patrick Ben Koetter, Marc Schiffbauer, Wolfgang Stief Aufsichtsratsvorsitzender: Florian Kirstein
On Tue, Mar 30, 2021 at 6:56 PM Braden McGrath via Freeradius-Users <freeradius-users@lists.freeradius.org> wrote:
Have you considered using nested ifs to query twice for the "network-admins AND $othergroup" scenario? If you're concerned about LDAP load / query time, you can enable caching in the ldap module; then the LDAP-Group list is saved and lookups happen against the cache instead of being re-queried each time you ask for it.
using a complicated series of ANDs is something we hope to not have to fall back on due to complexity. Let's say there were 5 groups: * 3 groups for 'network' layer2/3 * 2 groups for 'optical' layer1 .. there's 31 combinations of 5 items, meaning 31 IF statements, which is unruly. Thanks for the caching tip, unsure if this is currently enabled but will do so! On Wed, Mar 31, 2021 at 2:14 AM Michael Schwartzkopff <ms@sys4.de> wrote:
If the user is member of multiple groups, the LDAP-Group is a Attribute-List (=Array).
see the man pages of unlang.
I used LDAP-Group array checks to select VPN connections. Please see
https://blog.sys4.de/strongswan-vpn-based-on-groups-en.html
for my solution. Hope my ideas can help you.
Very helpful thank you. I haven't used "case" before- since it's in a switch, in the link above, you mention " have to use the switch / case structure since a user can be member of multiple groups". Does this mean that the switch/case statement will update the reply for *all* matching cases, not just the first hit? If so this is exactly what I'm looking for, but my brain assumed, due to the "switch", that the first hit would win and it would stop processing after that. --Chris
On Mar 31, 2021, at 7:42 AM, Chris Wopat <me@falz.net> wrote:
using a complicated series of ANDs is something we hope to not have to fall back on due to complexity.
The unfortunate reality is that complexity has to go somewhere.
Let's say there were 5 groups:
* 3 groups for 'network' layer2/3 * 2 groups for 'optical' layer1
.. there's 31 combinations of 5 items, meaning 31 IF statements, which is unruly.
I'm not sure why you need those combinations. Why not just: * do network layer AND * do optical layer? Perhaps the rules really are "network 1 AND optical 1 gets A, network 1 AND optical 2 gets B", with A / B being completely different. In that case, you really do need massive amounts of ANDs. The general solution is to split your rules into independent sets, and then combine the sets.
Very helpful thank you. I haven't used "case" before- since it's in a switch, in the link above, you mention " have to use the switch / case structure since a user can be member of multiple groups". Does this mean that the switch/case statement will update the reply for *all* matching cases, not just the first hit?
It only uses the first one it finds. Why not just do 3 IFs for network later, followed by 2 IFs for optical layer? You can always add an extra "if" to catch the case of "no matching group was found". if network 1 update reply for network 1 else if network 2 ... else update reply with fake attribute saying "no match" if optical 1 update reply for optical 1 else if ... ... else update reply with fake attribute saying "no match" if reply has "no match" reject Alan DeKok.
On Wed, Mar 31, 2021 at 7:04 AM Alan DeKok <aland@deployingradius.com> wrote:
On Mar 31, 2021, at 7:42 AM, Chris Wopat <me@falz.net> wrote:
.. there's 31 combinations of 5 items, meaning 31 IF statements, which is unruly.
I'm not sure why you need those combinations. Why not just:
* do network layer AND * do optical layer?
Perhaps the rules really are "network 1 AND optical 1 gets A, network 1 AND optical 2 gets B", with A / B being completely different. In that case, you really do need massive amounts of ANDs.
The general solution is to split your rules into independent sets, and then combine the sets.
We do this currently, but have an issue with having a reject at the end of each, as it will reject users that are only a part of the 2nd group.
Very helpful thank you. I haven't used "case" before- since it's in a switch, in the link above, you mention " have to use the switch / case structure since a user can be member of multiple groups". Does this mean that the switch/case statement will update the reply for *all* matching cases, not just the first hit?
It only uses the first one it finds.
Why not just do 3 IFs for network later, followed by 2 IFs for optical layer?
You can always add an extra "if" to catch the case of "no matching group was found".
if network 1 update reply for network 1 else if network 2 ... else update reply with fake attribute saying "no match"
if optical 1 update reply for optical 1 else if ... ... else update reply with fake attribute saying "no match"
if reply has "no match" reject
This is very similar to what we're doing now, except your 'no match' stuff, which may in fact be the actual fix. To clarify, you're using some BS reply-item, which is in some vendor dictionary just to store this variable string, then comparing that at the end? In theory this sounds like it would resolve this for us. Looking at the dictionary file, it says to use between 3000 and 4000 for local attributes, perhaps just plopping something like this in that file would work? ATTRIBUTE Local-Reject-Check 3000 string OR perhaps as an int: ATTRIBUTE Local-Reject-Check 3000 integer What we have now is: network: if / elsif / elsif / (no reject currently, or it wouldn't make it to the optical check if not in network group) optical: if / elsif / else reject The solution you list seems sane on paper, we hadn't thought about using a reply item to store a variable for a later check, but assuming that's all possible, I think this is our winner. --Chris
On Mar 31, 2021, at 8:30 AM, Chris Wopat <me@falz.net> wrote:
To clarify, you're using some BS reply-item, which is in some vendor dictionary just to store this variable string, then comparing that at the end? In theory this sounds like it would resolve this for us.
Looking at the dictionary file, it says to use between 3000 and 4000 for local attributes, perhaps just plopping something like this in that file would work?
Yes That would work.
ATTRIBUTE Local-Reject-Check 3000 string
OR perhaps as an int:
ATTRIBUTE Local-Reject-Check 3000 integer
It doesn't really matter. All that matters is if it exists. Maybe make it a string, and then put the value as *why* it didn't match.
What we have now is:
network: if / elsif / elsif / (no reject currently, or it wouldn't make it to the optical check if not in network group)
optical: if / elsif / else reject
The solution you list seems sane on paper, we hadn't thought about using a reply item to store a variable for a later check, but assuming that's all possible, I think this is our winner.
Sounds like it. if network 1 ... ... else { update reply { Local-Reject-Check += "No matching network" } } if optical 1 ... ... else { update reply { Local-Reject-Check += "No matching optical" } } if (&reply:Local-Reject-Check) { reject } And that should do it. Alan DeKok.
On Wed, Mar 31, 2021 at 7:34 AM Alan DeKok <aland@deployingradius.com> wrote:
if network 1 ... ... else { update reply { Local-Reject-Check += "No matching network" } }
if optical 1 ... ... else { update reply { Local-Reject-Check += "No matching optical" } }
if (&reply:Local-Reject-Check) { reject }
And that should do it.
Thanks for the sample config. Just chiming in to say that this *mostly* worked, the last item, which appears to be checking the existence of "Local-Reject-Check" within the reply, didn't work and I'd always get a reject. Note I'm on FreeRADIUS 2 still, on my todo list is upgrade to v3, will plug away at it more when I do that. --Chris
On Apr 6, 2021, at 10:35 AM, Chris Wopat <me@falz.net> wrote:
Thanks for the sample config. Just chiming in to say that this *mostly* worked, the last item, which appears to be checking the existence of "Local-Reject-Check" within the reply, didn't work and I'd always get a reject.
Maybe just give the attribute a value?
Note I'm on FreeRADIUS 2 still, on my todo list is upgrade to v3, will plug away at it more when I do that.
Sure. I haven't looked at v2 in about 3 years, so I can't help much there, sorry. Alan DeKok.
Digging up this thread as I finally am testing with FreeRADIUS 3. I've ported our config over for ldap, things working great in testing. However, I'd like to re-address my last note, which was with the pseudocode of if/else + if/else, we still get a reject. See below: On Tue, Apr 6, 2021 at 12:43 PM Alan DeKok <aland@deployingradius.com> wrote:
On Apr 6, 2021, at 10:35 AM, Chris Wopat <me@falz.net> wrote:
Thanks for the sample config. Just chiming in to say that this *mostly* worked, the last item, which appears to be checking the existence of "Local-Reject-Check" within the reply, didn't work and I'd always get a reject.
Due to my brain comprehending output of `radiusd -X` in v3 *much* better, what appears to be happening is there's never an explicit 'Accept' with the method suggested at: http://lists.freeradius.org/pipermail/freeradius-users/2021-March/099690.htm... In that case, if a member of 'network 1' and 'optical 1', it authenticates. If a member of one but not the other, Local-Reject-Check is populated and it will always reject. Below is a snippet when one is a member of 'network*' group but not 'optical*' group (omitted the network* stuff) (9) User is not a member of "optical-users" (9) elsif (LDAP-Group == "optical-users") -> FALSE (9) else { (9) update reply { (9) Local-Reject-Check += "No matching optical" (9) } # update reply = noop (9) } # else = noop (9) if (&reply:Local-Reject-Check) { (9) if (&reply:Local-Reject-Check) -> TRUE (9) if (&reply:Local-Reject-Check) { (9) [reject] = reject (9) } # if (&reply:Local-Reject-Check) = reject (9) } # post-auth = reject (9) Using Post-Auth-Type Reject I'm wondering if i should look at doing one of these: 1) Explicitly state a "Post-Auth-Type Accept" and somehow change the if logic 2) Somehow mix in vendor attribute to match device type (juniper, cisco, whatever) and *only* look for the network* group when it's a network vendor? I feel as though there's a much simpler answer to both of those, but I can't see the forest for the trees. Cheers, Chris
On May 19, 2021, at 9:48 AM, Chris Wopat <me@falz.net> wrote:
Digging up this thread as I finally am testing with FreeRADIUS 3. I've ported our config over for ldap, things working great in testing.
That's good.
However, I'd like to re-address my last note, which was with the pseudocode of if/else + if/else, we still get a reject.
That happens if none of the LDAP groups match, and your force a reject.
Due to my brain comprehending output of `radiusd -X` in v3 *much* better, what appears to be happening is there's never an explicit 'Accept' with the method suggested at:
There's never an explicit accept, because you told it to reject. If you don't tell it to reject the user, AND the user has a good password, the server sends Accept.
In that case, if a member of 'network 1' and 'optical 1', it authenticates. If a member of one but not the other, Local-Reject-Check is populated and it will always reject.
Likely because that's the way you wrote the rules.
Below is a snippet when one is a member of 'network*' group but not 'optical*' group (omitted the network* stuff)
Post the FULL debug output. This should be your default. You're asking us to help debug things, but only giving a tiny bit of the output. That is very much unhelpful.
I feel as though there's a much simpler answer to both of those, but I can't see the forest for the trees.
Post the FULL debug output. Alan DeKok.
On Wed, May 19, 2021 at 10:31 AM Alan DeKok <aland@deployingradius.com> wrote:
On May 19, 2021, at 9:48 AM, Chris Wopat <me@falz.net> wrote:
Below is a snippet when one is a member of 'network*' group but not 'optical*' group (omitted the network* stuff)
Post the FULL debug output. This should be your default.
You're asking us to help debug things, but only giving a tiny bit of the output. That is very much unhelpful.
Agree in hindsight.
I feel as though there's a much simpler answer to both of those, but I can't see the forest for the trees.
Post the FULL debug output.
Here's the debug of a failed auth where this user was a part of "network-administrators" but not of "optical-*": * http://pastebin.falz.net/1899174 Here's the post-auth section, which I'm also posting in-line in this email. * http://pastebin.falz.net/1899176 post-auth { if (LDAP-Group == "network-administrators") { update reply { cisco-avpair = "shell:priv-lvl=15", Service-Type = "6", Juniper-Local-User-Name = "superuser", PaloAlto-Admin-Role = "superuser", } } elsif (LDAP-Group == "network-users") { update reply { cisco-avpair = "shell:priv-lvl=1", Juniper-Local-User-Name = "op", PaloAlto-Admin-Role = "devicereader", } } elsif (LDAP-Group == "network-cpe-administrators") { update reply { cisco-avpair = "shell:priv-lvl=15", Service-Type = "6", } } else { update reply { Local-Reject-Check += "No matching network" } } if (LDAP-Group == "optical-administrators") { update reply { Ciena-User-Privilege = "super", Infinera-User-Priv-NA = "NA-PRIVILEGED", #<snip more Infinera-User-stuff> Infinera-User-Category = "administrator" } } elsif (LDAP-Group == "optical-users") { update reply { Ciena-User-Privilege = "limited" } } else { update reply { Local-Reject-Check += "No matching optical" } } if (&reply:Local-Reject-Check) { reject } Thanks for taking a look! --Chris
On May 19, 2021, at 2:40 PM, Chris Wopat <me@falz.net> wrote:
Here's the debug of a failed auth where this user was a part of "network-administrators" but not of "optical-*":
... (1) if (LDAP-Group == "network-administrators") -> TRUE Good... (1) ... skipping elsif: Preceding "if" was taken (1) ... skipping elsif: Preceding "if" was taken (1) ... skipping else: Preceding "if" was taken (1) if (LDAP-Group == "optical-administrators") { Hm... not good. TLSMC: MozNSS compatibility interception begins. tlsmc_convert: INFO: cannot open the NSS DB, expecting PEM configuration is present. tlsmc_intercept_initialization: INFO: successfully intercepted TLS initialization. Continuing with OpenSSL only. TLSMC: MozNSS compatibility interception ends. libldap is using the horrible RedHat "NSS" library which pretends to be OpenSSL, and gets it wrong. It might work, but it definitely won't work if you're doing EAP-TLS. (1) elsif (LDAP-Group == "optical-users") -> FALSE (1) else { (1) update reply { ((1) Local-Reject-Check += "No matching optical" The rules check for: if member of network-admin ok else reject AND SEPARATELY if member of optical-admin ok else reject The first test passes, the second doesn't. That's why it's rejecting.
Here's the post-auth section, which I'm also posting in-line in this email.
The description you posted says that the user should match both a network group and an optical group. That's what the rules do. So the question is, what do you want it to do? Write it down in plain english. And it should then be possible to translate it to "unlang". Alan DeKok.
On Wed, May 19, 2021 at 4:39 PM Alan DeKok <aland@deployingradius.com> wrote:
libldap is using the horrible RedHat "NSS" library which pretends to be OpenSSL, and gets it wrong. It might work, but it definitely won't work if you're doing EAP-TLS.
Fortunately it still does work (issues below aside). We'll see if that continues? We're stuck with Centos for the moment, the fact that Redhat killed Centos 8 prematurely (end of this year) may affect taht decision. TBD, possibly for another time.
(1) elsif (LDAP-Group == "optical-users") -> FALSE (1) else { (1) update reply { ((1) Local-Reject-Check += "No matching optical"
The rules check for:
if member of network-admin ok else reject
AND SEPARATELY
if member of optical-admin ok else reject
The first test passes, the second doesn't. That's why it's rejecting.
Here's the post-auth section, which I'm also posting in-line in this email.
The description you posted says that the user should match both a network group and an optical group. That's what the rules do.
So the question is, what do you want it to do? Write it down in plain english. And it should then be possible to translate it to "unlang".
Yes exactly. We're looking to handle situations where: 1) A user may be a member of a single network-* and single optical-* group 2) A user may be a member of a single network-* group (and no optical-*) 3) A user may be a member of a single optical-* group (and no network-*) 1 works fine, 2 and 3 do not. Re-reading my original message and a few follow ups (http://lists.freeradius.org/pipermail/freeradius-users/2021-March/099660.htm...) I can see that this is unclear. It feels like there should be an easy way to nest or deal with this in unlang with some basic if/else stuff but for whatever reason my brain cannot come up with it. We would strongly prefer to not have to create more new LDAP groups for this (ie ones that are called network-and-optical-admins) or whatever, but i guess technically that would be one workaround, but more confusing for those who admin LDAP group membership. Cheers, Chris
On May 20, 2021, at 8:45 AM, Chris Wopat <me@falz.net> wrote:
Yes exactly. We're looking to handle situations where:
1) A user may be a member of a single network-* and single optical-* group 2) A user may be a member of a single network-* group (and no optical-*) 3) A user may be a member of a single optical-* group (and no network-*)
So don't reject if checks (2) or (3) fail. Instead, reject if *all* checks fail.
It feels like there should be an easy way to nest or deal with this in unlang with some basic if/else stuff but for whatever reason my brain cannot come up with it. We would strongly prefer to not have to create more new LDAP groups for this (ie ones that are called network-and-optical-admins) or whatever, but i guess technically that would be one workaround, but more confusing for those who admin LDAP group membership.
You don't need more LDAP groups. You can instead update each matching entry with a special "OK" attribute. And you don't need commas after each entry in an "update" section: if (LDAP-Group == "network-administrators") { update reply { cisco-avpair = "shell:priv-lvl=15" Service-Type = "6" Juniper-Local-User-Name = "superuser" PaloAlto-Admin-Role = "superuser" My-Local-Match = 1 } } elsif (LDAP-Group == "network-users") { update reply { cisco-avpair = "shell:priv-lvl=1" Juniper-Local-User-Name = "op" PaloAlto-Admin-Role = "devicereader" My-Local-Match = 1 } } ... And do the same for the optical groups. Then instead of looking for a reject, look for a match: if (!&reply:My-Local-Match) { reject # didn't match _any_ groups } Alan DeKok.
participants (4)
-
Alan DeKok -
Braden McGrath -
Chris Wopat -
Michael Schwartzkopff