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!"
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.