Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
3a25dd7
refactor: Split cmd/authctl/user/user_test.go
adombeck Nov 21, 2025
beb98ae
Fix comment
adombeck Jun 17, 2025
07ab3e3
Add gRPC method SetUserID
adombeck Jun 17, 2025
da51e70
Add `authctl user set-uid` command
adombeck Jun 17, 2025
ade5c8e
Return a gRPC error with code PermissionDenied
adombeck Jun 17, 2025
6f08095
debian/authd.service.in: Grant capability CAP_DAC_READ_SEARCH
adombeck Jun 17, 2025
aefcfe3
fileutils: Add ChownRecursiveFrom
adombeck Jun 17, 2025
a6ded36
testutils: Add RunTestAsRoot
adombeck Nov 20, 2025
e8182c2
Add TestChownRecursiveFrom
adombeck Nov 20, 2025
5c9eb9e
Automatically change owner of home directory when changing UID
adombeck Jun 17, 2025
bce17b1
Add TestSetUIDCommand
adombeck Nov 12, 2025
55e00ff
Add db_test.TestSetUserID
adombeck Nov 17, 2025
bf7ac1a
Support creating users/groups in bubblewrap
adombeck Nov 18, 2025
66279be
tests: Support chown in bubblewrap
adombeck Nov 27, 2025
4a29f2d
tests: Run TestChownRecursiveFrom in bubblewrap
adombeck Dec 1, 2025
1abdc6a
refactor: Remove unused parameter from runInBubbleWrap
adombeck Dec 2, 2025
610eb65
testutils/bubblewrap: Support updating golden files
adombeck Dec 2, 2025
ba7e667
testutils: Skip cleanup when in bubblewrap
adombeck Dec 2, 2025
5bcfbfa
bubblewrap: Mount a tmpfs to /tmp in the sandbox
adombeck Dec 2, 2025
7a24a26
Add users_test.TestSetUserID
adombeck Dec 2, 2025
46a27a1
Add SetGroupID
adombeck Nov 25, 2025
11c2e5d
Add db_test.TestSetGroupID
adombeck Nov 25, 2025
9f465d0
Add users_test.TestSetGroupID
adombeck Dec 2, 2025
61729e5
Add `authctl group` command
adombeck Dec 2, 2025
8624c4d
Add `authctl group set-gid` command
adombeck Dec 2, 2025
a2c49b3
Add TestSetGIDCommand
adombeck Dec 2, 2025
3c4c581
pam/integration-tests: Don't print authctl usage message in VHS tape
adombeck Dec 2, 2025
bec309d
authctl: Add long description for set-uid command
adombeck Dec 2, 2025
7e53750
authctl: Add long description for set-gid command
adombeck Dec 3, 2025
c049caf
Take lock in SetUserID/SetGroupID
adombeck Dec 4, 2025
928e117
tests: Don't skip bubblewrap tests in CI
adombeck Dec 4, 2025
bfd88e6
Try enabling unprivileged user namespaces in CI
adombeck Dec 4, 2025
c28399c
tests: Avoid unshare hanging forever in some environments
adombeck Dec 4, 2025
b42ae41
tests: Make chown work with bubblewrap executed via sudo
adombeck Dec 4, 2025
ade95b8
tests: Skip bubblewrap tests in autopkgtest
adombeck Dec 8, 2025
e4e1c51
ci: Group log lines of llvm-symbolizer installation
adombeck Dec 10, 2025
08a2129
autopkgtest: Run go tests without -v
adombeck Dec 10, 2025
e7e9a58
Check if user is busy before changing its UID
adombeck Dec 11, 2025
ea93e47
authctl: Add argument completion
adombeck Dec 10, 2025
75cc1ee
testlog: Fix duplicate newlines
adombeck Dec 16, 2025
2fc22cf
tests: Improve logging of bubblewrap commands
adombeck Dec 16, 2025
8a30e8e
tests: Improve logging when building authd and authctl
adombeck Dec 16, 2025
37253d1
tests: Improve logging of authctl tests
adombeck Dec 16, 2025
d29de3e
authctl: Tell authd which language to return warnings in
adombeck Dec 19, 2025
b694e95
authctl: Use cobra's Example field
adombeck Jan 5, 2026
e217be7
authctl: Move root command to separate package
adombeck Jan 5, 2026
3355bb3
authctl: Add docgen tool
adombeck Jan 5, 2026
f62f707
authctl: Format chown command as code block
adombeck Jan 5, 2026
25f3251
authctl: Improve set-uid and set-gid description
adombeck Jan 6, 2026
ca60147
authctl: Disable "auto generated by cobra" line
adombeck Jan 6, 2026
a114830
authctl: Add "long" descriptions for lock and unlock commands
adombeck Jan 6, 2026
1d57531
docs: Add generate.go
adombeck Jan 5, 2026
aaded91
docs: Add "authctl" to wordlist
adombeck Jan 6, 2026
eeae34b
docs: Suppress warnings relating to headers
adombeck Jan 6, 2026
08f6ba3
Update cmd/authctl/user/set-uid.go
adombeck Jan 7, 2026
60a8136
Update cmd/authctl/group/set-gid.go
adombeck Jan 7, 2026
0d2cd4b
authctl: Fix grammar of lock/unlock command description
adombeck Jan 7, 2026
63cd457
docs: Generate CLI docs
adombeck Jan 5, 2026
62920d5
docs: Link to CLI docs on reference page
adombeck Jan 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/qa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,10 @@ jobs:
# Print executed commands to ease debugging
set -x

echo "::group::Install llvm-symbolizer"
# For llvm-symbolizer
sudo apt-get install -y llvm
echo "::endgroup::"

go test -C ./pam/internal -json -asan -gcflags=all="${GO_GC_FLAGS}" -failfast -timeout ${GO_TESTS_TIMEOUT} ./... | \
gotestfmt --logfile "${AUTHD_TESTS_ARTIFACTS_PATH}/gotestfmt.pam-internal-asan.log" || exit_code=$?
Expand Down
18 changes: 18 additions & 0 deletions cmd/authctl/group/group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Package group provides utilities for managing group operations.
package group

import (
"github.com/spf13/cobra"
)

// GroupCmd is a command to perform group-related operations.
var GroupCmd = &cobra.Command{
Use: "group",
Short: "Commands related to groups",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { return cmd.Usage() },
}

func init() {
GroupCmd.AddCommand(setGIDCmd)
}
59 changes: 59 additions & 0 deletions cmd/authctl/group/group_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package group_test

import (
"fmt"
"os"
"os/exec"
"testing"

"github.com/ubuntu/authd/internal/testutils"
)

var authctlPath string
var daemonPath string

func TestGroupCommand(t *testing.T) {
t.Parallel()

tests := map[string]struct {
args []string
expectedExitCode int
}{
"Usage_message_when_no_args": {expectedExitCode: 0},
"Help_flag": {args: []string{"--help"}, expectedExitCode: 0},

"Error_on_invalid_command": {args: []string{"invalid-command"}, expectedExitCode: 1},
"Error_on_invalid_flag": {args: []string{"--invalid-flag"}, expectedExitCode: 1},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()

//nolint:gosec // G204 it's safe to use exec.Command with a variable here
cmd := exec.Command(authctlPath, append([]string{"group"}, tc.args...)...)
testutils.CheckCommand(t, cmd, tc.expectedExitCode)
})
}
}

func TestMain(m *testing.M) {
var authctlCleanup func()
var err error
authctlPath, authctlCleanup, err = testutils.BuildAuthctl()
if err != nil {
fmt.Fprintf(os.Stderr, "Setup: %v\n", err)
os.Exit(1)
}
defer authctlCleanup()

var daemonCleanup func()
daemonPath, daemonCleanup, err = testutils.BuildAuthdWithExampleBroker()
if err != nil {
fmt.Fprintf(os.Stderr, "Setup: %v\n", err)
os.Exit(1)
}
defer daemonCleanup()

m.Run()
}
76 changes: 76 additions & 0 deletions cmd/authctl/group/set-gid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package group

import (
"context"
"errors"
"fmt"
"os"
"strconv"

"github.com/spf13/cobra"
"github.com/ubuntu/authd/cmd/authctl/internal/client"
"github.com/ubuntu/authd/cmd/authctl/internal/completion"
"github.com/ubuntu/authd/internal/proto/authd"
)

// setGIDCmd is a command to set the GID of a group managed by authd.
var setGIDCmd = &cobra.Command{
Use: "set-gid <name> <gid>",
Short: "Set the GID of a group managed by authd",
Long: `Set the GID of a group managed by authd to the specified value.

The new GID must be unique and non-negative. The command must be run as root.

When a group's GID is changed, any users whose primary group is set to this
group will have the GID of their primary group updated. The home directories of
these users, and any files within these directories that are owned by the group,
will be updated to the new GID.

Files outside users' home directories are not updated and must be changed
manually. Note that changing a GID can be unsafe if files on the system are
still owned by the original GID: those files may become accessible to a
different group that is later assigned that GID. To change group ownership of
all files on the system from the old GID to the new GID, run:
Comment on lines +29 to +33
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Files outside users' home directories are not updated and must be changed
manually. Note that changing a GID can be unsafe if files on the system are
still owned by the original GID: those files may become accessible to a
different group that is later assigned that GID. To change group ownership of
all files on the system from the old GID to the new GID, run:
Files outside users' home directories are not updated and must be changed
manually.
Note that changing a GID can be unsafe if files on the system are
still owned by the original GID: those files may become accessible to a
different group that is later assigned that GID. To change group ownership of
all files on the system from the old GID to the new GID, run:

Copy link
Contributor

Choose a reason for hiding this comment

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

Can reconsider based on discussion of similar case above.


sudo chown -R --from :OLD_GID :NEW_GID /

`,
Example: ` # Set the GID of group "staff" to 30000
authctl group set-gid staff 30000`,
Args: cobra.ExactArgs(2),
ValidArgsFunction: completion.Groups,
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
gidStr := args[1]
gid, err := strconv.ParseUint(gidStr, 10, 32)
if err != nil {
// Remove the "strconv.ParseUint: parsing ..." part from the error message
// because it doesn't add any useful information.
if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil {
err = unwrappedErr
}
return fmt.Errorf("failed to parse GID %q: %w", gidStr, err)
}

client, err := client.NewUserServiceClient()
if err != nil {
return err
}

resp, err := client.SetGroupID(context.Background(), &authd.SetGroupIDRequest{
Name: name,
Id: uint32(gid),
Lang: os.Getenv("LANG"),
})
if err != nil {
return err
}

// Print any warnings returned by the server.
for _, warning := range resp.Warnings {
fmt.Fprintf(os.Stderr, "Warning: %s\n", warning)
}

return nil
},
}
87 changes: 87 additions & 0 deletions cmd/authctl/group/set-gid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package group_test

import (
"math"
"os"
"os/exec"
"path/filepath"
"strconv"
"testing"

"github.com/stretchr/testify/require"
"github.com/ubuntu/authd/internal/testutils"
"google.golang.org/grpc/codes"
)

func TestSetGIDCommand(t *testing.T) {
// We can't run these tests in parallel because the daemon with the example
// broker which we're using here uses userslocking.Z_ForTests_OverrideLocking()
// which makes userslocking.WriteLock() return an error immediately when the lock
// is already held - unlike the normal behavior which tries to acquire the lock
// for 15 seconds before returning an error.

daemonSocket := testutils.StartAuthd(t, daemonPath,
testutils.WithGroupFile(filepath.Join("testdata", "empty.group")),
testutils.WithPreviousDBState("one_user_and_group"),
testutils.WithCurrentUserAsRoot,
)

err := os.Setenv("AUTHD_SOCKET", daemonSocket)
require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable")

tests := map[string]struct {
args []string
authdUnavailable bool

expectedExitCode int
}{
"Set_group_gid_success": {
args: []string{"set-gid", "group1", "123456"},
expectedExitCode: 0,
},

"Error_when_group_does_not_exist": {
args: []string{"set-gid", "invalidgroup", "123456"},
expectedExitCode: int(codes.NotFound),
},
"Error_when_gid_is_invalid": {
args: []string{"set-gid", "group1", "invalidgid"},
expectedExitCode: 1,
},
"Error_when_gid_is_too_large": {
args: []string{"set-gid", "group1", strconv.Itoa(math.MaxInt32 + 1)},
expectedExitCode: int(codes.Unknown),
},
"Error_when_gid_is_already_taken": {
args: []string{"set-gid", "group1", "0"},
expectedExitCode: int(codes.Unknown),
},
"Error_when_gid_is_negative": {
args: []string{"set-gid", "group1", "-1000"},
expectedExitCode: 1,
},
"Error_when_authd_is_unavailable": {
args: []string{"set-gid", "group1", "123456"},
authdUnavailable: true,
expectedExitCode: int(codes.Unavailable),
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
if tc.authdUnavailable {
origValue := os.Getenv("AUTHD_SOCKET")
err := os.Setenv("AUTHD_SOCKET", "/non-existent")
require.NoError(t, err, "Failed to set AUTHD_SOCKET environment variable")
t.Cleanup(func() {
err := os.Setenv("AUTHD_SOCKET", origValue)
require.NoError(t, err, "Failed to restore AUTHD_SOCKET environment variable")
})
}

//nolint:gosec // G204 it's safe to use exec.Command with a variable here
cmd := exec.Command(authctlPath, append([]string{"group"}, tc.args...)...)
testutils.CheckCommand(t, cmd, tc.expectedExitCode)
})
}
}
17 changes: 17 additions & 0 deletions cmd/authctl/group/testdata/db/one_user_and_group.db.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
users:
- name: user1
uid: 1111
gid: 11111
gecos: |-
User1 gecos
On multiple lines
dir: /home/user1
shell: /bin/bash
broker_id: broker-id
groups:
- name: group1
gid: 11111
ugid: "12345678"
users_to_groups:
- uid: 1111
gid: 11111
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Usage:
authctl group [flags]
authctl group [command]

Available Commands:
set-gid Set the GID of a group managed by authd

Flags:
-h, --help help for group

Use "authctl group [command] --help" for more information about a command.

unknown command "invalid-command" for "authctl group"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Usage:
authctl group [flags]
authctl group [command]

Available Commands:
set-gid Set the GID of a group managed by authd

Flags:
-h, --help help for group

Use "authctl group [command] --help" for more information about a command.

unknown flag: --invalid-flag
13 changes: 13 additions & 0 deletions cmd/authctl/group/testdata/golden/TestGroupCommand/Help_flag
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Commands related to groups

Usage:
authctl group [flags]
authctl group [command]

Available Commands:
set-gid Set the GID of a group managed by authd

Flags:
-h, --help help for group

Use "authctl group [command] --help" for more information about a command.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Usage:
authctl group [flags]
authctl group [command]

Available Commands:
set-gid Set the GID of a group managed by authd

Flags:
-h, --help help for group

Use "authctl group [command] --help" for more information about a command.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Error: connection error: desc = "transport: Error while dialing: dial unix /non-existent: connect: no such file or directory"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Error: GID 0 already exists
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
failed to parse GID "invalidgid": invalid syntax
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Usage:
authctl group set-gid <name> <gid> [flags]

Flags:
-h, --help help for set-gid

unknown shorthand flag: '1' in -1000
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Error: GID 2147483648 is too large to convert to int32
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Error: group "invalidgroup" not found
35 changes: 35 additions & 0 deletions cmd/authctl/internal/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Package client provides a utility function to create a gRPC client for the authd service.
package client

import (
"fmt"
"os"
"regexp"

"github.com/ubuntu/authd/internal/consts"
"github.com/ubuntu/authd/internal/proto/authd"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

// NewUserServiceClient creates and returns a new [authd.UserServiceClient].
func NewUserServiceClient() (authd.UserServiceClient, error) {
authdSocket := os.Getenv("AUTHD_SOCKET")
if authdSocket == "" {
authdSocket = "unix://" + consts.DefaultSocketPath
}

// Check if the socket has a scheme, else default to "unix://"
schemeRegex := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9+.-]*:`)
if !schemeRegex.MatchString(authdSocket) {
authdSocket = "unix://" + authdSocket
}

conn, err := grpc.NewClient(authdSocket, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, fmt.Errorf("failed to connect to authd: %w", err)
}

client := authd.NewUserServiceClient(conn)
return client, nil
}
Loading
Loading