Skip to content

add or remove users from group memberships based on their age.#317

Merged
laffer1 merged 1 commit intomasterfrom
feature/agev_groups
Mar 26, 2026
Merged

add or remove users from group memberships based on their age.#317
laffer1 merged 1 commit intomasterfrom
feature/agev_groups

Conversation

@laffer1
Copy link
Copy Markdown
Member

@laffer1 laffer1 commented Mar 24, 2026

Adds to groups based on age range. This is for mport package manager use.
AI-Assisted-by: gemini 2.5 pro

Summary by Sourcery

Update aged daemon to maintain age-based Unix group memberships when user ages are updated.

New Features:

  • Automatically adjust user membership in age-based groups (age4p, age13p, age16p, age18p) according to stored age thresholds when user information is updated.

Enhancements:

  • Add helper logic to invoke the system pw tool non-interactively and silently to modify group memberships based on age state.

AI-Assisted-by: gemini 2.5 pro
Signed-off-by: Lucas Holt <luke@foolishgames.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Mar 24, 2026

Reviewer's Guide

Update the aged daemon so that when a user’s age is updated, it synchronizes their membership in age-based system groups (age4p, age13p, age16p, age18p) by calling pw(8) to add or remove them from the corresponding groups and logging results via syslog, while silencing pw output.

Sequence diagram for age update and group synchronization

sequenceDiagram
    actor Client
    participant AgedDaemon
    participant SQLiteDB
    participant PwTool as pw
    participant Syslog

    Client->>AgedDaemon: send age update request (uid, dob)
    AgedDaemon->>SQLiteDB: UPDATE user age
    SQLiteDB-->>AgedDaemon: update complete
    AgedDaemon->>AgedDaemon: getpwuid(target_uid) -> username
    alt username found
        AgedDaemon->>AgedDaemon: update_age_groups(username, age)
        loop for each age group
            AgedDaemon->>AgedDaemon: getgrnam(group)
            alt group exists
                AgedDaemon->>AgedDaemon: check membership in grp->gr_mem
                alt age >= min_age and not in_group
                    AgedDaemon->>PwTool: run_pw_command(group, username, -m)
                    PwTool->>PwTool: fork, execve(/usr/sbin/pw groupmod ...)
                    PwTool-->>AgedDaemon: exit status
                    alt success
                        AgedDaemon->>Syslog: LOG_INFO added user to group
                    else failure
                        AgedDaemon->>Syslog: LOG_ERR failed to add user
                    end
                else age < min_age and in_group
                    AgedDaemon->>PwTool: run_pw_command(group, username, -d)
                    PwTool->>PwTool: fork, execve(/usr/sbin/pw groupmod ...)
                    PwTool-->>AgedDaemon: exit status
                    alt success
                        AgedDaemon->>Syslog: LOG_INFO removed user from group
                    else failure
                        AgedDaemon->>Syslog: LOG_ERR failed to remove user
                    end
                end
            else group missing
                AgedDaemon->>Syslog: LOG_WARNING group not found
            end
        end
    else username not found
        AgedDaemon->>Syslog: LOG_ERR could not find username for uid
    end
    AgedDaemon-->>Client: OK
Loading

Class-style diagram for aged daemon functions related to age groups

classDiagram
    class AgedDaemon {
        +main() int
        +calculate_age(date_string const) int
        +get_range(age int, buffer char*) void
        +init_db() void
        +update_age_groups(username const char*, age int) void
        +run_pw_command(group const char*, user const char*, action const char*) int
    }

    class PwTool {
        +groupmod(group const char*, action const char*, user const char*) int
    }

    class SystemDB {
        +getpwuid(uid_t) struct passwd*
        +getgrnam(group const char*) struct group*
    }

    class SyslogService {
        +syslog(priority int, message const char*) void
    }

    AgedDaemon --> PwTool : uses
    AgedDaemon --> SystemDB : queries
    AgedDaemon --> SyslogService : logs
Loading

File-Level Changes

Change Details Files
Trigger age-based group membership updates whenever a user’s age record is changed.
  • After successfully updating a user’s age in the SQLite database, look up the username from the uid using getpwuid(3).
  • If the username is found, call a new helper to reconcile the user’s membership in age-based groups based on the new age.
  • Log an error via syslog if the uid cannot be resolved to a username when attempting a group update.
usr.sbin/aged/aged.c
Add a helper to run pw(8) group modification commands non-interactively and without emitting output.
  • Introduce run_pw_command() which forks, sets a controlled PATH environment, and execve(2)s /usr/sbin/pw groupmod with appropriate arguments.
  • Redirect the child’s stdout and stderr to /dev/null using open(2) and dup2(2) so pw runs silently.
  • Wait for the pw child process with waitpid(2) and return its exit status, logging errors on fork/wait failures.
usr.sbin/aged/aged.c
Implement logic to compute and enforce membership in age-based groups from a user’s age.
  • Define update_age_groups() that iterates over the configured age-based groups (age4p, age13p, age16p, age18p) with associated minimum ages.
  • For each group, look it up via getgrnam(3), determine if the target user is already listed in gr_mem, and decide whether the user should be in the group based on age versus minimum age.
  • Use run_pw_command() with -m to add users who meet the age threshold but are not members; use -d to remove users who fall below the threshold but are members.
  • Log missing groups as warnings and log success or failure of each add/remove via syslog.
usr.sbin/aged/aged.c
Update includes to support new process and file descriptor handling.
  • Include <sys/wait.h> for waitpid(2) and related macros.
  • Include <fcntl.h> for open(2) flags when redirecting pw output to /dev/null.
usr.sbin/aged/aged.c

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The waitpid call in run_pw_command should handle EINTR (e.g., loop until it returns a child or a non-EINTR error) to avoid spuriously returning -1 when the wait is interrupted by a signal.
  • In update_age_groups, consider defensively checking that grp->gr_mem is non-NULL before iterating over it, so that a group with no members (or an unusual getgrnam implementation) cannot lead to a NULL dereference.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `waitpid` call in `run_pw_command` should handle `EINTR` (e.g., loop until it returns a child or a non-EINTR error) to avoid spuriously returning `-1` when the wait is interrupted by a signal.
- In `update_age_groups`, consider defensively checking that `grp->gr_mem` is non-NULL before iterating over it, so that a group with no members (or an unusual `getgrnam` implementation) cannot lead to a NULL dereference.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The groups[] and min_ages[] arrays are kept in parallel and must remain in sync; consider consolidating them into a single array of structs (e.g., {name, min_age}) to make future changes less error-prone.
  • In run_pw_command, when execve fails the parent only sees a generic non-zero status; you might want to log the failing command and errno in the child before _exit(127) to aid debugging of pw invocation issues.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `groups[]` and `min_ages[]` arrays are kept in parallel and must remain in sync; consider consolidating them into a single array of structs (e.g., `{name, min_age}`) to make future changes less error-prone.
- In `run_pw_command`, when `execve` fails the parent only sees a generic non-zero status; you might want to log the failing command and `errno` in the child before `_exit(127)` to aid debugging of pw invocation issues.

## Individual Comments

### Comment 1
<location path="usr.sbin/aged/aged.c" line_range="343" />
<code_context>
+	run_pw_command(const char *group, const char *user, const char *action)
</code_context>
<issue_to_address>
**suggestion:** Consider redirecting stdin to /dev/null as well when invoking `pw`.

Right now only stdout and stderr are redirected. If `pw groupmod` (or future `pw` calls) ever read from stdin, they’ll inherit the daemon’s stdin and may block or interfere with it. In the child, also dup2 `STDIN_FILENO` to `/dev/null` before `execve` so the subprocess is fully detached from the daemon’s stdio.
</issue_to_address>

### Comment 2
<location path="usr.sbin/aged/aged.c" line_range="374-381" />
<code_context>
+	int num_groups = sizeof(groups) / sizeof(groups[0]);
+
+	for (int i = 0; i < num_groups; i++) {
+		struct group *grp = getgrnam(groups[i]);
+		if (grp == NULL) {
+			syslog(LOG_WARNING, "Group %s not found.", groups[i]);
+			continue;
+		}
+
+		int in_group = 0;
+		for (int j = 0; grp->gr_mem[j] != NULL; j++) {
+			if (strcmp(grp->gr_mem[j], username) == 0) {
+				in_group = 1;
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against a potential NULL `gr_mem` pointer from `getgrnam` before iterating.

Some libc implementations set `gr_mem` to NULL for groups without members. In that case, `for (int j = 0; grp->gr_mem[j] != NULL; j++)` will dereference a NULL pointer. Please guard for `grp->gr_mem == NULL` before entering the loop and skip the iteration (e.g., set `in_group = 0` and continue) to avoid a potential crash.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread usr.sbin/aged/aged.c
syslog(LOG_ERR, "fork failed: %m");
return -1;
} else if (pid == 0) {
int fd = open("/dev/null", O_WRONLY);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider redirecting stdin to /dev/null as well when invoking pw.

Right now only stdout and stderr are redirected. If pw groupmod (or future pw calls) ever read from stdin, they’ll inherit the daemon’s stdin and may block or interfere with it. In the child, also dup2 STDIN_FILENO to /dev/null before execve so the subprocess is fully detached from the daemon’s stdio.

Comment thread usr.sbin/aged/aged.c
Comment on lines +374 to +381
struct group *grp = getgrnam(groups[i]);
if (grp == NULL) {
syslog(LOG_WARNING, "Group %s not found.", groups[i]);
continue;
}

int in_group = 0;
for (int j = 0; grp->gr_mem[j] != NULL; j++) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against a potential NULL gr_mem pointer from getgrnam before iterating.

Some libc implementations set gr_mem to NULL for groups without members. In that case, for (int j = 0; grp->gr_mem[j] != NULL; j++) will dereference a NULL pointer. Please guard for grp->gr_mem == NULL before entering the loop and skip the iteration (e.g., set in_group = 0 and continue) to avoid a potential crash.

@laffer1 laffer1 merged commit b883117 into master Mar 26, 2026
7 of 15 checks passed
@laffer1 laffer1 deleted the feature/agev_groups branch March 26, 2026 04:17
laffer1 added a commit that referenced this pull request Mar 26, 2026
AI-Assisted-by: gemini 2.5 pro

Signed-off-by: Lucas Holt <luke@foolishgames.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant