How to identify whether a given UID is a member of a given group - on Solaris or anywhere else.
Jonathan Leffler <[email protected]>
| Newsgroups | gmane.comp.security.sun |
|---|---|
| Message-ID | <OFC78EE5E2.9C7C54E0-ON87256DD7.000648B7-88256DD7.00071FA6@us.ibm.com> |
Bear with me a moment before sputtering "getgrgid(), getgrnam(),
getpwuid(), getpwnam(), setgrent(), getgrent(), endgrent(), setpwent(),
getpwent(), endpwent(), getgroups(), setgroups(), initgroups()...".
The database server I work on needs to identify people 'properly'. For the
purposes of this exercise, the database server is a collection of processes
communicating via shared memory and semaphores, and at least one of the
processes has EUID 0 (root). We've got privileges, in other words, if we
need them.
There are times when we need to establish either:
whether a person connecting to the database server is a member of a
specific group, or
all the groups that the user belongs to.
Superficially, the first job looks trivial - we know which groups we want
to check (by GID), so we can call getgrgid() and see whether the user name
(not UID - oh well) is in the list.
Trivial. Well, yes, but it doesn't work reliably! For why? Because
people cheat. Sysadmin-type people who have large collections of users to
deal with cheat. Sort of...
Well, more precisely, they don't do things as straightforwardly as you'd
like. For example, we have a group 'ccusers' (ClearCase Users - doesn't
matter much) with GID 8714. When you do getgrgid() on Solaris 8 (and all
versions of Solaris as far as we know), you get back a list of 8 users,
including usr[1234]_with_long_name, qausr00[12]xxxxxxxxxxxxxxxxxx32 (enough
x's for the name to be 32 characters long, plus one with 'extra' on the end
for 37 characters total), and ALLCAPNM. I'm in ccusers; I'm not in that
list. So, how? Well, it turns out that there are also groups such as
ccusers-manager which are assigned GID 8714, so there are synonyms for the
GID. How do you get at those extra entries? The only way we know of is
the setgrent(), getgrent() and endgrent() trio - we iterate through the
entire groups database, ignoring groups where the GID is not 8714, and
scanning the user list for those groups where the GID is 8714.
This turns out to be very similar processing to the second query - the
difference is that you can stop the first when you get a positive finding
(but not on a negative), whereas the second will always have to iterate
over all the group entries.
In the Sun manuals, there is some commentary on the nsswitch.conf man page:
Enumeration - getXXXent()
Many of the databases have enumeration functions: passwd has
getpwent(), hosts has gethostent(), and so on. These were
reasonable when the only source was files but often make
little sense for hierarchically structured sources that con-
tain large numbers of entries, much less for multiple
sources. The interfaces are still provided and the implemen-
tations strive to provide reasonable results, but the data
returned may be incomplete (enumeration for hosts is simply
not supported by the dns source), inconsistent (if multiple
sources are used), formatted in an unexpected fashion (for a
host with a canonical name and three aliases, the nisplus
source will return four hostents, and they may not be con-
secutive), or very expensive (enumerating a passwd database
of 5,000 users is probably a bad idea). Furthermore, multi-
ple threads in the same process using the same reentrant
enumeration function (getXXXent_r() are supported beginning
with SunOS 5.3) share the same enumeration position; if they
interleave calls, they will enumerate disjoint subsets of
the same database.
In general, the use of the enumeration functions is depre-
cated. In the case of passwd, shadow, and group, it may
sometimes be appropriate to use fgetgrent(), fgetpwent(),
and fgetspent() (see getgrnam(3C), getpwnam(3C), and
getspnam(3C), respectively), which use only the files
source.
The performance comments are an obvious problem - enumerating everything is
going to be slow, and lots of times, the user is not going to be a member
of the group we're testing for, so the enumeration will be complete. Any
ideas about the warnings on completeness?
The setgroups() function has no role here; it works with a list of groups
already determined. The getgroups() function also has no role - sure, it
tells you which groups the calling process is in, but that is unrelated to
the groups that a separate UID is assigned to. Pity; getgroups() is in
POSIX.1. Since setgroups() is privileged, it is not in POSIX.1 - other
than in the rationale as unsupported because it is privileged (at least,
not 1996, and I didn't see it in the SUS v3, which is roughly POSIX.1
2001).
The initgroups() function is interesting. Its in Solaris - not sure about
other platforms. The specification is:
int initgroups(const char *uname, gid_t basegroup);
Assuming the process is appropriately privileged, this arranges to call
setgroups() with all the groups associated with the user name plus the base
group - which typically is the group number from the password file. It
only processes the first NGROUPS_MAX entries, including the base group.
Do you know anything about the initgroups() function? Is it only on
Solaris? Obviously, it can be simulated fairly readily using the
technology outlined above, but with the same negative side effects - it is
damn slow. For the normal use, it probably doesn't matter, but for us, it
could be a severe pain in the posterior. And using it is not trivial; we'd
have to do getgroups() to retrieve the current groups, initgroups() to set
the correct groups, and then getgroups() to get the groups for the user
followed by a setgroups() to reinstate the original groups. The search for
whether a user belongs to a particular group could be complicated if we
need to track how many group entries the user does appear in so that we
stop looking when we've examined the GROUPS_MAX'th (distinct GID) entry.
At that point, it doesn't matter whether the user is listed in the group or
not - the system would not recognize that they are and would hold back all
subsequent groups, but the naive algorithm would recognize them. This,
unless careful, the database could grant access rights that the user would
not otherwise have.
If the information was held in a relational database, we'd be able to deal
with these queries easily - but that's not where it is held.
Is there a good way for a process to find out which groups a particular
user belongs to? Or to enumerate the groups to which the user actually
does belong? Any answers - whether specific to Solaris or generic (or
specific to any other platform, come to that) would be appreciated.
And thanks for reading this far, and for any help you can give.
PS: I only get the digest version of [email protected]; I won't
see responses until the digest arrives unless you send to me directly.
It's not all that urgent (it's only been a problem for the last decade or
so), but if you need to converse with me, it will be slow.
--
Jonathan Leffler ([email protected])
STSM, Informix Database Engineering, IBM Data Management
4100 Bohannon Drive, Menlo Park, CA 94025
Tel: +1 650-926-6921 Tie-Line: 630-6921
"I don't suffer from insanity; I enjoy every minute of it!"