Python’s grp module reads the Unix group database (/etc/group), returning struct_group named tuples. import grp. getgrnam: grp.getgrnam(name) → struct_group; raises KeyError if not found. getgrgid: grp.getgrgid(gid) → struct_group. getgrall: grp.getgrall() → list[struct_group]. struct_group fields: gr_name (str), gr_passwd (str, usually "x"), gr_gid (int), gr_mem (list of member username strings). Current process GID: os.getgid() / os.getegid(). All group IDs for a user: os.getgrouplist(user, gid) (returns list of ints). Add supplementary group: requires root — os.setgroups(gids). Check membership: username in grp.getgrnam("docker").gr_mem. Windows: not available. Claude Code generates group membership validators, permission checkers, privileged-group guards, and user-group audit reporters.
CLAUDE.md for grp
## grp Stack
- Stdlib: import grp, os
- By name: entry = grp.getgrnam("sudo") # KeyError if not found
- By GID: entry = grp.getgrgid(0) # root group
- All: entries = grp.getgrall()
- Fields: .gr_name .gr_passwd .gr_gid .gr_mem (list of usernames)
- Current: os.getgid() os.getegid()
- Members: os.getgrouplist(username, primary_gid) # returns [int, ...]
grp Unix Group Pipeline
# app/grputil.py — group lookup, membership, audit, privilege guard
from __future__ import annotations
import os
import platform
from dataclasses import dataclass
from typing import Iterator
_GRP_AVAILABLE = platform.system() != "Windows"
if _GRP_AVAILABLE:
import grp
# ─────────────────────────────────────────────────────────────────────────────
# 1. Basic lookups
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class GroupEntry:
name: str
gid: int
members: list[str]
def __str__(self) -> str:
members = ", ".join(self.members) if self.members else "(none)"
return f"gid={self.gid:5d} {self.name} [{members}]"
def lookup_by_name(name: str) -> GroupEntry | None:
"""
Look up a group by name. Returns None if not found.
Example:
g = lookup_by_name("docker")
if g: print(g.members)
"""
if not _GRP_AVAILABLE:
return None
try:
e = grp.getgrnam(name)
return GroupEntry(name=e.gr_name, gid=e.gr_gid, members=list(e.gr_mem))
except KeyError:
return None
def lookup_by_gid(gid: int) -> GroupEntry | None:
"""
Look up a group by GID. Returns None if not found.
Example:
g = lookup_by_gid(os.getgid())
if g: print(g.name)
"""
if not _GRP_AVAILABLE:
return None
try:
e = grp.getgrgid(gid)
return GroupEntry(name=e.gr_name, gid=e.gr_gid, members=list(e.gr_mem))
except KeyError:
return None
def all_groups() -> list[GroupEntry]:
"""
Return all group entries from the system database.
Example:
for g in all_groups():
if g.members: print(g)
"""
if not _GRP_AVAILABLE:
return []
return [
GroupEntry(name=e.gr_name, gid=e.gr_gid, members=list(e.gr_mem))
for e in grp.getgrall()
]
# ─────────────────────────────────────────────────────────────────────────────
# 2. Membership checks
# ─────────────────────────────────────────────────────────────────────────────
def user_in_group(username: str, group_name: str) -> bool:
"""
Return True if username is listed as a member of group_name in /etc/group.
Does NOT check primary group membership (see user_groups() for that).
Example:
if user_in_group("alice", "docker"):
print("alice can use Docker")
"""
if not _GRP_AVAILABLE:
return False
try:
return username in grp.getgrnam(group_name).gr_mem
except KeyError:
return False
def user_groups(username: str, primary_gid: int | None = None) -> list[GroupEntry]:
"""
Return all groups a user belongs to, including their primary group.
If primary_gid is None, tries to look it up from the pwd module.
Example:
import pwd
pw = pwd.getpwnam("alice")
for g in user_groups("alice", pw.pw_gid):
print(g)
"""
if not _GRP_AVAILABLE:
return []
# Determine primary GID
if primary_gid is None:
try:
import pwd as _pwd
primary_gid = _pwd.getpwnam(username).pw_gid
except (ImportError, KeyError):
primary_gid = os.getgid()
gids = os.getgrouplist(username, primary_gid)
results: list[GroupEntry] = []
for gid in gids:
g = lookup_by_gid(gid)
if g:
results.append(g)
return results
def current_process_groups() -> list[GroupEntry]:
"""
Return all groups the current process belongs to.
Example:
for g in current_process_groups():
print(g)
"""
if not _GRP_AVAILABLE:
return []
results = []
for gid in os.getgroups():
g = lookup_by_gid(gid)
if g:
results.append(g)
return results
def process_in_group(group_name: str) -> bool:
"""
Return True if the current process has the named group in its supplementary
group set (including effective GID).
Example:
if process_in_group("sudo"):
print("process has sudo group access")
"""
if not _GRP_AVAILABLE:
return False
try:
target_gid = grp.getgrnam(group_name).gr_gid
except KeyError:
return False
return target_gid in (os.getegid(), *os.getgroups())
# ─────────────────────────────────────────────────────────────────────────────
# 3. Privilege guard
# ─────────────────────────────────────────────────────────────────────────────
class InsufficientGroupError(PermissionError):
"""Raised when the process lacks the required group membership."""
pass
def require_group(group_name: str) -> None:
"""
Assert the current process belongs to group_name.
Raises InsufficientGroupError if not.
Example:
require_group("docker")
# ... code that needs docker group
"""
if not process_in_group(group_name):
g = lookup_by_name(group_name)
gid_str = f" (gid={g.gid})" if g else " (group not found)"
raise InsufficientGroupError(
f"Process must be a member of group '{group_name}'{gid_str}. "
f"Current groups: {[g.name for g in current_process_groups()]}"
)
def require_root() -> None:
"""
Assert the current process is running as root (UID 0 or GID 0).
Raises PermissionError if not.
Example:
require_root()
# privileged operation
"""
if os.geteuid() != 0:
raise PermissionError(
f"Operation requires root (UID 0). Current UID: {os.geteuid()}"
)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Group audit
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class GroupAudit:
total_groups: int
groups_with_members: int
largest_group: GroupEntry | None
empty_groups: list[GroupEntry]
privileged_groups: list[GroupEntry] # GID < 100
def __str__(self) -> str:
largest = self.largest_group.name if self.largest_group else "—"
return (
f"total={self.total_groups} "
f"with_members={self.groups_with_members} "
f"empty={len(self.empty_groups)} "
f"privileged={len(self.privileged_groups)} "
f"largest={largest}"
)
def audit_groups() -> GroupAudit:
"""
Produce a summary audit of the system group database.
Example:
audit = audit_groups()
print(audit)
for g in audit.privileged_groups:
print(f" {g}")
"""
groups = all_groups()
if not groups:
return GroupAudit(0, 0, None, [], [])
with_members = [g for g in groups if g.members]
empty = [g for g in groups if not g.members]
privileged = [g for g in groups if g.gid < 100]
largest = max(with_members, key=lambda g: len(g.members)) if with_members else None
return GroupAudit(
total_groups=len(groups),
groups_with_members=len(with_members),
largest_group=largest,
empty_groups=empty,
privileged_groups=privileged,
)
def find_groups_for_user_pattern(username_pattern: str) -> list[tuple[GroupEntry, list[str]]]:
"""
Find all groups that have members matching a simple substring pattern.
Returns list of (GroupEntry, matching_members).
Example:
for g, members in find_groups_for_user_pattern("alice"):
print(g.name, members)
"""
results = []
for g in all_groups():
matching = [m for m in g.members if username_pattern in m]
if matching:
results.append((g, matching))
return results
def groups_by_gid_range(low: int, high: int) -> list[GroupEntry]:
"""
Return groups whose GID falls in [low, high] (inclusive).
Example:
system_groups = groups_by_gid_range(0, 999)
"""
return [g for g in all_groups() if low <= g.gid <= high]
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
print("=== grp demo ===")
if not _GRP_AVAILABLE:
print(" grp not available on Windows — skipping")
raise SystemExit(0)
# ── current process groups ─────────────────────────────────────────────────
print("\n--- current process groups ---")
proc_groups = current_process_groups()
for g in proc_groups[:5]:
print(f" {g}")
if len(proc_groups) > 5:
print(f" ... and {len(proc_groups) - 5} more")
# ── lookup by GID ──────────────────────────────────────────────────────────
print("\n--- lookup by GID ---")
for gid in [0, os.getgid()]:
g = lookup_by_gid(gid)
print(f" gid={gid}: {g}")
# ── lookup by name ─────────────────────────────────────────────────────────
print("\n--- lookup by name ---")
for name in ["root", "nogroup", "nobody"]:
g = lookup_by_name(name)
print(f" {name!r}: {'found — ' + str(g) if g else 'not found'}")
# ── process_in_group ───────────────────────────────────────────────────────
print("\n--- process_in_group ---")
for name in ["root", "docker", "sudo"]:
print(f" in '{name}': {process_in_group(name)}")
# ── audit ──────────────────────────────────────────────────────────────────
print("\n--- audit_groups ---")
audit = audit_groups()
print(f" {audit}")
print(f" privileged group names: {[g.name for g in audit.privileged_groups[:8]]}")
# ── GID range ─────────────────────────────────────────────────────────────
print("\n--- system groups (GID 0–99) ---")
sys_groups = groups_by_gid_range(0, 99)
for g in sys_groups[:8]:
print(f" {g}")
if len(sys_groups) > 8:
print(f" ... and {len(sys_groups) - 8} more")
print("\n=== done ===")
For the pwd alternative — pwd.getpwnam(name) and pwd.getpwuid(uid) query the user database (/etc/passwd) and return struct_passwd entries with pw_name, pw_uid, pw_gid, pw_dir, and pw_shell — use pwd when you need user-level identity (UID, home directory, login shell); use grp when you need group-level authority (GID, group membership list, supplementary groups). The two modules are complementary: grp.getgrall() enumerates groups while os.getgrouplist(username, primary_gid) bridges from a user to their group IDs. For the os alternative — os.getgroups() returns the list of supplementary GIDs for the current process, and os.getgid()/os.getegid() return primary/effective GID — use os.getgroups() when you only need GID integers for permission checks; use grp when you need the group name string or the full member list from the group database. The Claude Skills 360 bundle includes grp skill sets covering lookup_by_name()/lookup_by_gid()/all_groups() database readers, user_in_group()/user_groups()/current_process_groups()/process_in_group() membership checkers, require_group()/require_root() privilege guards, and GroupAudit with audit_groups()/find_groups_for_user_pattern()/groups_by_gid_range(). Start with the free tier to try group database patterns and grp pipeline code generation.